dsh-edge 0.1.0 → 0.1.3
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 +1 -1
- package/README.i18n.yaml +2 -2
- package/README.md +6 -4
- package/README.zh.md +6 -4
- package/THIRD_PARTY_NOTICES.md +42 -14
- package/dist/index.html +1 -1
- package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +7 -7
- package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +61 -61
- package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +14 -14
- package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +287 -287
- package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +10 -10
- package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +11 -11
- package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +15 -15
- package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +8 -8
- package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +27 -27
- package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +8 -8
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +31 -31
- package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +23 -23
- package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +19 -19
- package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +29 -29
- package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +7 -7
- package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +64 -64
- package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +183 -183
- package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +44 -44
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +24 -24
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +79 -79
- package/dist/plugins/{@deepseek-ai/dsh-client-ui-edge → dsh-edge-client-ui}/client.js +12 -12
- package/package.json +26 -23
- package/scripts/assemble-web.mjs +1 -1
- package/scripts/bundle.mjs +41 -8
- package/scripts/cli.d.mts +1 -0
- package/scripts/cli.mjs +37 -6
- package/scripts/install.d.mts +6 -6
- package/scripts/install.mjs +36 -19
- package/scripts/legal-files.mjs +65 -7
- package/scripts/smoke-installed.mjs +47 -0
- package/scripts/verify-packed.d.mts +24 -0
- package/scripts/verify-packed.mjs +85 -23
- package/scripts/wrangler-config.d.mts +19 -2
- package/scripts/wrangler-config.mjs +68 -27
- package/worker/direct/index.js +2347 -0
- package/worker/isolated/index.js +44491 -0
- package/src/agent.ts +0 -119
- package/src/auth.ts +0 -352
- package/src/deepseek.ts +0 -231
- package/src/deployment.ts +0 -147
- package/src/direct-shell-core-empty.ts +0 -10
- package/src/direct-shell-protocol.ts +0 -5
- package/src/direct-shell.ts +0 -341
- package/src/do-session-persistence.ts +0 -1064
- package/src/edge-api.ts +0 -796
- package/src/edge-credentials.ts +0 -56
- package/src/edge-remotes.ts +0 -41
- package/src/edge-workspace-store.ts +0 -339
- package/src/http.ts +0 -212
- package/src/index.ts +0 -196
- package/src/instance.ts +0 -1007
- package/src/isolated-direct-shell-unavailable.ts +0 -11
- package/src/protocol.ts +0 -39
- package/src/release.ts +0 -9
- package/src/session-store.ts +0 -1110
- package/src/sse.ts +0 -86
- package/src/web-search.ts +0 -38
- package/src/workspace.ts +0 -270
package/src/deployment.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/** Deployment configuration shared by Edge health and turn admission. */
|
|
2
|
-
|
|
3
|
-
import type { EdgeReasoningEffort } from './deepseek.ts'
|
|
4
|
-
import {
|
|
5
|
-
resolveEdgeBaseURL,
|
|
6
|
-
resolveEdgeMaxOutputTokens,
|
|
7
|
-
resolveEdgeModel,
|
|
8
|
-
resolveEdgeReasoningEffort,
|
|
9
|
-
resolveEdgeStreamIdleTimeoutMs,
|
|
10
|
-
} from './deepseek.ts'
|
|
11
|
-
import { resolveDeepSeekApiKey } from './http.ts'
|
|
12
|
-
import { resolveEdgeSearchBaseURL } from './web-search.ts'
|
|
13
|
-
import {
|
|
14
|
-
resolveEdgeCommandTimeoutPolicy,
|
|
15
|
-
type EdgeCommandTimeoutPolicy,
|
|
16
|
-
} from './workspace.ts'
|
|
17
|
-
import { DSH_EDGE_UPSTREAM_VERSION, DSH_EDGE_VERSION } from './release.ts'
|
|
18
|
-
|
|
19
|
-
declare const __DSH_EDGE_DEPLOYMENT_ID__: string | undefined
|
|
20
|
-
|
|
21
|
-
const LOCAL_DEPLOYMENT_ID = 'local-development'
|
|
22
|
-
|
|
23
|
-
/** Worker variables that control one Edge model and shell turn. */
|
|
24
|
-
export interface EdgeDeploymentConfigSource {
|
|
25
|
-
LOADER?: unknown
|
|
26
|
-
DEEPSEEK_API_KEY?: string
|
|
27
|
-
DEEPSEEK_BASE_URL?: string
|
|
28
|
-
DEEPSEEK_MAX_OUTPUT_TOKENS?: string
|
|
29
|
-
DEEPSEEK_MODEL?: string
|
|
30
|
-
DEEPSEEK_REASONING_EFFORT?: string
|
|
31
|
-
DEEPSEEK_SEARCH_BASE_URL?: string
|
|
32
|
-
DEEPSEEK_STREAM_IDLE_TIMEOUT_MS?: string
|
|
33
|
-
DSH_EDGE_DEFAULT_COMMAND_TIMEOUT_MS?: string
|
|
34
|
-
DSH_EDGE_MAX_COMMAND_TIMEOUT_MS?: string
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Validated deployment choices consumed by one Edge turn. */
|
|
38
|
-
export interface EdgeDeploymentConfig {
|
|
39
|
-
baseURL: string
|
|
40
|
-
maxTokens: number
|
|
41
|
-
model: string
|
|
42
|
-
reasoningEffort: EdgeReasoningEffort
|
|
43
|
-
searchBaseURL: string
|
|
44
|
-
streamIdleTimeoutMs: number
|
|
45
|
-
commandTimeoutPolicy: EdgeCommandTimeoutPolicy
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Secret-free deployment facts safe to project to the authenticated owner. */
|
|
49
|
-
export interface EdgeDeploymentProfile {
|
|
50
|
-
shell: 'just-bash-direct' | 'just-bash-isolated'
|
|
51
|
-
storage: 'durable-object-sqlite-vfs'
|
|
52
|
-
deploymentId: string
|
|
53
|
-
apiKeyConfigured: boolean
|
|
54
|
-
baseURL: string
|
|
55
|
-
maxTokens: number
|
|
56
|
-
model: string
|
|
57
|
-
reasoningEffort: EdgeReasoningEffort
|
|
58
|
-
searchBaseURL: string
|
|
59
|
-
streamIdleTimeoutMs: number
|
|
60
|
-
commandTimeoutPolicy: EdgeCommandTimeoutPolicy
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Identify the exact Worker artifact that answered a readiness request. */
|
|
64
|
-
export function resolveEdgeDeploymentId(): string {
|
|
65
|
-
return typeof __DSH_EDGE_DEPLOYMENT_ID__ === 'string'
|
|
66
|
-
? __DSH_EDGE_DEPLOYMENT_ID__
|
|
67
|
-
: LOCAL_DEPLOYMENT_ID
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Resolve every deployment choice before health succeeds or a turn is claimed. */
|
|
71
|
-
export function resolveEdgeDeploymentConfig(
|
|
72
|
-
source: EdgeDeploymentConfigSource,
|
|
73
|
-
): EdgeDeploymentConfig {
|
|
74
|
-
resolveDeepSeekApiKey(source.DEEPSEEK_API_KEY)
|
|
75
|
-
return {
|
|
76
|
-
baseURL: resolveEdgeBaseURL(source.DEEPSEEK_BASE_URL),
|
|
77
|
-
maxTokens: resolveEdgeMaxOutputTokens(source.DEEPSEEK_MAX_OUTPUT_TOKENS),
|
|
78
|
-
model: resolveEdgeModel(source.DEEPSEEK_MODEL),
|
|
79
|
-
reasoningEffort: resolveEdgeReasoningEffort(source.DEEPSEEK_REASONING_EFFORT),
|
|
80
|
-
searchBaseURL: resolveEdgeSearchBaseURL(source.DEEPSEEK_SEARCH_BASE_URL),
|
|
81
|
-
streamIdleTimeoutMs: resolveEdgeStreamIdleTimeoutMs(
|
|
82
|
-
source.DEEPSEEK_STREAM_IDLE_TIMEOUT_MS,
|
|
83
|
-
),
|
|
84
|
-
commandTimeoutPolicy: resolveEdgeCommandTimeoutPolicy(
|
|
85
|
-
source.DSH_EDGE_DEFAULT_COMMAND_TIMEOUT_MS,
|
|
86
|
-
source.DSH_EDGE_MAX_COMMAND_TIMEOUT_MS,
|
|
87
|
-
),
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Whether the deployment supplies a non-blank DeepSeek Worker binding. */
|
|
92
|
-
export function edgeDeploymentApiKeyConfigured(source: EdgeDeploymentConfigSource): boolean {
|
|
93
|
-
return typeof source.DEEPSEEK_API_KEY === 'string'
|
|
94
|
-
&& source.DEEPSEEK_API_KEY.trim().length > 0
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Omit URL components that commonly carry credentials from the browser projection. */
|
|
98
|
-
function projectedEdgeBaseURL(raw?: string): string {
|
|
99
|
-
const value = resolveEdgeBaseURL(raw)
|
|
100
|
-
const parsed = new URL(value)
|
|
101
|
-
if (parsed.search.length === 0 && parsed.hash.length === 0) return value
|
|
102
|
-
parsed.search = ''
|
|
103
|
-
parsed.hash = ''
|
|
104
|
-
return parsed.href
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Resolve the effective runtime profile without retaining or returning a credential value. */
|
|
108
|
-
export function resolveEdgeDeploymentProfile(
|
|
109
|
-
source: EdgeDeploymentConfigSource,
|
|
110
|
-
): EdgeDeploymentProfile {
|
|
111
|
-
return {
|
|
112
|
-
shell: source.LOADER === undefined ? 'just-bash-direct' : 'just-bash-isolated',
|
|
113
|
-
storage: 'durable-object-sqlite-vfs',
|
|
114
|
-
deploymentId: resolveEdgeDeploymentId(),
|
|
115
|
-
apiKeyConfigured: edgeDeploymentApiKeyConfigured(source),
|
|
116
|
-
baseURL: projectedEdgeBaseURL(source.DEEPSEEK_BASE_URL),
|
|
117
|
-
maxTokens: resolveEdgeMaxOutputTokens(source.DEEPSEEK_MAX_OUTPUT_TOKENS),
|
|
118
|
-
model: resolveEdgeModel(source.DEEPSEEK_MODEL),
|
|
119
|
-
reasoningEffort: resolveEdgeReasoningEffort(source.DEEPSEEK_REASONING_EFFORT),
|
|
120
|
-
searchBaseURL: resolveEdgeSearchBaseURL(source.DEEPSEEK_SEARCH_BASE_URL),
|
|
121
|
-
streamIdleTimeoutMs: resolveEdgeStreamIdleTimeoutMs(
|
|
122
|
-
source.DEEPSEEK_STREAM_IDLE_TIMEOUT_MS,
|
|
123
|
-
),
|
|
124
|
-
commandTimeoutPolicy: resolveEdgeCommandTimeoutPolicy(
|
|
125
|
-
source.DSH_EDGE_DEFAULT_COMMAND_TIMEOUT_MS,
|
|
126
|
-
source.DSH_EDGE_MAX_COMMAND_TIMEOUT_MS,
|
|
127
|
-
),
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Validate the default turn configuration and return the public runtime identity. */
|
|
132
|
-
export function resolveEdgeDeploymentHealth(source: EdgeDeploymentConfigSource) {
|
|
133
|
-
resolveEdgeDeploymentConfig(source)
|
|
134
|
-
const profile = resolveEdgeDeploymentProfile(source)
|
|
135
|
-
return {
|
|
136
|
-
ok: true,
|
|
137
|
-
service: 'dsh-edge',
|
|
138
|
-
storage: profile.storage,
|
|
139
|
-
shell: profile.shell,
|
|
140
|
-
agent: 'upstream-react-loop-agent',
|
|
141
|
-
access: 'single-owner-cookie',
|
|
142
|
-
deploymentId: profile.deploymentId,
|
|
143
|
-
version: DSH_EDGE_VERSION,
|
|
144
|
-
upstreamVersion: DSH_EDGE_UPSTREAM_VERSION,
|
|
145
|
-
status: 'ready',
|
|
146
|
-
} as const
|
|
147
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Direct-build replacement for Computer's embedded Dynamic Worker modules.
|
|
3
|
-
*
|
|
4
|
-
* Direct deployments have no Loader binding, so they cannot select
|
|
5
|
-
* `WorkerShellBackend`. The alias keeps Computer's filesystem and command
|
|
6
|
-
* adapters available without uploading an unreachable shell runtime.
|
|
7
|
-
*/
|
|
8
|
-
const directShellCore = Object.freeze({})
|
|
9
|
-
|
|
10
|
-
export default directShellCore
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
/** Byte ceiling shared by shell execution, retention, and truncation rendering. */
|
|
2
|
-
export const EDGE_SHELL_OUTPUT_LIMIT_BYTES = 65_536
|
|
3
|
-
|
|
4
|
-
/** Standard shell-event result used when direct just-bash stops at its output budget. */
|
|
5
|
-
export const DIRECT_SHELL_OUTPUT_TRUNCATED = 'dsh-edge:direct-output-truncated'
|
package/src/direct-shell.ts
DELETED
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
/** In-process just-bash backend for the Cloudflare Computer workspace runtime. */
|
|
2
|
-
|
|
3
|
-
import type { BackendHandle, WorkspaceBackend } from '@cloudflare/computer'
|
|
4
|
-
import {
|
|
5
|
-
WorkspaceFsAdapter,
|
|
6
|
-
defineArtifactsCommand,
|
|
7
|
-
defineAssetsCommand,
|
|
8
|
-
defineGitCommand,
|
|
9
|
-
type WorkspaceFs,
|
|
10
|
-
} from '@cloudflare/computer/backends/worker-shell'
|
|
11
|
-
import {
|
|
12
|
-
Bash,
|
|
13
|
-
type BashExecResult,
|
|
14
|
-
type IFileSystem,
|
|
15
|
-
} from 'just-bash/browser'
|
|
16
|
-
import {
|
|
17
|
-
DIRECT_SHELL_OUTPUT_TRUNCATED,
|
|
18
|
-
EDGE_SHELL_OUTPUT_LIMIT_BYTES,
|
|
19
|
-
} from './direct-shell-protocol.ts'
|
|
20
|
-
|
|
21
|
-
const DEFAULT_BACKEND_ID = 'worker-shell'
|
|
22
|
-
const DEFAULT_CWD = '/workspace'
|
|
23
|
-
type DirectBackendHost = Parameters<WorkspaceBackend['connect']>[0]
|
|
24
|
-
type WorkspaceSyncRpc = BackendHandle['rpc']['sync']
|
|
25
|
-
type ShellExec = BackendHandle['rpc']['shell']['exec']
|
|
26
|
-
type ShellExecInput = Parameters<ShellExec>[0]
|
|
27
|
-
type ShellExecEnvelope = Awaited<ReturnType<ShellExec>>
|
|
28
|
-
type ShellExecEvent = ShellExecEnvelope['events'] extends ReadableStream<infer Event>
|
|
29
|
-
? Event
|
|
30
|
-
: never
|
|
31
|
-
|
|
32
|
-
interface DirectBashExecOptions {
|
|
33
|
-
cwd?: string
|
|
34
|
-
env?: Record<string, string>
|
|
35
|
-
stdin?: string
|
|
36
|
-
stdinKind?: 'bytes'
|
|
37
|
-
signal?: AbortSignal
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface DirectExecution {
|
|
41
|
-
controller: AbortController
|
|
42
|
-
done: Promise<void>
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface DirectBashOutcome {
|
|
46
|
-
result: BashExecResult
|
|
47
|
-
terminalResult?: typeof DIRECT_SHELL_OUTPUT_TRUNCATED
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Options for the free in-process shell backend. */
|
|
51
|
-
export interface DirectShellBackendOptions {
|
|
52
|
-
id?: string
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Run just-bash in the owning Durable Object against Computer's local VFS. */
|
|
56
|
-
export class DirectShellBackend implements WorkspaceBackend {
|
|
57
|
-
readonly type = 'direct-shell'
|
|
58
|
-
readonly id: string
|
|
59
|
-
|
|
60
|
-
constructor(options: DirectShellBackendOptions = {}) {
|
|
61
|
-
this.id = options.id ?? DEFAULT_BACKEND_ID
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Connect one workspace-local shell runtime. */
|
|
65
|
-
connect(host: DirectBackendHost): Promise<BackendHandle> {
|
|
66
|
-
const runtime = new DirectShellRuntime(host)
|
|
67
|
-
return Promise.resolve({
|
|
68
|
-
rpc: {
|
|
69
|
-
sync: disabledSyncRpc(),
|
|
70
|
-
shell: {
|
|
71
|
-
exec: input => runtime.exec(input),
|
|
72
|
-
getExec: input => runtime.getExec(input.id),
|
|
73
|
-
killExec: input => runtime.killExec(input.id, input.signal),
|
|
74
|
-
disposeExec: input => runtime.disposeExec(input.id),
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
sync: 'none',
|
|
78
|
-
close: () => runtime.close(),
|
|
79
|
-
})
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
class DirectShellRuntime {
|
|
84
|
-
private readonly fs: WorkspaceFsAdapter
|
|
85
|
-
private readonly executions = new Map<string, DirectExecution>()
|
|
86
|
-
private closed = false
|
|
87
|
-
|
|
88
|
-
constructor(private readonly host: DirectBackendHost) {
|
|
89
|
-
this.fs = new WorkspaceFsAdapter(workspaceFs(host))
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
exec(input: ShellExecInput): Promise<ShellExecEnvelope> {
|
|
93
|
-
if (this.closed) throw directShellError('ECLOSED', 'direct shell backend is closed')
|
|
94
|
-
const id = input.id ?? crypto.randomUUID()
|
|
95
|
-
if (this.executions.has(id)) {
|
|
96
|
-
throw directShellError('EEXEC_BUSY', `execution ${id} is running`)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const controller = new AbortController()
|
|
100
|
-
const result = Promise.resolve().then(() => this.run(input, controller.signal))
|
|
101
|
-
const done = result.then(() => undefined, () => undefined)
|
|
102
|
-
this.executions.set(id, { controller, done })
|
|
103
|
-
return Promise.resolve({
|
|
104
|
-
id,
|
|
105
|
-
events: executionEvents(
|
|
106
|
-
id,
|
|
107
|
-
result,
|
|
108
|
-
() => { this.deleteExecution(id, controller) },
|
|
109
|
-
() => this.disposeExec(id),
|
|
110
|
-
),
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
getExec(id: string): Promise<ShellExecEnvelope> {
|
|
115
|
-
throw directShellError('ENOENT', `no such exec: ${id}`)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
killExec(id: string, signal = 'SIGTERM'): Promise<void> {
|
|
119
|
-
this.executions.get(id)?.controller.abort(
|
|
120
|
-
directShellError('EEXEC_CANCELLED', `execution cancelled with ${signal}`),
|
|
121
|
-
)
|
|
122
|
-
return Promise.resolve()
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async disposeExec(id: string): Promise<void> {
|
|
126
|
-
const execution = this.executions.get(id)
|
|
127
|
-
if (execution === undefined) return
|
|
128
|
-
execution.controller.abort(
|
|
129
|
-
directShellError('EEXEC_DISPOSED', `execution disposed: ${id}`),
|
|
130
|
-
)
|
|
131
|
-
await execution.done
|
|
132
|
-
this.deleteExecution(id, execution.controller)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async close(): Promise<void> {
|
|
136
|
-
if (this.closed) return
|
|
137
|
-
this.closed = true
|
|
138
|
-
const active = [...this.executions.values()]
|
|
139
|
-
for (const execution of active) {
|
|
140
|
-
execution.controller.abort(directShellError('ECLOSED', 'direct shell backend is closing'))
|
|
141
|
-
}
|
|
142
|
-
await Promise.allSettled(active.map(execution => execution.done))
|
|
143
|
-
this.executions.clear()
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
private async run(input: ShellExecInput, signal: AbortSignal): Promise<DirectBashOutcome> {
|
|
147
|
-
const timeoutLimit = input.timeoutMs !== undefined && input.timeoutMs > 0
|
|
148
|
-
? { maxExecutionTimeMs: input.timeoutMs }
|
|
149
|
-
: {}
|
|
150
|
-
const bash = new Bash({
|
|
151
|
-
// Computer and just-bash accept the same runtime encodings, but their
|
|
152
|
-
// interfaces spell the optional UTF-8 alias set differently.
|
|
153
|
-
fs: this.fs as unknown as IFileSystem,
|
|
154
|
-
cwd: input.cwd ?? DEFAULT_CWD,
|
|
155
|
-
...(input.env === undefined ? {} : { env: input.env }),
|
|
156
|
-
customCommands: [
|
|
157
|
-
defineGitCommand(this.host),
|
|
158
|
-
defineAssetsCommand({}),
|
|
159
|
-
defineArtifactsCommand(this.host),
|
|
160
|
-
],
|
|
161
|
-
defenseInDepth: { enabled: 'auto' },
|
|
162
|
-
executionLimitProfile: 'hardened',
|
|
163
|
-
executionLimits: {
|
|
164
|
-
maxOutputSize: EDGE_SHELL_OUTPUT_LIMIT_BYTES,
|
|
165
|
-
...timeoutLimit,
|
|
166
|
-
},
|
|
167
|
-
})
|
|
168
|
-
const directBash = bash as unknown as {
|
|
169
|
-
exec(command: string, options: DirectBashExecOptions): Promise<BashExecResult>
|
|
170
|
-
}
|
|
171
|
-
try {
|
|
172
|
-
const result = await directBash.exec(input.source, {
|
|
173
|
-
cwd: input.cwd ?? DEFAULT_CWD,
|
|
174
|
-
...(input.env === undefined ? {} : { env: input.env }),
|
|
175
|
-
...(input.stdin === undefined
|
|
176
|
-
? {}
|
|
177
|
-
: { stdin: latin1FromBytes(input.stdin), stdinKind: 'bytes' as const }),
|
|
178
|
-
signal,
|
|
179
|
-
})
|
|
180
|
-
return {
|
|
181
|
-
result,
|
|
182
|
-
...(hitDirectOutputLimit(result)
|
|
183
|
-
? { terminalResult: DIRECT_SHELL_OUTPUT_TRUNCATED }
|
|
184
|
-
: {}),
|
|
185
|
-
}
|
|
186
|
-
} catch (error) {
|
|
187
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
188
|
-
return {
|
|
189
|
-
result: {
|
|
190
|
-
stdout: '',
|
|
191
|
-
stderr: `${message}\n`,
|
|
192
|
-
exitCode: signal.aborted ? 130 : 1,
|
|
193
|
-
env: input.env ?? {},
|
|
194
|
-
},
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
private deleteExecution(id: string, controller: AbortController): void {
|
|
200
|
-
if (this.executions.get(id)?.controller === controller) this.executions.delete(id)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function workspaceFs(host: DirectBackendHost): WorkspaceFs {
|
|
205
|
-
const fs = host.fs
|
|
206
|
-
return {
|
|
207
|
-
readFile: fs.readFile.bind(fs),
|
|
208
|
-
exists: async path => await statOrNull(() => fs.stat(path)) !== null,
|
|
209
|
-
stat: path => fs.stat(path),
|
|
210
|
-
statOrNull: path => statOrNull(() => fs.stat(path)),
|
|
211
|
-
lstat: path => fs.lstat(path),
|
|
212
|
-
lstatOrNull: path => statOrNull(() => fs.lstat(path)),
|
|
213
|
-
readdir: path => fs.readdir(path),
|
|
214
|
-
find: (directory, pattern) => fs.find(directory, pattern),
|
|
215
|
-
ls: prefix => fs.ls(prefix),
|
|
216
|
-
grep: (pattern, path, options) => fs.grep(pattern, path, options),
|
|
217
|
-
readlink: path => fs.readlink(path),
|
|
218
|
-
writeFile: (path, content, options) => fs.writeFile(path, content, options),
|
|
219
|
-
mkdir: (path, options) => fs.mkdir(path, options),
|
|
220
|
-
rm: (path, options) => fs.rm(path, options),
|
|
221
|
-
chmod: (path, mode) => fs.chmod(path, mode),
|
|
222
|
-
symlink: (target, path) => fs.symlink(target, path),
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function statOrNull<T>(read: () => Promise<T>): Promise<T | null> {
|
|
227
|
-
try {
|
|
228
|
-
return await read()
|
|
229
|
-
} catch (error) {
|
|
230
|
-
if (hasErrorCode(error, 'ENOENT')) return null
|
|
231
|
-
throw error
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function hasErrorCode(error: unknown, code: string): boolean {
|
|
236
|
-
return typeof error === 'object'
|
|
237
|
-
&& error !== null
|
|
238
|
-
&& 'code' in error
|
|
239
|
-
&& error.code === code
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function executionEvents(
|
|
243
|
-
id: string,
|
|
244
|
-
result: Promise<DirectBashOutcome>,
|
|
245
|
-
onTerminal: () => void,
|
|
246
|
-
onCancel: () => Promise<void>,
|
|
247
|
-
): ReadableStream<ShellExecEvent> {
|
|
248
|
-
const encoder = new TextEncoder()
|
|
249
|
-
let events: ShellExecEvent[] | undefined
|
|
250
|
-
return new ReadableStream({
|
|
251
|
-
async pull(controller) {
|
|
252
|
-
try {
|
|
253
|
-
events ??= shellExecEvents(id, encoder, await result)
|
|
254
|
-
const event = events.shift()
|
|
255
|
-
if (event === undefined) return
|
|
256
|
-
controller.enqueue(event)
|
|
257
|
-
if (event.name === 'exit') {
|
|
258
|
-
controller.close()
|
|
259
|
-
onTerminal()
|
|
260
|
-
}
|
|
261
|
-
} catch (error) {
|
|
262
|
-
controller.error(error)
|
|
263
|
-
onTerminal()
|
|
264
|
-
}
|
|
265
|
-
},
|
|
266
|
-
cancel: onCancel,
|
|
267
|
-
}, { highWaterMark: 0 })
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function shellExecEvents(
|
|
271
|
-
id: string,
|
|
272
|
-
encoder: TextEncoder,
|
|
273
|
-
outcome: DirectBashOutcome,
|
|
274
|
-
): ShellExecEvent[] {
|
|
275
|
-
const { result } = outcome
|
|
276
|
-
const events: ShellExecEvent[] = []
|
|
277
|
-
let seq = 0
|
|
278
|
-
if (result.stdout.length > 0) {
|
|
279
|
-
events.push({ id, seq: ++seq, name: 'stdout', value: encoder.encode(result.stdout) })
|
|
280
|
-
}
|
|
281
|
-
if (result.stderr.length > 0) {
|
|
282
|
-
events.push({ id, seq: ++seq, name: 'stderr', value: encoder.encode(result.stderr) })
|
|
283
|
-
}
|
|
284
|
-
events.push({
|
|
285
|
-
id,
|
|
286
|
-
seq: ++seq,
|
|
287
|
-
name: 'exit',
|
|
288
|
-
code: result.exitCode,
|
|
289
|
-
...(outcome.terminalResult === undefined ? {} : { result: outcome.terminalResult }),
|
|
290
|
-
})
|
|
291
|
-
return events
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function hitDirectOutputLimit(result: BashExecResult): boolean {
|
|
295
|
-
if (result.exitCode !== 126) return false
|
|
296
|
-
const accountedBytes = result.internalOutputAccounting?.stderr
|
|
297
|
-
if (accountedBytes === undefined) return false
|
|
298
|
-
const stderr = new TextEncoder().encode(result.stderr)
|
|
299
|
-
if (accountedBytes < 0 || accountedBytes > stderr.byteLength) return false
|
|
300
|
-
const interpreterDiagnostic = new TextDecoder().decode(stderr.subarray(accountedBytes))
|
|
301
|
-
return [
|
|
302
|
-
`limit exceeded (${EDGE_SHELL_OUTPUT_LIMIT_BYTES} bytes)`,
|
|
303
|
-
`total output size exceeded (>${EDGE_SHELL_OUTPUT_LIMIT_BYTES} bytes), `
|
|
304
|
-
+ 'increase executionLimits.maxOutputSize',
|
|
305
|
-
].some(diagnostic => interpreterDiagnostic.includes(diagnostic))
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function latin1FromBytes(bytes: Uint8Array): string {
|
|
309
|
-
let result = ''
|
|
310
|
-
for (const byte of bytes) result += String.fromCharCode(byte)
|
|
311
|
-
return result
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function directShellError(code: string, message: string): Error & { code: string } {
|
|
315
|
-
const error = new Error(message) as Error & { code: string }
|
|
316
|
-
error.name = 'DirectShellError'
|
|
317
|
-
error.code = code
|
|
318
|
-
return error
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function disabledSyncRpc(): WorkspaceSyncRpc {
|
|
322
|
-
const refuse = (name: string): never => {
|
|
323
|
-
throw directShellError(
|
|
324
|
-
'ENOSYS',
|
|
325
|
-
`DirectShellBackend: sync.${name} must not be called because sync is disabled`,
|
|
326
|
-
)
|
|
327
|
-
}
|
|
328
|
-
return {
|
|
329
|
-
push: () => refuse('push'),
|
|
330
|
-
fetchChanges: () => refuse('fetchChanges'),
|
|
331
|
-
watermarks: () => refuse('watermarks'),
|
|
332
|
-
readEntry: () => refuse('readEntry'),
|
|
333
|
-
hasObjects: () => refuse('hasObjects'),
|
|
334
|
-
fetchObjects: () => new ReadableStream({
|
|
335
|
-
start(controller) {
|
|
336
|
-
controller.error(directShellError('ENOSYS', 'DirectShellBackend: sync is disabled'))
|
|
337
|
-
},
|
|
338
|
-
}),
|
|
339
|
-
pushObjects: () => refuse('pushObjects'),
|
|
340
|
-
}
|
|
341
|
-
}
|