@opengeni/runtime 0.7.0 → 0.11.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 (49) hide show
  1. package/THIRD_PARTY_NOTICES +32 -0
  2. package/dist/agent-UMUIWWD2.js +8 -0
  3. package/dist/agent-UMUIWWD2.js.map +1 -0
  4. package/dist/chunk-4YJ5Z52Z.js +1789 -0
  5. package/dist/chunk-4YJ5Z52Z.js.map +1 -0
  6. package/dist/chunk-DPULLAJG.js +215 -0
  7. package/dist/chunk-DPULLAJG.js.map +1 -0
  8. package/dist/chunk-K4V4QYCL.js +37683 -0
  9. package/dist/chunk-K4V4QYCL.js.map +1 -0
  10. package/dist/chunk-PZ5AY32C.js +10 -0
  11. package/dist/chunk-PZ5AY32C.js.map +1 -0
  12. package/dist/chunk-YUAFQEFY.js +21522 -0
  13. package/dist/chunk-YUAFQEFY.js.map +1 -0
  14. package/dist/{index-DIiiwQY2.d.ts → index-D6yi-JcU.d.ts} +138 -15
  15. package/dist/index.d.ts +333 -122
  16. package/dist/index.js +8260 -349
  17. package/dist/index.js.map +1 -1
  18. package/dist/localSources-T3HZ6KY4.js +15 -0
  19. package/dist/localSources-T3HZ6KY4.js.map +1 -0
  20. package/dist/sandbox/index.d.ts +1 -1
  21. package/dist/sandbox/index.js +42 -5
  22. package/dist/skill-library.d.ts +74 -0
  23. package/dist/skill-library.js +18 -0
  24. package/dist/skill-library.js.map +1 -0
  25. package/package.json +13 -5
  26. package/src/bundled_hashicorp_terraform_skills/README.md +3 -3
  27. package/src/bundled_hashicorp_terraform_skills/checkov/SKILL.md +1 -1
  28. package/src/context-compaction.ts +421 -24
  29. package/src/history-sanitizer.ts +14 -5
  30. package/src/index.ts +1451 -304
  31. package/src/sandbox/channel-a.ts +535 -97
  32. package/src/sandbox/index.ts +36 -2
  33. package/src/sandbox/routing/routing-session.ts +10 -2
  34. package/src/sandbox/run-credentials.ts +818 -0
  35. package/src/sandbox/select.ts +3 -3
  36. package/src/sandbox/selfhosted/capabilities.ts +1 -1
  37. package/src/sandbox/selfhosted/op-stream.ts +40 -0
  38. package/src/sandbox/selfhosted/op-testing.ts +50 -3
  39. package/src/sandbox/selfhosted/session.ts +17 -4
  40. package/src/sandbox/selfhosted/testing.ts +1 -1
  41. package/src/sandbox/stream-port.ts +1 -1
  42. package/src/sandbox/stream-token.ts +1 -1
  43. package/src/sandbox/terminal-server.ts +6 -2
  44. package/src/sandbox/toolspace-token.ts +147 -0
  45. package/src/sandbox/turn-tool-cancellation.ts +940 -0
  46. package/src/skill-library.ts +323 -0
  47. package/dist/chunk-RYDCVTMW.js +0 -5531
  48. package/dist/chunk-RYDCVTMW.js.map +0 -1
  49. /package/src/{bundled_hashicorp_terraform_skills → bundled_skill_library}/azure-verified-modules/SKILL.md +0 -0
@@ -3,7 +3,7 @@
3
3
  // negotiateCapabilities() turns a static CapabilityDescriptor + runtime context
4
4
  // (the selected OS, the lease liveness/epoch, and the deployment's desktop
5
5
  // policy) into a coherent SessionCapabilities document. The load-bearing rule
6
- // (master-spine Part D): a capability cell is ALWAYS present — when unavailable
6
+ // (sandbox contract Part D): a capability cell is ALWAYS present — when unavailable
7
7
  // it is `available:false` + a typed `reason`, NEVER absent. Degradation is a
