@gpzhang2001/sharpkit-sandbox 0.2.1
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/LICENSE +201 -0
- package/README.md +50 -0
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/lib/index.d.ts +321 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +1145 -0
- package/lib/index.js.map +1 -0
- package/package.json +48 -0
- package/src/brand.ts +24 -0
- package/src/caido.ts +257 -0
- package/src/index.ts +366 -0
- package/src/mounts.ts +197 -0
- package/src/session.ts +444 -0
- package/src/spec.ts +263 -0
package/src/spec.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure spec→argv builders for the docker CLI sandbox (decision D1: argv
|
|
3
|
+
* constructed directly, no dockerode/docker SDK). Every function here is a
|
|
4
|
+
* total pure function over its inputs — no I/O, no clock, no environment —
|
|
5
|
+
* so the docker contract is exhaustively unit-testable (M1 DoD: 100%
|
|
6
|
+
* branch coverage on this module). Behavioral parity references are the
|
|
7
|
+
* strix runtime sources: session_manager.py env/port handling and
|
|
8
|
+
* docker_client.py `_create_container` (caps, extra_hosts, resource/log
|
|
9
|
+
* limits, 127.0.0.1-ephemeral port publishing, network mode).
|
|
10
|
+
* @module @gpzhang2001/sharpkit-sandbox/spec
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** One host→container bind mount (`docker create -v source:target[:ro]`). */
|
|
14
|
+
export interface SandboxBindMount {
|
|
15
|
+
readonly source: string
|
|
16
|
+
readonly target: string
|
|
17
|
+
readonly readOnly: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Opt-in resource limits (strix: STRIX_SANDBOX_{MEM_LIMIT,SHM_SIZE,CPUS,PIDS_LIMIT}). */
|
|
21
|
+
export interface SandboxResourceLimits {
|
|
22
|
+
/** Docker memory string, e.g. `"2g"`. */
|
|
23
|
+
readonly memLimit?: string | undefined
|
|
24
|
+
/** Docker shm size string, e.g. `"512m"`. */
|
|
25
|
+
readonly shmSize?: string | undefined
|
|
26
|
+
/** CPU count as a decimal fraction/multiple, e.g. `1.5`. */
|
|
27
|
+
readonly cpus?: number | undefined
|
|
28
|
+
/** Process-id limit inside the container. */
|
|
29
|
+
readonly pidsLimit?: number | undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Everything `docker create` needs; assembled by the service layer. */
|
|
33
|
+
export interface SandboxCreateSpec {
|
|
34
|
+
/** Image reference, e.g. `ghcr.io/gpzhang2001/sharpkit-sandbox:1.0.0-fork2`. */
|
|
35
|
+
readonly image: string
|
|
36
|
+
/** Keep-alive command after the image (`["tail","-f","/dev/null"]` in strix). */
|
|
37
|
+
readonly command: readonly string[]
|
|
38
|
+
/** Container environment (`-e k=v` per entry; insertion order preserved). */
|
|
39
|
+
readonly env: Readonly<Record<string, string>>
|
|
40
|
+
/** Bind mounts, already sorted shallowest-target-first (strix ordering). */
|
|
41
|
+
readonly bindMounts: readonly SandboxBindMount[]
|
|
42
|
+
/** Container-side Caido port; published to `127.0.0.1` ephemeral unless network is set. */
|
|
43
|
+
readonly caidoPort: number
|
|
44
|
+
/** Attach to an existing docker network and publish no ports (strix sandbox-network mode). */
|
|
45
|
+
readonly network?: string | undefined
|
|
46
|
+
/** Linux capabilities appended after any defaults (`--cap-add`). */
|
|
47
|
+
readonly caps?: readonly string[]
|
|
48
|
+
/** Extra /etc/hosts entries (`--add-host k=v`). */
|
|
49
|
+
readonly extraHosts?: Readonly<Record<string, string>>
|
|
50
|
+
/** Optional resource limits. */
|
|
51
|
+
readonly resourceLimits?: SandboxResourceLimits
|
|
52
|
+
/** Log rotation max-size; disables log opts when one of 0/off/none/unlimited (strix default "50m"). */
|
|
53
|
+
readonly logMaxSize?: string
|
|
54
|
+
/** Log rotation file count (strix default 3). */
|
|
55
|
+
readonly logMaxFile?: number
|
|
56
|
+
/** Container labels (`--label k=v`; strix: sharpkit-run-id / sharpkit-run-type). */
|
|
57
|
+
readonly labels?: Readonly<Record<string, string>>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Values that disable the json-file log rotation opts (strix `_apply_log_limits`). */
|
|
61
|
+
const LOG_DISABLED_SIZES = new Set(['0', 'off', 'none', 'unlimited'])
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build the `docker create` argv for a sandbox spec. Env/hosts/labels are
|
|
65
|
+
* emitted in sorted-key order for deterministic tests; mounts keep their
|
|
66
|
+
* (pre-sorted) order.
|
|
67
|
+
* @param spec - the assembled create spec.
|
|
68
|
+
* @returns the full argv, `["docker","create",…flags,image,…command]`.
|
|
69
|
+
*/
|
|
70
|
+
export function buildCreateArgv(spec: SandboxCreateSpec): string[] {
|
|
71
|
+
const argv: string[] = ['docker', 'create']
|
|
72
|
+
for (const cap of spec.caps ?? []) argv.push('--cap-add', cap)
|
|
73
|
+
for (const key of Object.keys(spec.extraHosts ?? {}).sort()) argv.push('--add-host', `${key}=${spec.extraHosts?.[key]}`)
|
|
74
|
+
for (const key of Object.keys(spec.env).sort()) argv.push('-e', `${key}=${spec.env[key]}`)
|
|
75
|
+
for (const mount of spec.bindMounts) argv.push('-v', mount.readOnly ? `${mount.source}:${mount.target}:ro` : `${mount.source}:${mount.target}`)
|
|
76
|
+
if (spec.network !== undefined && spec.network !== '') {
|
|
77
|
+
// Sandbox-network mode: no published ports; consumers dial the container IP.
|
|
78
|
+
argv.push('--network', spec.network)
|
|
79
|
+
} else {
|
|
80
|
+
argv.push('-p', `127.0.0.1::${spec.caidoPort}`)
|
|
81
|
+
}
|
|
82
|
+
const limits = spec.resourceLimits
|
|
83
|
+
if (limits?.memLimit !== undefined && limits.memLimit !== '') argv.push('--memory', limits.memLimit)
|
|
84
|
+
if (limits?.shmSize !== undefined && limits.shmSize !== '') argv.push('--shm-size', limits.shmSize)
|
|
85
|
+
if (limits?.cpus !== undefined && limits.cpus > 0) argv.push('--cpus', String(limits.cpus))
|
|
86
|
+
if (limits?.pidsLimit !== undefined && Number.isInteger(limits.pidsLimit) && limits.pidsLimit > 0) argv.push('--pids-limit', String(limits.pidsLimit))
|
|
87
|
+
if (logRotationEnabled(spec.logMaxSize)) {
|
|
88
|
+
argv.push('--log-driver', 'json-file', '--log-opt', `max-size=${spec.logMaxSize}`, '--log-opt', `max-file=${spec.logMaxFile ?? 3}`)
|
|
89
|
+
}
|
|
90
|
+
for (const key of Object.keys(spec.labels ?? {}).sort()) argv.push('--label', `${key}=${spec.labels?.[key]}`)
|
|
91
|
+
argv.push(spec.image, ...spec.command)
|
|
92
|
+
return argv
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether json-file rotation opts should be emitted for a max-size value.
|
|
97
|
+
* @param logMaxSize - the configured max-size; absent disables (docker default, unbounded).
|
|
98
|
+
* @returns true when rotation opts must be emitted.
|
|
99
|
+
*/
|
|
100
|
+
export function logRotationEnabled(logMaxSize: string | undefined): boolean {
|
|
101
|
+
return logMaxSize !== undefined && logMaxSize !== '' && !LOG_DISABLED_SIZES.has(logMaxSize.toLowerCase())
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build the container environment (strix session_manager.py:316-329 parity).
|
|
106
|
+
* Proxy vars point at the in-container Caido so all container HTTP traffic is
|
|
107
|
+
* interceptable; NO_PROXY keeps CDP/localhost traffic out of the proxy.
|
|
108
|
+
* @param options - ports/identity inputs; uid/gid only on Linux (ownership remap).
|
|
109
|
+
* @returns the env record in stable insertion order.
|
|
110
|
+
*/
|
|
111
|
+
export function buildContainerEnv(options: {
|
|
112
|
+
readonly caidoPort: number
|
|
113
|
+
readonly platform: NodeJS.Platform
|
|
114
|
+
readonly uid?: number | undefined
|
|
115
|
+
readonly gid?: number | undefined
|
|
116
|
+
}): Record<string, string> {
|
|
117
|
+
const proxy = `http://127.0.0.1:${options.caidoPort}`
|
|
118
|
+
const env: Record<string, string> = {
|
|
119
|
+
PYTHONUNBUFFERED: '1',
|
|
120
|
+
HOST_GATEWAY: 'host.docker.internal',
|
|
121
|
+
http_proxy: proxy,
|
|
122
|
+
https_proxy: proxy,
|
|
123
|
+
ALL_PROXY: proxy,
|
|
124
|
+
NO_PROXY: 'localhost,127.0.0.1',
|
|
125
|
+
}
|
|
126
|
+
if (options.platform === 'linux' && options.uid !== undefined && options.uid > 0) {
|
|
127
|
+
env.SHARPKIT_HOST_UID = String(options.uid)
|
|
128
|
+
env.SHARPKIT_HOST_GID = String(options.gid ?? options.uid)
|
|
129
|
+
}
|
|
130
|
+
return env
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Options for a non-interactive `docker exec` argv. */
|
|
134
|
+
export interface SandboxExecArgvOptions {
|
|
135
|
+
readonly containerId: string
|
|
136
|
+
readonly command: string
|
|
137
|
+
/** Working directory inside the container (`-w`), typically under /workspace. */
|
|
138
|
+
readonly cwd?: string | undefined
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build the non-interactive exec argv: a fresh login shell per call
|
|
143
|
+
* (`bash -lc`), matching the S1 spike and the image's login-shell PATH fixups.
|
|
144
|
+
*/
|
|
145
|
+
export function buildExecArgv(options: SandboxExecArgvOptions): string[] {
|
|
146
|
+
const argv = ['docker', 'exec', '-i']
|
|
147
|
+
if (options.cwd !== undefined) argv.push('-w', options.cwd)
|
|
148
|
+
argv.push(options.containerId, 'bash', '-lc', options.command)
|
|
149
|
+
return argv
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Options for a PTY-backed interactive `docker exec` argv. */
|
|
153
|
+
export interface SandboxTtyArgvOptions {
|
|
154
|
+
readonly containerId: string
|
|
155
|
+
readonly command: string
|
|
156
|
+
readonly cwd?: string | undefined
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build the PTY exec argv (`docker exec -it`); the host-side PTY is provided
|
|
161
|
+
* by `ctx.subprocess.spawnTerminal` and Ctrl-C is delivered as `\x03`
|
|
162
|
+
* (spike finding D1.1).
|
|
163
|
+
*/
|
|
164
|
+
export function buildExecTtyArgv(options: SandboxTtyArgvOptions): string[] {
|
|
165
|
+
const argv = ['docker', 'exec', '-it']
|
|
166
|
+
if (options.cwd !== undefined) argv.push('-w', options.cwd)
|
|
167
|
+
argv.push(options.containerId, 'bash', '-lc', options.command)
|
|
168
|
+
return argv
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A resolved host-side endpoint of a published container port. */
|
|
172
|
+
export interface SandboxHostEndpoint {
|
|
173
|
+
readonly host: string
|
|
174
|
+
readonly port: number
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Build the argv resolving a published port's host endpoint (`docker port`).
|
|
179
|
+
*/
|
|
180
|
+
export function buildPortArgv(containerId: string, port: number): string[] {
|
|
181
|
+
return ['docker', 'port', containerId, String(port)]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Parse `docker port` output into endpoints, preferring IPv4 (strix publishes
|
|
186
|
+
* to 127.0.0.1). IPv6 literals arrive bracketed and are returned bracket-free
|
|
187
|
+
* with `bracketedIPv6` only when the raw host contains `:`.
|
|
188
|
+
* @param output - the raw `docker port` stdout (zero or more lines).
|
|
189
|
+
* @returns endpoints, IPv4 entries first; empty when nothing is published.
|
|
190
|
+
*/
|
|
191
|
+
export function parsePortOutput(output: string): SandboxHostEndpoint[] {
|
|
192
|
+
const endpoints: SandboxHostEndpoint[] = []
|
|
193
|
+
for (const rawLine of output.split('\n')) {
|
|
194
|
+
const line = rawLine.trim()
|
|
195
|
+
if (line === '') continue
|
|
196
|
+
// Form: "127.0.0.1:49153" or "[::1]:49153".
|
|
197
|
+
const lastColon = line.lastIndexOf(':')
|
|
198
|
+
if (lastColon === -1) continue
|
|
199
|
+
const port = Number.parseInt(line.slice(lastColon + 1), 10)
|
|
200
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) continue
|
|
201
|
+
let host = line.slice(0, lastColon)
|
|
202
|
+
if (host.startsWith('[') && host.endsWith(']')) host = host.slice(1, -1)
|
|
203
|
+
if (host === '') continue
|
|
204
|
+
endpoints.push({ host, port })
|
|
205
|
+
}
|
|
206
|
+
const preferred = endpoints.filter(endpoint => !endpoint.host.includes(':'))
|
|
207
|
+
const ipv6 = endpoints.filter(endpoint => endpoint.host.includes(':'))
|
|
208
|
+
return [...preferred, ...ipv6]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Build the argv resolving the container's IP on a sandbox network (strix
|
|
213
|
+
* `StrixDockerSandboxSession._resolve_exposed_port`, network mode).
|
|
214
|
+
*/
|
|
215
|
+
export function buildNetworkIpArgv(containerId: string, network: string): string[] {
|
|
216
|
+
return ['docker', 'inspect', '--format', `{{.NetworkSettings.Networks.${network}.IPAddress}}`, containerId]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Container-side path reference for `docker cp` (`<id>:<path>`). */
|
|
220
|
+
export function containerRef(containerId: string, containerPath: string): string {
|
|
221
|
+
return `${containerId}:${containerPath}`
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Build the `docker cp` argv copying a host file into the container. */
|
|
225
|
+
export function buildPutFileArgv(hostPath: string, containerId: string, containerPath: string): string[] {
|
|
226
|
+
return ['docker', 'cp', hostPath, containerRef(containerId, containerPath)]
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Build the `docker cp` argv copying a container file out to a host path. */
|
|
230
|
+
export function buildGetFileArgv(containerId: string, containerPath: string, hostPath: string): string[] {
|
|
231
|
+
return ['docker', 'cp', containerRef(containerId, containerPath), hostPath]
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Build the graceful stop argv (`docker stop -t <seconds>`). */
|
|
235
|
+
export function buildStopArgv(containerId: string, graceMs: number): string[] {
|
|
236
|
+
const seconds = Math.max(0, Math.round(graceMs / 1000))
|
|
237
|
+
return ['docker', 'stop', '-t', String(seconds), containerId]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Build the plain remove argv; the caller escalates to force-remove on failure. */
|
|
241
|
+
export function buildRmArgv(containerId: string): string[] {
|
|
242
|
+
return ['docker', 'rm', containerId]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Build the force-remove argv (fallback path and failure cleanup). */
|
|
246
|
+
export function buildRmForceArgv(containerId: string): string[] {
|
|
247
|
+
return ['docker', 'rm', '-f', containerId]
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Build the argv checking whether an image is present locally. */
|
|
251
|
+
export function buildImageInspectArgv(image: string): string[] {
|
|
252
|
+
return ['docker', 'image', 'inspect', image]
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Build the pull argv (strix pulls only when the image is missing). */
|
|
256
|
+
export function buildPullArgv(image: string): string[] {
|
|
257
|
+
return ['docker', 'pull', image]
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Build the start argv. */
|
|
261
|
+
export function buildStartArgv(containerId: string): string[] {
|
|
262
|
+
return ['docker', 'start', containerId]
|
|
263
|
+
}
|