@alibaba-group/opensandbox 0.1.4 → 0.1.6

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.
@@ -33,14 +33,31 @@ function joinUrl(baseUrl: string, pathname: string): string {
33
33
  return `${base}${path}`;
34
34
  }
35
35
 
36
+ /** Request body for POST /command (from generated spec; includes uid, gid, envs). */
36
37
  type ApiRunCommandRequest =
37
38
  ExecdPaths["/command"]["post"]["requestBody"]["content"]["application/json"];
38
39
  type ApiCommandStatusOk =
39
40
  ExecdPaths["/command/status/{id}"]["get"]["responses"][200]["content"]["application/json"];
40
41
  type ApiCommandLogsOk =
41
42
  ExecdPaths["/command/{id}/logs"]["get"]["responses"][200]["content"]["text/plain"];
43
+ type ApiCreateSessionRequest =
44
+ NonNullable<ExecdPaths["/session"]["post"]["requestBody"]>["content"]["application/json"];
45
+ type ApiCreateSessionOk =
46
+ ExecdPaths["/session"]["post"]["responses"][200]["content"]["application/json"];
47
+ type ApiRunInSessionRequest =
48
+ ExecdPaths["/session/{sessionId}/run"]["post"]["requestBody"]["content"]["application/json"];
49
+
50
+ interface StreamingExecutionSpec<TBody> {
51
+ pathname: string;
52
+ body: TBody;
53
+ fallbackErrorMessage: string;
54
+ }
42
55
 
