@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.
- package/dist/chunk-2PO56VAL.js +3478 -0
- package/dist/chunk-2PO56VAL.js.map +1 -0
- package/dist/index.d.ts +912 -0
- package/dist/index.js +3663 -0
- package/dist/index.js.map +1 -0
- package/dist/sandbox/index.d.ts +1738 -0
- package/dist/sandbox/index.js +187 -0
- package/dist/sandbox/index.js.map +1 -0
- package/package.json +49 -0
- package/src/bundled_hashicorp_terraform_skills/LICENSE +373 -0
- package/src/bundled_hashicorp_terraform_skills/README.md +18 -0
- package/src/bundled_hashicorp_terraform_skills/UPSTREAM_GIT_SHA +1 -0
- package/src/bundled_hashicorp_terraform_skills/azure-verified-modules/SKILL.md +613 -0
- package/src/bundled_hashicorp_terraform_skills/checkov/SKILL.md +43 -0
- package/src/bundled_hashicorp_terraform_skills/refactor-module/SKILL.md +538 -0
- package/src/bundled_hashicorp_terraform_skills/social-media-marketing/SKILL.md +35 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-search-import/SKILL.md +372 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-search-import/references/MANUAL-IMPORT.md +113 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-search-import/scripts/list_resources.sh +38 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/SKILL.md +480 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/api-monitoring.md +543 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/component-blocks.md +476 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/deployment-blocks.md +391 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/examples.md +1529 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/linked-stacks.md +187 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/troubleshooting.md +671 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-style-guide/SKILL.md +353 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/SKILL.md +451 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/references/CI_CD.md +80 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/references/EXAMPLES.md +314 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/references/MOCK_PROVIDERS.md +171 -0
- package/src/codex-tool-search.ts +267 -0
- package/src/context-compaction.ts +538 -0
- package/src/history-sanitizer.ts +719 -0
- package/src/index.ts +3299 -0
- package/src/sandbox/capabilities.ts +69 -0
- package/src/sandbox/channel-a.ts +1031 -0
- package/src/sandbox/display-stack.ts +231 -0
- package/src/sandbox/errors.ts +34 -0
- package/src/sandbox/index.ts +832 -0
- package/src/sandbox/providers/blaxel.ts +35 -0
- package/src/sandbox/providers/cloudflare.ts +24 -0
- package/src/sandbox/providers/daytona.ts +34 -0
- package/src/sandbox/providers/docker.ts +17 -0
- package/src/sandbox/providers/e2b.ts +36 -0
- package/src/sandbox/providers/index.ts +107 -0
- package/src/sandbox/providers/local.ts +13 -0
- package/src/sandbox/providers/modal.ts +55 -0
- package/src/sandbox/providers/none.ts +13 -0
- package/src/sandbox/providers/runloop.ts +32 -0
- package/src/sandbox/providers/selfhosted.ts +96 -0
- package/src/sandbox/providers/types.ts +38 -0
- package/src/sandbox/providers/vercel.ts +29 -0
- package/src/sandbox/recording.ts +286 -0
- package/src/sandbox/routing/backend-resolver.ts +189 -0
- package/src/sandbox/routing/routing-session.ts +455 -0
- package/src/sandbox/select.ts +371 -0
- package/src/sandbox/selfhosted/capabilities.ts +255 -0
- package/src/sandbox/selfhosted/control-rpc.ts +351 -0
- package/src/sandbox/selfhosted/session.ts +930 -0
- package/src/sandbox/selfhosted/testing.ts +230 -0
- package/src/sandbox/stream-port.ts +185 -0
- package/src/sandbox/stream-token.ts +90 -0
- package/src/sandbox/terminal-server.ts +203 -0
- 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
|
+
};
|