@opengeni/runtime 0.4.0 → 0.6.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.
@@ -1,6 +1,6 @@
1
1
  export { collectSandboxEnvironment, parseExposedPorts } from '@opengeni/config';
2
2
  export { CAPABILITY_DESCRIPTORS, CapabilityDescriptor, DESKTOP_STREAM_PORT, StreamTokenPayload, StreamTokenPayload as StreamTokenPayloadType, TERMINAL_STREAM_PORT } from '@opengeni/contracts';
3
3
  import '@openai/agents/sandbox';
4
- export { A as ActiveBackendResolverDeps, a as ActiveBackendUnresolvableError, b as ActivePointer, C as ChannelAConflictError, c as ChannelAEmitter, d as ChannelAExecArgs, e as ChannelAExecResult, f as ChannelANotFoundError, g as ChannelASession, h as ChannelAUnsupportedError, i as ChannelAValidationError, j as ControlRpc, D as DEFAULT_DESKTOP_GEOMETRY, k as DISPLAY_STACK_TIMEOUT_MS, l as DesktopGeometry, m as DisplayStackError, n as DisplayStackUnsupportedError, E as EnsureDisplayStackOptions, o as EnsureDisplayStackResult, p as EnsureTerminalServerOptions, q as EnsureTerminalServerResult, r as EstablishedSandboxSession, s as ExposeStreamPortInput, t as ExposeStreamPortResult, u as ExposedPortEndpoint, F as FinalizeRecordingResult, L as LiveModalSandboxLeaseAttribution, M as MintStreamTokenInput, v as MockAgentResponder, w as MockAgentResponderOptions, x as MockExecHandler, y as ModalOrphanSweepResult, z as ModalSandboxAttribution, N as NatsControlRpc, B as NatsRequestConnection, G as NegotiationContext, H as NumstatEntry, P as PROVIDER_REGISTRY, I as ProviderConstructionContext, J as ProviderRegistration, K as RecordingCodec, O as RecordingContentType, Q as RecordingError, S as RecordingProcess, T as RecordingUnavailableError, U as ResolvedActiveBackend, V as RoutableBackendSession, W as RoutableSandbox, X as RoutingSandboxSession, Y as RoutingSandboxSessionDeps, Z as RoutingTransitionEvent, _ as RoutingUnsupportedError, $ as SELFHOSTED_DEFAULT_TIMEOUT_MS, a0 as SELFHOSTED_RECONNECT_WINDOW_MS, a1 as SELFHOSTED_RELAY_STREAM_PATH, a2 as STREAM_PORT, a3 as STREAM_TOKEN_DEFAULT_TTL_SECONDS, a4 as SandboxChannelAService, a5 as SandboxChannelAServiceOptions, a6 as SandboxConfigError, a7 as SandboxCreatedCallback, a8 as SandboxProviderUnavailableError, a9 as SelfhostedApplyDiff, aa as SelfhostedControlError, ab as SelfhostedEditor, ac as SelfhostedEnrollment, ad as SelfhostedExecArgs, ae as SelfhostedExecResult, af as SelfhostedImageOutput, ag as SelfhostedLivenessState, ah as SelfhostedNegotiationInput, ai as SelfhostedRelayConfig, aj as SelfhostedSandboxClient, ak as SelfhostedSession, al as SelfhostedSessionBuild, am as SelfhostedSessionDeps, an as SelfhostedSessionState, ao as SelfhostedUnavailableReason, ap as StartRecordingInput, aq as StreamPortUnavailableError, ar as TERMINAL_SERVER_TIMEOUT_MS, as as TerminalServerError, at as TerminalServerUnsupportedError, au as agentErrorToControlError, av as assertDescriptorRegistryInvariants, aw as assertProviderRegistryInvariants, ax as assertSafeRelPath, ay as backendSupportsOs, az as buildDisplayStackScript, aA as buildSelfhostedBackendSession, aB as buildStreamUrl, aC as buildTerminalServerScript, aD as contentTypeForCodec, aE as createSandboxClient, aF as createSandboxClientForBackend, aG as decodeModalSnapshotId, aH as deletePriorPersistedSnapshot, aI as deleteRecordingArtifacts, aJ as deserializeSandboxSessionStateEnvelope, aK as desktopCapableBackend, aL as ensureDisplayStack, aM as ensureTerminalServer, aN as establishSandboxSessionFromEnvelope, aO as exposeStreamPort, aP as extForCodec, aQ as isExecSessionLostBanner, aR as isProviderSandboxNotFoundError, aS as isSelfhostedProviderNotFoundError, aT as isWorkspaceEscapeError, aU as makeActiveBackendResolver, aV as mintStreamToken, aW as modalSandboxAttributionEnvironment, aX as modalSandboxAttributionTags, aY as negotiateCapabilities, aZ as negotiateSelfhostedCapabilities, a_ as offlineAgentError, a$ as offlineControlResponse, b0 as parseExecBannerSessionId, b1 as parseNumstatZ, b2 as parsePorcelainV2, b3 as parseUnifiedPatch, b4 as readRecordingBytes, b5 as readWorkspaceArchiveFromEnvelopeSessionState, b6 as recordingStorageKey, b7 as restoredSandboxSessionStateFromEntry, b8 as sandboxStateEntryFromRunState, b9 as selectBackend, ba as selfhostedLiveness, bb as serializeEstablishedSandboxEnvelope, bc as setSelfhostedApplyDiff, bd as startRecording, be as stopRecording, bf as stripExecBanner, bg as subjectFor, bh as sweepModalOrphanSandboxes, bi as tagModalSandbox, bj as tearDownDisplayStack, bk as tearDownTerminalServer, bl as terminateModalSandboxById, bm as timeoutAgentError, bn as timeoutControlResponse, bo as verifyStreamToken } from '../index-CSGkld-v.js';
4
+ export { A as ActiveBackendResolverDeps, a as ActiveBackendUnresolvableError, b as ActivePointer, C as ChannelAConflictError, c as ChannelAEmitter, d as ChannelAExecArgs, e as ChannelAExecResult, f as ChannelANotFoundError, g as ChannelASession, h as ChannelAUnsupportedError, i as ChannelAValidationError, j as ControlRpc, D as DEFAULT_DESKTOP_GEOMETRY, k as DISPLAY_STACK_TIMEOUT_MS, l as DesktopGeometry, m as DisplayStackError, n as DisplayStackUnsupportedError, E as EnsureDisplayStackOptions, o as EnsureDisplayStackResult, p as EnsureTerminalServerOptions, q as EnsureTerminalServerResult, r as EstablishedSandboxSession, s as ExposeStreamPortInput, t as ExposeStreamPortResult, u as ExposedPortEndpoint, F as FinalizeRecordingResult, L as LiveModalSandboxLeaseAttribution, M as MintStreamTokenInput, v as MockAgentResponder, w as MockAgentResponderOptions, x as MockExecHandler, y as ModalModuleLoader, z as ModalOrphanSweepResult, B as ModalSandboxAttribution, N as NatsControlRpc, G as NatsRequestConnection, H as NegotiationContext, I as NumstatEntry, P as PROVIDER_REGISTRY, J as ProviderConstructionContext, K as ProviderRegistration, O as RecordingCodec, Q as RecordingContentType, S as RecordingError, T as RecordingProcess, U as RecordingUnavailableError, V as ResolvedActiveBackend, W as RoutableBackendSession, X as RoutableSandbox, Y as RoutingSandboxSession, Z as RoutingSandboxSessionDeps, _ as RoutingTransitionEvent, $ as RoutingUnsupportedError, a0 as SELFHOSTED_DEFAULT_TIMEOUT_MS, a1 as SELFHOSTED_RECONNECT_WINDOW_MS, a2 as SELFHOSTED_RELAY_STREAM_PATH, a3 as STREAM_PORT, a4 as STREAM_TOKEN_DEFAULT_TTL_SECONDS, a5 as SandboxChannelAService, a6 as SandboxChannelAServiceOptions, a7 as SandboxConfigError, a8 as SandboxCreatedCallback, a9 as SandboxProviderUnavailableError, aa as SelfhostedApplyDiff, ab as SelfhostedControlError, ac as SelfhostedEditor, ad as SelfhostedEnrollment, ae as SelfhostedExecArgs, af as SelfhostedExecResult, ag as SelfhostedImageOutput, ah as SelfhostedLivenessState, ai as SelfhostedNegotiationInput, aj as SelfhostedRelayConfig, ak as SelfhostedSandboxClient, al as SelfhostedSession, am as SelfhostedSessionBuild, an as SelfhostedSessionDeps, ao as SelfhostedSessionState, ap as SelfhostedUnavailableReason, aq as StartRecordingInput, ar as StreamPortUnavailableError, as as TERMINAL_SERVER_TIMEOUT_MS, at as TerminalServerError, au as TerminalServerUnsupportedError, av as agentErrorToControlError, aw as assertDescriptorRegistryInvariants, ax as assertProviderRegistryInvariants, ay as assertSafeRelPath, az as backendSupportsOs, aA as buildDisplayStackScript, aB as buildSelfhostedBackendSession, aC as buildStreamUrl, aD as buildTerminalServerScript, aE as contentTypeForCodec, aF as createSandboxClient, aG as createSandboxClientForBackend, aH as decodeModalSnapshotId, aI as deletePriorPersistedSnapshot, aJ as deleteRecordingArtifacts, aK as deserializeSandboxSessionStateEnvelope, aL as desktopCapableBackend, aM as ensureDisplayStack, aN as ensureModalRegistryImage, aO as ensureTerminalServer, aP as establishSandboxSessionFromEnvelope, aQ as exposeStreamPort, aR as extForCodec, aS as isExecSessionLostBanner, aT as isProviderSandboxNotFoundError, aU as isSelfhostedProviderNotFoundError, aV as isWorkspaceEscapeError, aW as makeActiveBackendResolver, aX as mintStreamToken, aY as modalSandboxAttributionEnvironment, aZ as modalSandboxAttributionTags, a_ as negotiateCapabilities, a$ as negotiateSelfhostedCapabilities, b0 as offlineAgentError, b1 as offlineControlResponse, b2 as parseExecBannerSessionId, b3 as parseNumstatZ, b4 as parsePorcelainV2, b5 as parseUnifiedPatch, b6 as readRecordingBytes, b7 as readWorkspaceArchiveFromEnvelopeSessionState, b8 as recordingStorageKey, b9 as restoredSandboxSessionStateFromEntry, ba as sandboxStateEntryFromRunState, bb as selectBackend, bc as selfhostedLiveness, bd as serializeEstablishedSandboxEnvelope, be as setSelfhostedApplyDiff, bf as startRecording, bg as stopRecording, bh as stripExecBanner, bi as subjectFor, bj as sweepModalOrphanSandboxes, bk as tagModalSandbox, bl as tearDownDisplayStack, bm as tearDownTerminalServer, bn as terminateModalSandboxById, bo as timeoutAgentError, bp as timeoutControlResponse, bq as verifyStreamToken } from '../index-otZkoLYq.js';
5
5
  import 'modal';