43
56
  function toRunCommandRequest(command: string, opts?: RunCommandOpts): ApiRunCommandRequest {
57
+ if (opts?.gid != null && opts.uid == null) {
58
+ throw new Error("uid is required when gid is provided");
59
+ }
60
+
44
61
  const body: ApiRunCommandRequest = {
45
62
  command,
46
63
  cwd: opts?.workingDirectory,
@@ -49,9 +66,51 @@ function toRunCommandRequest(command: string, opts?: RunCommandOpts): ApiRunComm
49
66
  if (opts?.timeoutSeconds != null) {
50
67
  body.timeout = Math.round(opts.timeoutSeconds * 1000);
51
68
  }
69
+ if (opts?.uid != null) {
70
+ body.uid = opts.uid;
71
+ }
72
+ if (opts?.gid != null) {
73
+ body.gid = opts.gid;
74
+ }
75
+ if (opts?.envs != null) {
76
+ body.envs = opts.envs;
77
+ }
78
+ return body;
79
+ }
80
+
81
+ function toRunInSessionRequest(
82
+ command: string,
83
+ opts?: { workingDirectory?: string; timeoutSeconds?: number },
84
+ ): ApiRunInSessionRequest {
85
+ const body: ApiRunInSessionRequest = {
86
+ command,
87
+ };
88
+ if (opts?.workingDirectory != null) {
89
+ body.cwd = opts.workingDirectory;
90
+ }
91
+ if (opts?.timeoutSeconds != null) {
92
+ body.timeout = Math.round(opts.timeoutSeconds * 1000);
93
+ }
52
94
  return body;
53
95
  }
54
96
 
97
+ function inferForegroundExitCode(execution: CommandExecution): number | null {
98
+ const errorValue = execution.error?.value?.trim();
99
+ const parsedExitCode =
100
+ errorValue && /^-?\d+$/.test(errorValue) ? Number(errorValue) : Number.NaN;
101
+ return execution.error != null
102
+ ? (Number.isFinite(parsedExitCode) ? parsedExitCode : null)
103
+ : execution.complete
104
+ ? 0
105
+ : null;
106
+ }
107
+
108
+ function assertNonBlank(value: string, field: string): void {
109
+ if (!value.trim()) {
110
+ throw new Error(`${field} cannot be empty`);
111
+ }
112
+ }
113
+
55
114
  function parseOptionalDate(value: unknown, field: string): Date | undefined {
56
115
  if (value == null) return undefined;
57
116
  if (value instanceof Date) return value;
@@ -84,6 +143,79 @@ export class CommandsAdapter implements ExecdCommands {
84
143
  this.fetch = opts.fetch ?? fetch;
85
144
  }
86
145
 
146
+ private buildRunStreamSpec(
147
+ command: string,
148
+ opts?: RunCommandOpts,
149
+ ): StreamingExecutionSpec<ApiRunCommandRequest> {
150
+ assertNonBlank(command, "command");
151
+ return {
152
+ pathname: "/command",
153
+ body: toRunCommandRequest(command, opts),
154
+ fallbackErrorMessage: "Run command failed",
155
+ };
156
+ }
157
+
158
+ private buildRunInSessionStreamSpec(
159
+ sessionId: string,
160
+ command: string,
161
+ opts?: { workingDirectory?: string; timeoutSeconds?: number },
162
+ ): StreamingExecutionSpec<ApiRunInSessionRequest> {
163
+ assertNonBlank(sessionId, "sessionId");
164
+ assertNonBlank(command, "command");
165
+ return {
166
+ pathname: `/session/${encodeURIComponent(sessionId)}/run`,
167
+ body: toRunInSessionRequest(command, opts),
168
+ fallbackErrorMessage: "Run in session failed",
169
+ };
170
+ }
171
+
172
+ private async *streamExecution<TBody>(
173
+ spec: StreamingExecutionSpec<TBody>,
174
+ signal?: AbortSignal,
175
+ ): AsyncIterable<ServerStreamEvent> {
176
+ const url = joinUrl(this.opts.baseUrl, spec.pathname);
177
+ const res = await this.fetch(url, {
178
+ method: "POST",
179
+ headers: {
180
+ accept: "text/event-stream",
181
+ "content-type": "application/json",
182
+ ...(this.opts.headers ?? {}),
183
+ },
184
+ body: JSON.stringify(spec.body),
185
+ signal,
186
+ });
187
+
188
+ for await (const ev of parseJsonEventStream<ServerStreamEvent>(res, {
189
+ fallbackErrorMessage: spec.fallbackErrorMessage,
190
+ })) {
191
+ yield ev;
192
+ }
193
+ }
194
+
195
+ private async consumeExecutionStream(
196
+ stream: AsyncIterable<ServerStreamEvent>,
197
+ handlers?: ExecutionHandlers,
198
+ inferExitCode = false,
199
+ ): Promise<CommandExecution> {
200
+ const execution: CommandExecution = {
201
+ logs: { stdout: [], stderr: [] },
202
+ result: [],
203
+ };
204
+ const dispatcher = new ExecutionEventDispatcher(execution, handlers);
205
+ for await (const ev of stream) {
206
+ if (ev.type === "init" && (ev.text ?? "") === "" && execution.id) {
207
+ (ev as { text?: string }).text = execution.id;
208
+ }
209
+ await dispatcher.dispatch(ev as any);
210
+ }
211
+
212
+ if (inferExitCode) {
213
+ execution.exitCode = inferForegroundExitCode(execution);
214
+ }
215
+
216
+ return execution;
217
+ }
218
+
87
219
  async interrupt(sessionId: string): Promise<void> {
88
220
  const { error, response } = await this.client.DELETE("/command", {
89
221
  params: { query: { id: sessionId } },
@@ -134,21 +266,10 @@ export class CommandsAdapter implements ExecdCommands {
134
266
  opts?: RunCommandOpts,
135
267
  signal?: AbortSignal,
136
268
  ): AsyncIterable<ServerStreamEvent> {
137
- const url = joinUrl(this.opts.baseUrl, "/command");
138
- const body = JSON.stringify(toRunCommandRequest(command, opts));
139
-
140
- const res = await this.fetch(url, {
141
- method: "POST",
142
- headers: {
143
- "accept": "text/event-stream",
144
- "content-type": "application/json",
145
- ...(this.opts.headers ?? {}),
146
- },
147
- body,
269
+ for await (const ev of this.streamExecution(
270
+ this.buildRunStreamSpec(command, opts),
148
271
  signal,
149
- });
150
-
151
- for await (const ev of parseJsonEventStream<ServerStreamEvent>(res, { fallbackErrorMessage: "Run command failed" })) {
272
+ )) {
152
273
  yield ev;
153
274
  }
154
275
  }
@@ -159,19 +280,60 @@ export class CommandsAdapter implements ExecdCommands {
159
280
  handlers?: ExecutionHandlers,
160
281
  signal?: AbortSignal,
161
282
  ): Promise<CommandExecution> {
162
- const execution: CommandExecution = {
163
- logs: { stdout: [], stderr: [] },
164
- result: [],
165
- };
166
- const dispatcher = new ExecutionEventDispatcher(execution, handlers);
167
- for await (const ev of this.runStream(command, opts, signal)) {
168
- // Keep legacy behavior: if server sends "init" with empty id, preserve previous id.
169
- if (ev.type === "init" && (ev.text ?? "") === "" && execution.id) {
170
- (ev as any).text = execution.id;
171
- }
172
- await dispatcher.dispatch(ev as any);
283
+ return this.consumeExecutionStream(
284
+ this.runStream(command, opts, signal),
285
+ handlers,
286
+ !opts?.background,
287
+ );
288
+ }
289
+
290
+ async createSession(options?: { workingDirectory?: string }): Promise<string> {
291
+ const body: ApiCreateSessionRequest =
292
+ options?.workingDirectory != null ? { cwd: options.workingDirectory } : {};
293
+ const { data, error, response } = await this.client.POST("/session", {
294
+ body,
295
+ });
296
+ throwOnOpenApiFetchError({ error, response }, "Create session failed");
297
+ const ok = data as ApiCreateSessionOk | undefined;
298
+ if (!ok || typeof (ok as { session_id?: string }).session_id !== "string") {
299
+ throw new Error("Create session failed: unexpected response shape");
173
300
  }
301
+ return (ok as { session_id: string }).session_id;
302
+ }
174
303
 
175
- return execution;
304
+ async *runInSessionStream(
305
+ sessionId: string,
306
+ command: string,
307
+ opts?: { workingDirectory?: string; timeoutSeconds?: number },
308
+ signal?: AbortSignal,
309
+ ): AsyncIterable<ServerStreamEvent> {
310
+ for await (const ev of this.streamExecution(
311
+ this.buildRunInSessionStreamSpec(sessionId, command, opts),
312
+ signal,
313
+ )) {
314
+ yield ev;
315
+ }
316
+ }
317
+
318
+ async runInSession(
319
+ sessionId: string,
320
+ command: string,
321
+ options?: { workingDirectory?: string; timeoutSeconds?: number },
322
+ handlers?: ExecutionHandlers,
323
+ signal?: AbortSignal,
324
+ ): Promise<CommandExecution> {
325
+ return this.consumeExecutionStream(
326
+ this.runInSessionStream(sessionId, command, options, signal),
327
+ handlers,
328
+ true,
329
+ );
176
330
  }
177
- }
331
+
332
+ async deleteSession(sessionId: string): Promise<void> {
333
+ const { error, response } = await this.client.DELETE(
334
+ "/session/{sessionId}",
335
+ { params: { path: { sessionId } } },
336
+ );
337
+ throwOnOpenApiFetchError({ error, response }, "Delete session failed");
338
+ }
339
+ }
@@ -0,0 +1,46 @@
1
+ // Copyright 2026 Alibaba Group Holding Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import type { EgressClient } from "../openapi/egressClient.js";
16
+ import { throwOnOpenApiFetchError } from "./openapiError.js";
17
+ import type { paths as EgressPaths } from "../api/egress.js";
18
+ import type { NetworkPolicy, NetworkRule } from "../models/sandboxes.js";
19
+ import type { Egress } from "../services/egress.js";
20
+
21
+ type ApiGetPolicyOk =
22
+ EgressPaths["/policy"]["get"]["responses"][200]["content"]["application/json"];
23
+ type ApiPatchRulesRequest =
24
+ EgressPaths["/policy"]["patch"]["requestBody"]["content"]["application/json"];
25
+
26
+ export class EgressAdapter implements Egress {
27
+ constructor(private readonly client: EgressClient) {}
28
+
29
+ async getPolicy(): Promise<NetworkPolicy> {
30
+ const { data, error, response } = await this.client.GET("/policy");
31
+ throwOnOpenApiFetchError({ error, response }, "Get sandbox egress policy failed");
32
+ const raw = data as ApiGetPolicyOk | undefined;
33
+ if (!raw || typeof raw !== "object" || !raw.policy || typeof raw.policy !== "object") {
34
+ throw new Error("Get sandbox egress policy failed: unexpected response shape");
35
+ }
36
+ return raw.policy as NetworkPolicy;
37
+ }
38
+
39
+ async patchRules(rules: NetworkRule[]): Promise<void> {
40
+ const body: ApiPatchRulesRequest = rules as unknown as ApiPatchRulesRequest;
41
+ const { error, response } = await this.client.PATCH("/policy", {
42
+ body,
43
+ });
44
+ throwOnOpenApiFetchError({ error, response }, "Patch sandbox egress rules failed");
45
+ }
46
+ }
@@ -69,11 +69,16 @@ export class SandboxesAdapter implements Sandboxes {
69
69
  return d;
70
70
  }
71
71
 
72
+ private parseOptionalIsoDate(field: string, v: unknown): Date | null {
73
+ if (v == null) return null;
74
+ return this.parseIsoDate(field, v);
75
+ }
76
+
72
77
  private mapSandboxInfo(raw: ApiGetSandboxOk): SandboxInfo {
73
78
  return {
74
79
  ...(raw ?? {}),
75
80
  createdAt: this.parseIsoDate("createdAt", raw?.createdAt),
76
- expiresAt: this.parseIsoDate("expiresAt", raw?.expiresAt),
81
+ expiresAt: this.parseOptionalIsoDate("expiresAt", raw?.expiresAt),
77
82
  } as SandboxInfo;
78
83
  }
79
84
 
@@ -91,7 +96,7 @@ export class SandboxesAdapter implements Sandboxes {
91
96
  return {
92
97
  ...(raw ?? {}),
93
98
  createdAt: this.parseIsoDate("createdAt", raw?.createdAt),
94
- expiresAt: this.parseIsoDate("expiresAt", raw?.expiresAt),
99
+ expiresAt: this.parseOptionalIsoDate("expiresAt", raw?.expiresAt),
95
100
  } as CreateSandboxResponse;
96
101
  }
97
102
 
@@ -188,4 +193,4 @@ export class SandboxesAdapter implements Sandboxes {
188
193
  }
189
194
  return ok as unknown as Endpoint;
190
195
  }
191
- }
196
+ }
@@ -0,0 +1,184 @@
1
+ // Copyright 2026 Alibaba Group Holding Ltd..
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ /**
16
+ * This file was auto-generated by openapi-typescript.
17
+ * Do not make direct changes to the file.
18
+ */
19
+
20
+ export interface paths {
21
+ "/policy": {
22
+ parameters: {
23
+ query?: never;
24
+ header?: never;
25
+ path?: never;
26
+ cookie?: never;
27
+ };
28
+ /**
29
+ * Get current egress policy
30
+ * @description Returns the currently enforced egress policy and the sidecar's derived
31
+ * runtime mode metadata.
32
+ */
33
+ get: {
34
+ parameters: {
35
+ query?: never;
36
+ header?: never;
37
+ path?: never;
38
+ cookie?: never;
39
+ };
40
+ requestBody?: never;
41
+ responses: {
42
+ /** @description Current policy returned successfully. */
43
+ 200: {
44
+ headers: {
45
+ [name: string]: unknown;
46
+ };
47
+ content: {
48
+ "application/json": components["schemas"]["PolicyStatusResponse"];
49
+ };
50
+ };
51
+ 401: components["responses"]["Unauthorized"];
52
+ 500: components["responses"]["InternalServerError"];
53
+ };
54
+ };
55
+ put?: never;
56
+ post?: never;
57
+ delete?: never;
58
+ options?: never;
59
+ head?: never;
60
+ /**
61
+ * Patch egress rules
62
+ * @description Merge incoming egress rules with the currently enforced policy.
63
+ *
64
+ * This endpoint uses merge semantics:
65
+ * - Existing rules remain unless overridden by incoming rules.
66
+ * - Incoming rules are applied with higher priority than existing rules.
67
+ * - If multiple incoming rules refer to the same `target`, the first one wins.
68
+ */
69
+ patch: {
70
+ parameters: {
71
+ query?: never;
72
+ header?: never;
73
+ path?: never;
74
+ cookie?: never;
75
+ };
76
+ requestBody: {
77
+ content: {
78
+ "application/json": components["schemas"]["NetworkRule"][];
79
+ };
80
+ };
81
+ responses: {
82
+ /** @description Patch applied successfully. */
83
+ 200: {
84
+ headers: {
85
+ [name: string]: unknown;
86
+ };
87
+ content: {
88
+ "application/json": components["schemas"]["PolicyStatusResponse"];
89
+ };
90
+ };
91
+ 400: components["responses"]["BadRequest"];
92
+ 401: components["responses"]["Unauthorized"];
93
+ 500: components["responses"]["InternalServerError"];
94
+ };
95
+ };
96
+ trace?: never;
97
+ };
98
+ }
99
+ export type webhooks = Record<string, never>;
100
+ export interface components {
101
+ schemas: {
102
+ PolicyStatusResponse: {
103
+ /**
104
+ * @description Operation status reported by the sidecar.
105
+ * @example ok
106
+ */
107
+ status?: string;
108
+ /**
109
+ * @description Derived runtime mode for the current policy.
110
+ * @example deny_all
111
+ */
112
+ mode?: string;
113
+ /**
114
+ * @description Egress sidecar enforcement backend mode.
115
+ * @example dns
116
+ */
117
+ enforcementMode?: string;
118
+ /** @description Optional human-readable reason when the sidecar returns extra context. */
119
+ reason?: string;
120
+ policy?: components["schemas"]["NetworkPolicy"];
121
+ };
122
+ /**
123
+ * @description Egress network policy matching the sidecar `/policy` request body.
124
+ * If `defaultAction` is omitted, the sidecar defaults to "deny"; passing an empty
125
+ * object or null results in allow-all behavior at startup.
126
+ */
127
+ NetworkPolicy: {
128
+ /**
129
+ * @description Default action when no egress rule matches. Defaults to "deny".
130
+ * @enum {string}
131
+ */
132
+ defaultAction?: "allow" | "deny";
133
+ /** @description List of egress rules evaluated in order. */
134
+ egress?: components["schemas"]["NetworkRule"][];
135
+ };
136
+ NetworkRule: {
137
+ /**
138
+ * @description Whether to allow or deny matching targets.
139
+ * @enum {string}
140
+ */
141
+ action: "allow" | "deny";
142
+ /**
143
+ * @description FQDN or wildcard domain (e.g., "example.com", "*.example.com").
144
+ * IP/CIDR not yet supported in the egress MVP.
145
+ */
146
+ target: string;
147
+ };
148
+ };
149
+ responses: {
150
+ /** @description The request was invalid or malformed. */
151
+ BadRequest: {
152
+ headers: {
153
+ [name: string]: unknown;
154
+ };
155
+ content: {
156
+ "text/plain": string;
157
+ };
158
+ };
159
+ /** @description Authentication failed for the egress sidecar. */
160
+ Unauthorized: {
161
+ headers: {
162
+ [name: string]: unknown;
163
+ };
164
+ content: {
165
+ "text/plain": string;
166
+ };
167
+ };
168
+ /** @description The sidecar failed to apply or fetch policy state. */
169
+ InternalServerError: {
170
+ headers: {
171
+ [name: string]: unknown;
172
+ };
173
+ content: {
174
+ "text/plain": string;
175
+ };
176
+ };
177
+ };
178
+ parameters: never;
179
+ requestBodies: never;
180
+ headers: never;
181
+ pathItems: never;
182
+ }
183
+ export type $defs = Record<string, never>;
184
+ export type operations = Record<string, never>;