8
8
  // value, not a silent drop.
9
9
 
@@ -45,7 +45,7 @@ export interface NegotiationContext {
45
45
  * (settings.computerUseReadOnly). v1 default false (the agent clicks/types). */
46
46
  computerUseReadOnly?: boolean;
47
47
  /**
48
- * Whether a scoped-stream-token secret is resolvable (I8/OD-8). When desktop
48
+ * Whether a scoped-stream-token secret is resolvable (stream-token availability contract). When desktop
49
49
  * is enabled but this is false (no streamTokenSecret AND no delegationSecret),
50
50
  * the desktop plane GRACEFULLY DEGRADES to transport:null — the deployment
51
51
  * boots, but the pixel plane cannot mint scoped tokens. Defaults to true so a
@@ -279,7 +279,7 @@ export function negotiateCapabilities(ctx: NegotiationContext): SessionCapabilit
279
279
  available = false;
280
280
  reason = "disabled_by_policy";
281
281
  } else if (ctx.streamTokenSecretAvailable === false) {
282
- // Graceful degrade (I8/OD-8): desktop is enabled + backend-capable, but no
282
+ // Graceful degrade (stream-token availability contract): desktop is enabled + backend-capable, but no
283
283
  // stream-token secret is resolvable, so no scoped token can be minted. The
284
284
  // deployment boots; the desktop cell reports transport:null + a typed
285
285
  // reason rather than crashing the API.
@@ -1,4 +1,4 @@
1
- // Selfhosted capability negotiation (M3, dossier §10.2 item 4).
1
+ // Selfhosted capability negotiation (M3, item 4).
2
2
  //
3
3
  // `negotiateSelfhostedCapabilities` resolves the selfhosted-specific cells from
4
4
  // (a) the M2 enrollment row (consent / display / status / lastSeenAt), and
@@ -168,6 +168,37 @@ export class OpStreamExecClient {
168
168
  return this.deps.journal?.attachGeneration() ?? "1";
169
169
  }
170
170
 
171
+ /**
172
+ * Cancel an exec by its durable op id. OpCancel is idempotent and also acts
173
+ * as a cancel-before-start tombstone, so calling this while OpStart is still
174
+ * racing is safe: the runner either kills the accepted process tree or
175
+ * refuses the later start without spawning it.
176
+ */
177
+ async cancel(opId: string): Promise<void> {
178
+ const response = await this.deps.controlRpc.request(
179
+ this.deps.rpcSubject,
180
+ {
181
+ requestId: crypto.randomUUID(),
182
+ epoch: this.deps.epoch,
183
+ op: { $case: "opCancel", opCancel: { opId } },
184
+ },
185
+ { timeoutMs: this.deps.controlTimeoutMs },
186
+ );
187
+ if (response.error || !response.result) {
188
+ throw agentErrorToControlError(
189
+ response.error ?? {
190
+ code: ErrorCode.ERROR_CODE_PROTOCOL,
191
+ message: "op-stream cancel returned an empty result",
192
+ retryable: false,
193
+ detail: {},
194
+ },
195
+ );
196
+ }
197
+ if (response.result.$case !== "opStatus") {
198
+ throw protocolError(`op-stream cancel: unexpected result ${response.result.$case}`);
199
+ }
200
+ }
201
+
171
202
  /**
172
203
  * Run one exec over the op stream. `opId` is the durable id (B1) — a re-issue
173
204
  * with the same id attaches instead of re-running. `deadlineMs` is the exec
@@ -429,6 +460,15 @@ class OpConsumer {
429
460
  }
430
461
 
431
462
  private refusedStart(status: OpStatus | undefined): SelfhostedControlError {
463
+ if (status?.exit?.cancelled) {
464
+ return new SelfhostedControlError({
465
+ message: "The command was cancelled before the machine admitted it.",
466
+ code: ErrorCode.ERROR_CODE_TIMEOUT,
467
+ reason: null,
468
+ retryable: false,
469
+ detail: { cancelled: "true" },
470
+ });
471
+ }
432
472
  if (status?.state === OpState.OP_STATE_LOST) {
433
473
  return lostToControlError(status);
434
474
  }
@@ -110,6 +110,8 @@ export interface FakeOpScript {
110
110
  * live emission; later attaches replay from retention unfiltered. Default
111
111
  * false = the op is COMPLETE at start (pure collection). */
112
112
  live?: boolean;
113
+ /** Keep a live op RUNNING with no frames until OpCancel is received. */
114
+ holdUntilCancel?: boolean;
113
115
  /** Seqs whose LIVE emission is dropped (retention keeps them). */
114
116
  dropLiveSeqs?: ReadonlySet<number>;
115
117
  /** Seqs whose LIVE emission is duplicated (delivered twice back-to-back). */
@@ -155,6 +157,7 @@ export class FakeOpRunner implements ControlRpc {
155
157
  /** Op ids that answer LOST (evicted) on query/attach — bounded-retention
156
158
  * collateral. */
157
159
  readonly lostOps = new Set<string>();
160
+ private readonly cancelledBeforeStart = new Set<string>();
158
161
 
159
162
  constructor(opts: {
160
163
  transport: InMemoryOpStreamTransport;
@@ -219,6 +222,8 @@ export class FakeOpRunner implements ControlRpc {
219
222
  );
220
223
  case "opQuery":
221
224
  return this.handleQuery(req.requestId, op.opQuery.opId);
225
+ case "opCancel":
226
+ return this.handleCancel(req.requestId, op.opCancel.opId);
222
227
  default: {
223
228
  if (this.fallback) {
224
229
  return this.fallback.request(subject, req, opts);
@@ -233,6 +238,9 @@ export class FakeOpRunner implements ControlRpc {
233
238
  }
234
239
 
235
240
  private handleStart(opId: string): ControlResponse {
241
+ if (this.cancelledBeforeStart.has(opId)) {
242
+ return startResponse(opId, cancelledStatus(opId), false);
243
+ }
236
244
  const existing = this.runs.get(opId);
237
245
  if (existing) {
238
246
  // IDEMPOTENT BY OP ID: a known op answers its current status and NEVER
@@ -294,7 +302,7 @@ export class FakeOpRunner implements ControlRpc {
294
302
  if (run.script.live && !run.liveEmitted) {
295
303
  // First attach on a live op: nothing retained yet worth replaying — the
296
304
  // child "runs now" and the (faulty) live flow begins.
297
- this.emitLive(opId, run, subject);
305
+ if (!run.script.holdUntilCancel) this.emitLive(opId, run, subject);
298
306
  return statusOnly(requestId, this.statusOf(opId, run));
299
307
  }
300
308
  // A heal (or completed-op collection): replay from RETENTION — every frame
@@ -320,6 +328,26 @@ export class FakeOpRunner implements ControlRpc {
320
328
  return statusOnly(requestId, this.statusOf(opId, run));
321
329
  }
322
330
 
331
+ private handleCancel(requestId: string, opId: string): ControlResponse {
332
+ const run = this.runs.get(opId);
333
+ if (!run) {
334
+ this.cancelledBeforeStart.add(opId);
335
+ return statusOnly(requestId, cancelledStatus(opId));
336
+ }
337
+ if (!run.liveEmitted && run.script.holdUntilCancel) {
338
+ run.exit.exitCode = -1;
339
+ run.exit.cancelled = true;
340
+ run.liveEmitted = true;
341
+ const subject = opFrameSubject(this.workspaceId, this.agentId, opId);
342
+ queueMicrotask(() => {
343
+ for (const frame of run.frames) {
344
+ this.transport.deliver(subject, OpFrame.encode(frame).finish());
345
+ }
346
+ });
347
+ }
348
+ return statusOnly(requestId, this.statusOf(opId, run));
349
+ }
350
+
323
351
  /** The LIVE flow: fault filters applied (drops, duplicates, adjacent
324
352
  * reordering); retention still holds every frame for the heal replay. */
325
353
  private emitLive(opId: string, run: FakeOpRun, subject: string): void {
@@ -441,11 +469,11 @@ function errorResponse(requestId: string, code: ErrorCode, message: string): Con
441
469
  };
442
470
  }
443
471
 
444
- function startResponse(requestId: string, status: OpStatus): ControlResponse {
472
+ function startResponse(requestId: string, status: OpStatus, accepted = true): ControlResponse {
445
473
  return {
446
474
  requestId,
447
475
  error: undefined,
448
- result: { $case: "opStart", opStart: { accepted: true, status } },
476
+ result: { $case: "opStart", opStart: { accepted, status } },
449
477
  };
450
478
  }
451
479
 
@@ -466,3 +494,22 @@ function lostStatus(opId: string): OpStatus {
466
494
  lostReason: OpLostReason.OP_LOST_REASON_EVICTED,
467
495
  };
468
496
  }
497
+
498
+ function cancelledStatus(opId: string): OpStatus {
499
+ return {
500
+ opId,
501
+ state: OpState.OP_STATE_COMPLETE,
502
+ nextSeq: "1",
503
+ exit: {
504
+ exitCode: -1,
505
+ timedOut: false,
506
+ cancelled: true,
507
+ durationMs: "0",
508
+ digests: {},
509
+ totals: {},
510
+ failureCode: "",
511
+ failureDetail: {},
512
+ },
513
+ lostReason: OpLostReason.OP_LOST_REASON_UNSPECIFIED,
514
+ };
515
+ }
@@ -1,7 +1,7 @@
1
1
  // `SelfhostedSession` + `SelfhostedSandboxClient` — the NATS-backed structural
2
2
  // sandbox surface for the `selfhosted` backend (bring-your-own-compute).
3
3
  //
4
- // The insight (dossier §7): every existing seam (Channel-A exec/fs/git, the
4
+ // The insight: every existing seam (Channel-A exec/fs/git, the
5
5
  // viewer's `resolveExposedPort`, computer-use) consumes a provider session
6
6
  // STRUCTURALLY — `session.exec ?? session.execCommand`, `session.readFile`,
7
7
  // `session.resolveExposedPort`, `session.serializeSessionState`. If the
@@ -64,7 +64,7 @@ const encoder = new TextEncoder();
64
64
  * provided-session root-delta guard (`validateProvidedSessionManifestUpdate`).
65
65
  *
66
66
  * On a bring-your-own machine this path DOES NOT EXIST: the machine's real root
67
- * is the agent's `workspace_root` (reported in Hello, e.g. "/home/jorge/repo").
67
+ * is the agent's `workspace_root` (reported in Hello, e.g. "/home/user/repo").
68
68
  * The Rust agent's `resolve_cwd` maps an EMPTY cwd / a RELATIVE path onto its
69
69
  * `workspace_root`, but takes an ABSOLUTE path AS-IS. So a virtual-root-anchored
70
70
  * path the SDK hands us ("/workspace" or "/workspace/sub", e.g. an exec workdir
@@ -187,6 +187,7 @@ const SELFHOSTED_MAX_EXEC_TIMEOUT_MS = 2_147_483_647 - SELFHOSTED_EXEC_REPLY_GRA
187
187
  const SELFHOSTED_EXEC_ENV_ALLOWLIST = [
188
188
  "OPENGENI_TOOLSPACE_URL",
189
189
  "OPENGENI_TOOLSPACE_TOKEN_FILE",
190
+ "OPENGENI_OGTOOL_PACKAGE_SPEC",
190
191
  ] as const;
191
192
 
192
193
  function selfhostedExecEnv(environment: Record<string, string>): Record<string, string> {
@@ -741,6 +742,18 @@ export class SelfhostedSession {
741
742
  return result.output;
742
743
  }
743
744
 
745
+ /**
746
+ * Physical turn-cancellation seam used by the runtime tool fence. Only the
747
+ * op-stream transport has a process-tree cancellation protocol; a legacy
748
+ * runner returns false and the caller remains fail-closed until its exec
749
+ * deadline rather than claiming the machine is quiescent.
750
+ */
751
+ async cancelExecCommand(opId: string): Promise<boolean> {
752
+ if (!this.opStreamClient) return false;
753
+ await this.opStreamClient.cancel(opId);
754
+ return true;
755
+ }
756
+
744
757
  /** SDK shell capability never calls this (gated on `supportsPty()` which is
745
758
  * false), but the surface advertises it. Selfhosted exec has no interactive PTY
746
759
  * session over the structured RPC, so a stdin write is unsupported. */
@@ -994,7 +1007,7 @@ export class SelfhostedSession {
994
1007
  * after asking the agent to ensure a stream channel for the port. M8b wires the
995
1008
  * real relay tier (the byte pump) behind THIS seam.
996
1009
  *
997
- * THE CHANNEL-KEY QUERY (the M8b relay-dial contract, dossier §10.5): the relay
1010
+ * THE CHANNEL-KEY QUERY (the M8b relay-dial contract): the relay
998
1011
  * routes by `{workspaceId, agentId, port}` — the EXACT `ChannelKey::query` the
999
1012
  * agent's relay client (`opengeni-agent-stream`) appends when it registers the
1000
1013
  * producer side: `ws=<workspaceId>&agent=<agentId>&port=<port>`. We append the
@@ -1400,7 +1413,7 @@ function kindToProtocol(kind: StreamKind | undefined): string {
1400
1413
 
1401
1414
  /**
1402
1415
  * The selfhosted NotFound discriminator — THE load-bearing safety property
1403
- * (dossier §10.2/§19): for selfhosted, `agent-offline` (no responder) is NEVER a
1416
+ *: for selfhosted, `agent-offline` (no responder) is NEVER a
1404
1417
  * provider NotFound. A user's real machine is not recreatable; if the lease saw
1405
1418
  * agent-offline as NotFound it would cold-create a RIVAL box (a Modal box) for
1406
1419
  * the user's machine. So this ALWAYS returns FALSE for selfhosted — there is no
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // It is shipped from the runtime package (a testing util, not test-only-private)
9
9
  // because the API/worker integration suites (M4+) reuse it to drive
10
- // `withChannelA`/viewer/swap end-to-end without a real machine (dossier §16).
10
+ // `withChannelA`/viewer/swap end-to-end without a real machine.
11
11
 
12
12
  import {
13
13
  AgentError,
@@ -15,7 +15,7 @@
15
15
  // (Modal raw-TLS host:port, Daytona signed preview, Blaxel preview-token query)
16
16
  // carries the reach-the-port boundary; the OpenGeni stream token is RECORDED
17
17
  // against the viewer holder and is the in-box websockify edge boundary (P3/P5).
18
- // Per the master-spine ruling, exposeStreamPort returns the token alongside the
18
+ // Per the sandbox contract ruling, exposeStreamPort returns the token alongside the
19
19
  // URL so the caller records it; it does NOT append it to `url`.
20
20
 
21
21
  import { DESKTOP_STREAM_PORT } from "@opengeni/contracts";
@@ -1,7 +1,7 @@
1
1
  // @opengeni/runtime/sandbox — scoped stream-token mint/verify (P3.1).
2
2
  //
3
3
  // The agent-loop-free home for the scoped data-plane stream token used by the
4
- // desktop pixel plane (Channel B, master-spine §C.3 / crosscut PART 1.3). It is
4
+ // desktop pixel plane (Channel B, sandbox contract §C.3 / crosscut PART 1.3). It is
5
5
  // a THIN wrapper over the contracts HMAC envelope (signStreamToken /
6
6
  // verifyStreamToken) — NOT a second crypto: it REUSES the exact base64Url +
7
7
  // hmacSha256 construction that backs signDelegatedAccessToken, with the distinct
@@ -110,7 +110,11 @@ export type EnsureTerminalServerResult = {
110
110
  *
111
111
  * Exported (pure, side-effect-free) so the ensureTerminalServer unit test can
112
112
  * assert the exact command sequence without a live box. Mirrors
113
- * buildDisplayStackScript.
113
+ * buildDisplayStackScript. A ttyd process is sandbox-group-global, not
114
+ * OpenGeni-session-specific, so it must never inherit the box-global legacy
115
+ * Toolspace pointer. Session-scoped interactive exec/PTY uses Channel A; the
116
+ * shared ttyd shell fails closed for Toolspace until that stream plane itself is
117
+ * session-isolated.
114
118
  */
115
119
  export function buildTerminalServerScript(options: EnsureTerminalServerOptions = {}): string {
116
120
  const port = options.port ?? TERMINAL_STREAM_PORT;
@@ -120,7 +124,7 @@ export function buildTerminalServerScript(options: EnsureTerminalServerOptions =
120
124
  return (
121
125
  `mkdir -p /tmp/opengeni-terminal && ` +
122
126
  `flock -w 30 /tmp/opengeni-terminal/up.outer.lock ` +
123
- `env TERMINAL_PORT=${port} opengeni-terminal-up`
127
+ `env TERMINAL_PORT=${port} OPENGENI_TOOLSPACE_TOKEN_FILE=/dev/null opengeni-terminal-up`
124
128
  );
125
129
  }
126
130
 
@@ -0,0 +1,147 @@
1
+ import { createHash } from "node:crypto";
2
+ import { posix as posixPath } from "node:path";
3
+
4
+ import type {
5
+ ExecCommandArgs,
6
+ SandboxClient,
7
+ SandboxSessionLike,
8
+ SandboxSessionState,
9
+ } from "@openai/agents/sandbox";
10
+
11
+ const MAX_TOOLSPACE_SESSION_ID_BYTES = 512;
12
+
13
+ export class ToolspaceTokenPathError extends Error {
14
+ constructor(message: string) {
15
+ super(message);
16
+ this.name = "ToolspaceTokenPathError";
17
+ }
18
+ }
19
+
20
+ function shellQuote(value: string): string {
21
+ return `'${value.replace(/'/g, "'\\''")}'`;
22
+ }
23
+
24
+ function assertToolspaceSessionId(sessionId: string): string {
25
+ if (
26
+ !sessionId ||
27
+ sessionId.trim() !== sessionId ||
28
+ new TextEncoder().encode(sessionId).byteLength > MAX_TOOLSPACE_SESSION_ID_BYTES ||
29
+ sessionId.includes("\0")
30
+ ) {
31
+ throw new ToolspaceTokenPathError("Toolspace session id is invalid");
32
+ }
33
+ return sessionId;
34
+ }
35
+
36
+ /**
37
+ * Derive the per-session token file beside the legacy manifest pointer.
38
+ *
39
+ * The manifest pointer stays box-global so an already-warm shared sandbox does
40
+ * not receive an illegal environment delta. Every OpenGeni command overrides
41
+ * that pointer with this deterministic path. The hash is path hygiene and
42
+ * avoids disclosing host/session ids in filenames; filesystem paths are not an
43
+ * authorization boundary. The delegated bearer's session claim remains the
44
+ * authority boundary for Toolspace calls.
45
+ */
46
+ export function toolspaceTokenFileForSession(manifestTokenFile: string, sessionId: string): string {
47
+ if (
48
+ !manifestTokenFile ||
49
+ manifestTokenFile.includes("\0") ||
50
+ !posixPath.isAbsolute(manifestTokenFile)
51
+ ) {
52
+ throw new ToolspaceTokenPathError("Toolspace manifest token file must be absolute");
53
+ }
54
+ const normalizedManifestFile = posixPath.normalize(manifestTokenFile);
55
+ const digest = createHash("sha256").update(assertToolspaceSessionId(sessionId)).digest("hex");
56
+ return posixPath.join(posixPath.dirname(normalizedManifestFile), "toolspace-tokens", digest);
57
+ }
58
+
59
+ export function toolspaceTokenFileFromEnvironment(
60
+ environment: Readonly<Record<string, string>>,
61
+ sessionId: string,
62
+ ): string {
63
+ const manifestTokenFile =
64
+ environment.OPENGENI_TOOLSPACE_TOKEN_FILE ??
65
+ `${environment.HOME ?? "/workspace"}/.opengeni/toolspace-token`;
66
+ return toolspaceTokenFileForSession(manifestTokenFile, sessionId);
67
+ }
68
+
69
+ /** Prefix one sandbox command with its session-specific Toolspace pointer. */
70
+ export function withToolspaceTokenEnvironment(cmd: string, tokenFile: string): string {
71
+ if (!tokenFile || tokenFile.includes("\0") || !posixPath.isAbsolute(tokenFile)) {
72
+ throw new ToolspaceTokenPathError("Toolspace token file must be absolute");
73
+ }
74
+ return [`export OPENGENI_TOOLSPACE_TOKEN_FILE=${shellQuote(tokenFile)}`, cmd].join("\n");
75
+ }
76
+
77
+ /** Preserve provider identity/capabilities while decorating command creation. */
78
+ export function withToolspaceTokenSession<T extends object>(session: T, tokenFile: string): T {
79
+ return new Proxy(session, {
80
+ get(target, property, receiver) {
81
+ if (property === "exec" || property === "execCommand") {
82
+ const command = Reflect.get(target, property, target) as
83
+ | ((args: ExecCommandArgs) => Promise<unknown>)
84
+ | undefined;
85
+ if (!command) return undefined;
86
+ return async (args: ExecCommandArgs) =>
87
+ await command.call(target, {
88
+ ...args,
89
+ cmd: withToolspaceTokenEnvironment(args.cmd, tokenFile),
90
+ });
91
+ }
92
+ const value = Reflect.get(target, property, receiver) as unknown;
93
+ return typeof value === "function" ? value.bind(target) : value;
94
+ },
95
+ });
96
+ }
97
+
98
+ /** Decorate every client-created/resumed session with one session pointer. */
99
+ export function withToolspaceTokenClient(client: SandboxClient, tokenFile: string): SandboxClient {
100
+ const decorated = new WeakMap<object, SandboxSessionLike>();
101
+ const wrap = <T extends SandboxSessionLike>(session: T): T => {
102
+ const existing = decorated.get(session);
103
+ if (existing) return existing as T;
104
+ const wrapped = withToolspaceTokenSession(session, tokenFile);
105
+ decorated.set(session, wrapped);
106
+ return wrapped;
107
+ };
108
+ return {
109
+ backendId: client.backendId,
110
+ ...(client.supportsDefaultOptions !== undefined
111
+ ? { supportsDefaultOptions: client.supportsDefaultOptions }
112
+ : {}),
113
+ ...(client.create
114
+ ? { create: async (...args: any[]) => wrap(await (client.create as any)(...args)) }
115
+ : {}),
116
+ ...(client.resume
117
+ ? { resume: async (...args: any[]) => wrap(await (client.resume as any)(...args)) }
118
+ : {}),
119
+ ...(client.delete
120
+ ? { delete: async (state: SandboxSessionState) => await client.delete!(state) }
121
+ : {}),
122
+ ...(client.serializeSessionState
123
+ ? {
124
+ serializeSessionState: async (state: SandboxSessionState, options) =>
125
+ await client.serializeSessionState!(state, options),
126
+ }
127
+ : {}),
128
+ ...(client.canPersistOwnedSessionState
129
+ ? {
130
+ canPersistOwnedSessionState: async (state: SandboxSessionState) =>
131
+ await client.canPersistOwnedSessionState!(state),
132
+ }
133
+ : {}),
134
+ ...(client.canReusePreservedOwnedSession
135
+ ? {
136
+ canReusePreservedOwnedSession: async (state: SandboxSessionState) =>
137
+ await client.canReusePreservedOwnedSession!(state),
138
+ }
139
+ : {}),
140
+ ...(client.deserializeSessionState
141
+ ? {
142
+ deserializeSessionState: async (state: Record<string, unknown>) =>
143
+ await client.deserializeSessionState!(state),
144
+ }
145
+ : {}),
146
+ } as SandboxClient;
147
+ }