6
6
  import '@opengeni/agent-proto';
@@ -53,6 +53,7 @@ import {
53
53
  deserializeSandboxSessionStateEnvelope,
54
54
  desktopCapableBackend,
55
55
  ensureDisplayStack,
56
+ ensureModalRegistryImage,
56
57
  ensureTerminalServer,
57
58
  establishSandboxSessionFromEnvelope,
58
59
  exposeStreamPort,
@@ -95,7 +96,7 @@ import {
95
96
  timeoutAgentError,
96
97
  timeoutControlResponse,
97
98
  verifyStreamToken
98
- } from "../chunk-HGQ252FL.js";
99
+ } from "../chunk-BHA2SU3H.js";
99
100
  export {
100
101
  ActiveBackendUnresolvableError,
101
102
  CAPABILITY_DESCRIPTORS,
@@ -151,6 +152,7 @@ export {
151
152
  deserializeSandboxSessionStateEnvelope,
152
153
  desktopCapableBackend,
153
154
  ensureDisplayStack,
155
+ ensureModalRegistryImage,
154
156
  ensureTerminalServer,
155
157
  establishSandboxSessionFromEnvelope,
156
158
  exposeStreamPort,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/runtime",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./dist/index.js",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@opengeni/agent-proto": "^0.2.1",
34
- "@opengeni/config": "^0.3.0",
34
+ "@opengeni/config": "^0.4.0",
35
35
  "@opengeni/contracts": "^0.9.0",
36
36
  "@openai/agents": "^0.11.6",
37
37
  "@openai/agents-extensions": "^0.11.6",
package/src/index.ts CHANGED
@@ -937,6 +937,25 @@ export function appendSessionInstructions(composed: string, sessionInstructions?
937
937
  return trimmed ? `${composed} ${trimmed}` : composed;
938
938
  }
939
939
 
940
+ /**
941
+ * Appends the generic programmatic-tool-calling (toolspace) directive to the
942
+ * composed workspace + CORE instructions, joined by " ". This is GENERIC
943
+ * substrate prompting — the same text for every host, never per-host copy.
944
+ *
945
+ * Included ONLY when `toolspaceAvailable` is true, which the caller sets from the
946
+ * exact condition that gates the sandbox token mint: the toolspace feature is
947
+ * enabled AND a toolspace token was minted for THIS turn. That mint now happens
948
+ * on every backend including selfhosted (connected machines get the token too),
949
+ * so the block appears there as well; a turn with no minted token (feature off)
950
+ * has no toolspace URL/token in its sandbox and must not advertise a capability
951
+ * that is not there — the gate is false and this is a no-op. Placed BEFORE the
952
+ * per-session instructions so host/session specificity still wins over this
953
+ * substrate note.
954
+ */
955
+ export function appendToolspaceInstructions(composed: string, toolspaceAvailable: boolean): string {
956
+ return toolspaceAvailable ? `${composed} ${TOOLSPACE_PROGRAMMATIC_DIRECTIVE}` : composed;
957
+ }
958
+
940
959
  /**
941
960
  * Appends the one-shot genesis title directive (genesis turn only), joined by
942
961
  * " " and always LAST so a white-label persona template or a per-session
@@ -1030,14 +1049,21 @@ export function buildOpenGeniAgent(settings: Settings, resources: ResourceRef[],
1030
1049
  // Persona composition order (all one system-level instructions string):
1031
1050
  // 1. workspace instructionsTemplate (or deployment default) with the
1032
1051
  // non-bypassable CORE substituted at {{core}} — composeAgentInstructions,
1033
- // 2. + the per-session persona instructions (session-specific, LAST so it
1034
- // refines the workspace persona),
1035
- // 3. + the one-shot genesis title directive (genesis turn only).
1036
- // With no session instructions and no genesis hint this is byte-identical to
1037
- // the historical composed instructions.
1052
+ // 2. + the generic programmatic-tool-calling (toolspace) directive, ONLY
1053
+ // when a toolspace token was minted for this turn (feature enabled + a
1054
+ // per-turn seed — the mint gate, which includes selfhosted turns since
1055
+ // they now receive the token too) — appendToolspaceInstructions,
1056
+ // 3. + the per-session persona instructions (session-specific, so it
1057
+ // refines both the workspace persona and the substrate note),
1058
+ // 4. + the one-shot genesis title directive (genesis turn only).
1059
+ // With no toolspace token, no session instructions, and no genesis hint this
1060
+ // is byte-identical to the historical composed instructions.
1038
1061
  instructions: appendGenesisTitleDirective(
1039
1062
  appendSessionInstructions(
1040
- composeAgentInstructions(options.instructionsTemplate ?? settings.agentInstructionsTemplate, options.workspaceEnvironment),
1063
+ appendToolspaceInstructions(
1064
+ composeAgentInstructions(options.instructionsTemplate ?? settings.agentInstructionsTemplate, options.workspaceEnvironment),
1065
+ settings.toolspaceEnabled && Boolean(options.toolspaceTokenSeed),
1066
+ ),
1041
1067
  options.sessionInstructions,
1042
1068
  ),
1043
1069
  options.genesisTitleHint,
@@ -2194,6 +2220,13 @@ export type ContextRobustnessFilterOptions = {
2194
2220
  export const GENESIS_TITLE_DIRECTIVE =
2195
2221
  "This is the first turn of a new session. Before responding to the user, call the opengeni__set_session_title tool with a concise 3-7 word title that summarizes what this session is about, then address the user's request normally.";
2196
2222
 
2223
+ // Generic substrate prompting for programmatic tool calling (toolspace). Same
2224
+ // text for every host; gated per-turn by appendToolspaceInstructions on the
2225
+ // presence of a minted toolspace token, so it only appears when the sandbox
2226
+ // actually exposes the ogtool CLI + $OPENGENI_TOOLSPACE_URL/_TOKEN_FILE.
2227
+ export const TOOLSPACE_PROGRAMMATIC_DIRECTIVE =
2228
+ "Every tool on your MCP surface is also callable programmatically from the sandbox shell, so scripts can invoke tools without a model round trip per call. Run `ogtool list` to see the available tools and their input schemas (from tools/list), then `ogtool call <tool-name> '<json-args>'`; equivalently, POST MCP JSON-RPC to $OPENGENI_TOOLSPACE_URL with the bearer token read from $OPENGENI_TOOLSPACE_TOKEN_FILE. Prefer programmatic calls for loops, polling, and bulk filtering: their results stay in the sandbox and do not consume your context window. Tools that require human approval must still be invoked normally — called programmatically they return a typed error.";
2229
+
2197
2230
  /**
2198
2231
  * callModelInputFilter that removes provider-assigned item ids (rs_/msg_/fc_…)
2199
2232
  * from every input item immediately before each model call. Responses-API
@@ -2398,6 +2431,22 @@ export async function runAgentStream(agent: Agent<any, any>, input: PreparedAgen
2398
2431
  ...(runAs ? { runAs } : {}),
2399
2432
  });
2400
2433
  }
2434
+ } else {
2435
+ // SELFHOSTED TOOLSPACE (parity): the platform setup hooks (repository clone,
2436
+ // az login) and file materialization stay OFF the user's real machine — but
2437
+ // the toolspace token seed is the ONE piece of per-turn material that must
2438
+ // reach it. It writes a scoped, own-session-bound token to
2439
+ // $OPENGENI_TOOLSPACE_TOKEN_FILE over the SAME exec channel the clone-seed
2440
+ // uses (off-manifest, value never on the manifest), and it grants no more
2441
+ // than the machine owner's own authority (toolspace:call, this session, turn
2442
+ // TTL, budgeted, approval-tools excluded) — the invariant that makes it safe
2443
+ // to cross to a user machine when the git token is not. It is also the
2444
+ // machine's only path to programmatic tool calling. Seed it (only) here; the
2445
+ // hook list is empty when no toolspace token was minted for this turn.
2446
+ const toolspaceHooks = sandboxToolspaceTokenHooksForAgent(agent);
2447
+ if (toolspaceHooks.length > 0) {
2448
+ await runBeforeAgentStartHooks(setupSession, toolspaceHooks, ownedHookContext);
2449
+ }
2401
2450
  }
2402
2451
  // Keep the decoration as a safety net for any session the SDK does create/resume
2403
2452
  // through the client during this run (it is inert for the provided session).
@@ -55,12 +55,14 @@ export {
55
55
  type ProviderConstructionContext,
56
56
  } from "./providers";
57
57
  export {
58
+ ensureModalRegistryImage,
58
59
  modalSandboxAttributionEnvironment,
59
60
  modalSandboxAttributionTags,
60
61
  sweepModalOrphanSandboxes,
61
62
  tagModalSandbox,
62
63
  terminateModalSandboxById,
63
64
  type LiveModalSandboxLeaseAttribution,
65
+ type ModalModuleLoader,
64
66
  type ModalOrphanSweepResult,
65
67
  type ModalSandboxAttribution,
66
68
  } from "./providers/modal";
@@ -87,8 +87,9 @@ export const modalProvider: ProviderRegistration = {
87
87
  if (settings.modalWorkspacePersistence) {
88
88
  options.workspacePersistence = settings.modalWorkspacePersistence;
89
89
  }
90
- if (settings.modalImageRef) {
91
- options.image = ModalImageSelector.fromTag(settings.modalImageRef);
90
+ const imageSelector = resolveModalImageSelector(settings);
91
+ if (imageSelector) {
92
+ options.image = imageSelector;
92
93
  }
93
94
  if (settings.modalTokenId) {
94
95
  options.tokenId = settings.modalTokenId;
@@ -106,6 +107,110 @@ export const modalProvider: ProviderRegistration = {
106
107
  type ModalModule = typeof import("modal");
107
108
  type ModalClientLike = InstanceType<ModalModule["ModalClient"]>;
108
109
 
110
+ // --- Private-registry image resolution (OPENGENI_MODAL_IMAGE_REGISTRY_SECRET) ------
111
+ //
112
+ // The Agents-extension Modal backend resolves `modalImageRef` via
113
+ // `Image.fromRegistry(tag)` with NO secret, so it can only pull PUBLIC images. To run
114
+ // a PRIVATE image we resolve the named Modal Secret and pre-build the authenticated
115
+ // `fromRegistry(tag, secret)` image ONCE per process, then hand the provider `build`
116
+ // a `ModalImageSelector.fromImage(...)`. `build` is synchronous and modal is imported
117
+ // lazily (never loaded for non-modal backends), so resolution can't happen inside
118
+ // `build`; the worker boot awaits `ensureModalRegistryImage` first and `build` reads
119
+ // the settled result. Modal images are lazy, workspace-scoped definitions, so an image
120
+ // built by this module's client is usable by the ModalSandboxClient's own client.
121
+
122
+ /** Loader seam so unit tests can inject a fake modal module. */
123
+ export type ModalModuleLoader = () => Promise<
124
+ Pick<ModalModule, "ModalClient" | "Secret">
125
+ >;
126
+
127
+ const defaultModalLoader: ModalModuleLoader = () => import("modal");
128
+
129
+ /** Settled, synchronously-readable resolved images, keyed per config. */
130
+ const resolvedRegistryImages = new Map<string, unknown>();
131
+ /** In-flight resolutions, for cross-call de-duplication. */
132
+ const inFlightRegistryImages = new Map<string, Promise<void>>();
133
+
134
+ function registryImageCacheKey(settings: Settings): string {
135
+ return [
136
+ settings.modalImageRef ?? "",
137
+ settings.modalImageRegistrySecret ?? "",
138
+ settings.modalEnvironment ?? "",
139
+ ].join("|");
140
+ }
141
+
142
+ /**
143
+ * Resolve + cache the private-registry Modal image. No-op unless BOTH
144
+ * `modalImageRef` and `modalImageRegistrySecret` are set. Memoized per
145
+ * (imageRef, secret, environment) so it runs once per worker process. Awaited at
146
+ * worker boot BEFORE the first sandbox is created; `build` then reads the resolved
147
+ * image and otherwise falls back to the public `fromTag` path.
148
+ */
149
+ export async function ensureModalRegistryImage(
150
+ settings: Settings,
151
+ loadModal: ModalModuleLoader = defaultModalLoader,
152
+ ): Promise<void> {
153
+ if (!settings.modalImageRegistrySecret || !settings.modalImageRef) {
154
+ return;
155
+ }
156
+ const key = registryImageCacheKey(settings);
157
+ if (resolvedRegistryImages.has(key)) {
158
+ return;
159
+ }
160
+ let pending = inFlightRegistryImages.get(key);
161
+ if (!pending) {
162
+ pending = (async () => {
163
+ const modal = await loadModal();
164
+ const client = new modal.ModalClient(modalClientOptions(settings));
165
+ const secret = await modal.Secret.fromName(
166
+ settings.modalImageRegistrySecret!,
167
+ settings.modalEnvironment ? { environment: settings.modalEnvironment } : undefined,
168
+ );
169
+ // fromRegistry is synchronous and returns a lazy image definition (built
170
+ // server-side at sandbox create); the resolved secretId travels with it.
171
+ const image = client.images.fromRegistry(settings.modalImageRef!, secret);
172
+ resolvedRegistryImages.set(key, image);
173
+ })().finally(() => {
174
+ inFlightRegistryImages.delete(key);
175
+ });
176
+ inFlightRegistryImages.set(key, pending);
177
+ }
178
+ await pending;
179
+ }
180
+
181
+ /** The resolved private-registry image for these settings, or undefined if none. */
182
+ function cachedModalRegistryImage(settings: Settings): unknown | undefined {
183
+ if (!settings.modalImageRegistrySecret || !settings.modalImageRef) {
184
+ return undefined;
185
+ }
186
+ return resolvedRegistryImages.get(registryImageCacheKey(settings));
187
+ }
188
+
189
+ /**
190
+ * Choose the image selector for a Modal sandbox client from settings. Returns:
191
+ * - `fromImage(resolved)` when a private-registry secret is configured AND the
192
+ * image has been resolved (ensureModalRegistryImage ran at worker boot);
193
+ * - `fromTag(modalImageRef)` for the public path (no secret, or cold cache — the
194
+ * resume/attach paths never pull an image so the tag branch is harmless there);
195
+ * - `undefined` when no image ref is set (Modal uses its default image).
196
+ * Exported for unit tests.
197
+ */
198
+ export function resolveModalImageSelector(settings: Settings): ModalImageSelector | undefined {
199
+ if (!settings.modalImageRef) {
200
+ return undefined;
201
+ }
202
+ const registryImage = cachedModalRegistryImage(settings);
203
+ return registryImage
204
+ ? ModalImageSelector.fromImage(registryImage as Parameters<typeof ModalImageSelector.fromImage>[0])
205
+ : ModalImageSelector.fromTag(settings.modalImageRef);
206
+ }
207
+
208
+ /** Test-only: clear the resolved/in-flight image caches. */
209
+ export function __resetModalRegistryImageCacheForTest(): void {
210
+ resolvedRegistryImages.clear();
211
+ inFlightRegistryImages.clear();
212
+ }
213
+
109
214
  function modalClientOptions(settings: Settings): ConstructorParameters<ModalModule["ModalClient"]>[0] {
110
215
  return {
111
216
  ...(settings.modalTokenId ? { tokenId: settings.modalTokenId } : {}),
@@ -144,6 +144,28 @@ export interface SelfhostedImageOutput {
144
144
  * (the turn pauses + retries); it is NOT a hard failure. */
145
145
  export const SELFHOSTED_DEFAULT_TIMEOUT_MS = 30_000;
146
146
 
147
+ /**
148
+ * The ONLY manifest-environment keys exported into a selfhosted machine's exec
149
+ * shell. The manifest env is otherwise NOT consumed by selfhosted exec — the
150
+ * machine owns its own environment, and pushing HOME, GIT_ vars, and the like
151
+ * onto a real computer would clobber it. The toolspace pointers are the exception:
152
+ * `ogtool` (and the token-seed hook) need the non-secret programmatic-calling URL
153
+ * and the token-FILE PATH to reach the tool surface. The token VALUE never rides
154
+ * here — it is seeded to that file off-manifest over the same exec channel.
155
+ */
156
+ const SELFHOSTED_EXEC_ENV_ALLOWLIST = ["OPENGENI_TOOLSPACE_URL", "OPENGENI_TOOLSPACE_TOKEN_FILE"] as const;
157
+
158
+ function selfhostedExecEnv(environment: Record<string, string>): Record<string, string> {
159
+ const env: Record<string, string> = {};
160
+ for (const key of SELFHOSTED_EXEC_ENV_ALLOWLIST) {
161
+ const value = environment[key];
162
+ if (value) {
163
+ env[key] = value;
164
+ }
165
+ }
166
+ return env;
167
+ }
168
+
147
169
  /** The relay-URL shape config the session needs to build a stream endpoint. M8b
148
170
  * wires the real relay deployment behind THIS seam so `buildStreamUrl` works
149
171
  * unchanged behind `resolveExposedPort`. */
@@ -181,9 +203,11 @@ export interface SelfhostedSessionDeps {
181
203
  * session delta; `validateNoEnvironmentDelta` throws "Live sandbox sessions cannot
182
204
  * change manifest environment variables" on ANY env mismatch. So `state.manifest`'s
183
205
  * `environment` MUST EQUAL the turn's environment for the delta to be empty. The
184
- * selfhosted exec routes over NATS and does NOT consume the env, but the manifest
185
- * must carry it for parity. Omitted → `{}` (the negotiation-only / test path,
186
- * which never applies a turn manifest, so there is no delta to validate).
206
+ * selfhosted exec routes over NATS and does NOT consume this env wholesale (the
207
+ * machine owns its own shell), EXCEPT the allowlisted toolspace pointers it
208
+ * exports per exec so `ogtool` works — see SELFHOSTED_EXEC_ENV_ALLOWLIST. The
209
+ * manifest carries the full env for parity. Omitted → `{}` (the negotiation-only
210
+ * / test path, which never applies a turn manifest, so there is no delta).
187
211
  */
188
212
  environment?: Record<string, string>;
189
213
  /**
@@ -340,7 +364,11 @@ export class SelfhostedSession {
340
364
  // SELFHOSTED_VIRTUAL_ROOT). Empty → the session workingDir (itself "" by
341
365
  // default ⇒ the agent runs in its workspace_root).
342
366
  cwd: toMachinePath(args.workdir, this.workingDir),
343
- env: {},
367
+ // The machine owns its own shell environment; the manifest env is NOT
368
+ // exported here. The ONE exception is the non-secret toolspace pointers
369
+ // (URL + token-file path), so a programmatic `ogtool` call and the token-
370
+ // seed hook can find the tool surface. See SELFHOSTED_EXEC_ENV_ALLOWLIST.
371
+ env: selfhostedExecEnv(this.state.environment),
344
372
  stdin: new Uint8Array(0),
345
373
  timeoutMs: 0,
346
374
  };