dsh-edge 0.1.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.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +183 -0
- package/README.zh.md +183 -0
- package/THIRD_PARTY_NOTICES.md +199 -0
- package/dist/_headers +3 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/fonts/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/fonts/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/fonts/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/fonts/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/fonts/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/index-C-1AiF3k.js +112 -0
- package/dist/assets/index-CSGf6Qzd.css +1 -0
- package/dist/assets/langs/c-BIGW1oBm.js +2 -0
- package/dist/assets/langs/cpp-DIPi6g--.js +2 -0
- package/dist/assets/langs/csharp-DSvCPggb.js +2 -0
- package/dist/assets/langs/css-CLj8gQPS.js +2 -0
- package/dist/assets/langs/go-C27-OAKa.js +2 -0
- package/dist/assets/langs/html-CfGypltT.js +2 -0
- package/dist/assets/langs/ini-BEwlwnbL.js +2 -0
- package/dist/assets/langs/java-CylS5w8V.js +2 -0
- package/dist/assets/langs/kotlin-BdnUsdx6.js +2 -0
- package/dist/assets/langs/less-B1dDrJ26.js +2 -0
- package/dist/assets/langs/lua-BaeVxFsk.js +2 -0
- package/dist/assets/langs/markdown-Cvjx9yec.js +2 -0
- package/dist/assets/langs/mdx-Cmh6b_Ma.js +2 -0
- package/dist/assets/langs/php-D-uCvoST.js +2 -0
- package/dist/assets/langs/python-B6aJPvgy.js +2 -0
- package/dist/assets/langs/ruby-DEItuUFs.js +2 -0
- package/dist/assets/langs/rust-B1yitclQ.js +2 -0
- package/dist/assets/langs/scss-D5BDwBP9.js +2 -0
- package/dist/assets/langs/sql-CRqJ_cUM.js +2 -0
- package/dist/assets/langs/swift-D82vCrfD.js +2 -0
- package/dist/assets/langs/toml-vGWfd6FD.js +2 -0
- package/dist/assets/langs/xml-sdJ4AIDG.js +2 -0
- package/dist/assets/langs/yaml-Buea-lGh.js +2 -0
- package/dist/assets/vendor-Cjbwl5VI.js +411 -0
- package/dist/assets/vendor-CjyC-hUb.css +1 -0
- package/dist/favicon.svg +8 -0
- package/dist/index.html +35 -0
- package/dist/manifest.webmanifest +16 -0
- package/dist/plugins/@deepseek-ai/dsh-api-gateway/client.js +420 -0
- package/dist/plugins/@deepseek-ai/dsh-api-remotes/client.js +5937 -0
- package/dist/plugins/@deepseek-ai/dsh-client-connection/client.js +10207 -0
- package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +1231 -0
- package/dist/plugins/@deepseek-ai/dsh-client-modules/client.js +259 -0
- package/dist/plugins/@deepseek-ai/dsh-client-runtime/client.js +10539 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +1725 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +1116 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +9832 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +373 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-edge/client.js +549 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +859 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +280 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +456 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +804 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +461 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings/client.js +254 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +609 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +292 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +327 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +710 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +1312 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +1630 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +7370 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +700 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +474 -0
- package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +2426 -0
- package/dist/plugins/@deepseek-ai/dsh-typert-registry/client.js +1372 -0
- package/package.json +97 -0
- package/scripts/assemble-web.mjs +230 -0
- package/scripts/bundle-size.d.mts +2 -0
- package/scripts/bundle-size.mjs +26 -0
- package/scripts/bundle.mjs +53 -0
- package/scripts/cli.d.mts +39 -0
- package/scripts/cli.mjs +496 -0
- package/scripts/install.d.mts +149 -0
- package/scripts/install.mjs +989 -0
- package/scripts/legal-files.mjs +24 -0
- package/scripts/verify-packed.mjs +32 -0
- package/scripts/wrangler-config.d.mts +18 -0
- package/scripts/wrangler-config.mjs +84 -0
- package/src/agent.ts +119 -0
- package/src/auth.ts +352 -0
- package/src/deepseek.ts +231 -0
- package/src/deployment.ts +147 -0
- package/src/direct-shell-core-empty.ts +10 -0
- package/src/direct-shell-protocol.ts +5 -0
- package/src/direct-shell.ts +341 -0
- package/src/do-session-persistence.ts +1064 -0
- package/src/edge-api.ts +796 -0
- package/src/edge-credentials.ts +56 -0
- package/src/edge-remotes.ts +41 -0
- package/src/edge-workspace-store.ts +339 -0
- package/src/http.ts +212 -0
- package/src/index.ts +196 -0
- package/src/instance.ts +1007 -0
- package/src/isolated-direct-shell-unavailable.ts +11 -0
- package/src/protocol.ts +39 -0
- package/src/release.ts +9 -0
- package/src/session-store.ts +1110 -0
- package/src/sse.ts +86 -0
- package/src/web-search.ts +38 -0
- package/src/workspace.ts +270 -0
- package/wrangler.jsonc +56 -0
package/src/sse.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** Server-sent-event encoding for canonical DSH SessionEvents. */
|
|
2
|
+
|
|
3
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
|
4
|
+
|
|
5
|
+
const encoder = new TextEncoder()
|
|
6
|
+
const MAX_LIVE_STREAM_QUEUED_BYTES = 1_048_576
|
|
7
|
+
|
|
8
|
+
/** Encode one upstream event without translating its envelope or payload. */
|
|
9
|
+
export function encodeSessionEvent(event: SessionEvent): Uint8Array {
|
|
10
|
+
const eventType = encodeEventType(event.type)
|
|
11
|
+
return encoder.encode(
|
|
12
|
+
`id: ${event.seq}\nevent: ${eventType}\ndata: ${JSON.stringify(event)}\n\n`,
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Keep canonical names stable while making arbitrary ignorable names one SSE field. */
|
|
17
|
+
function encodeEventType(type: string): string {
|
|
18
|
+
return type
|
|
19
|
+
.replaceAll('%', '%25')
|
|
20
|
+
.replaceAll('\r', '%0D')
|
|
21
|
+
.replaceAll('\n', '%0A')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Headers used by live and finite replay streams. */
|
|
25
|
+
export function edgeEventStreamHeaders(extra: HeadersInit = {}): Headers {
|
|
26
|
+
const headers = new Headers(extra)
|
|
27
|
+
headers.set('cache-control', 'no-cache, no-transform')
|
|
28
|
+
headers.set('content-type', 'text/event-stream; charset=utf-8')
|
|
29
|
+
headers.set('x-accel-buffering', 'no')
|
|
30
|
+
return headers
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LiveSessionEventStream {
|
|
34
|
+
readonly stream: ReadableStream<Uint8Array>
|
|
35
|
+
/** Settles after the producer closes or errors the client-visible stream. */
|
|
36
|
+
readonly completion: Promise<void>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Run an event producer whose escaped failures become client-visible stream errors. */
|
|
40
|
+
export function createLiveSessionEventStream(
|
|
41
|
+
run: (publish: (event: SessionEvent) => void) => Promise<void>,
|
|
42
|
+
onFailure: (error: unknown) => void,
|
|
43
|
+
): LiveSessionEventStream {
|
|
44
|
+
let connected = true
|
|
45
|
+
let settle!: () => void
|
|
46
|
+
const completion = new Promise<void>((resolve) => { settle = resolve })
|
|
47
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
48
|
+
start(controller) {
|
|
49
|
+
void Promise.resolve().then(async () => run((event) => {
|
|
50
|
+
if (!connected) return
|
|
51
|
+
try {
|
|
52
|
+
const chunk = encodeSessionEvent(event)
|
|
53
|
+
if (controller.desiredSize === null || controller.desiredSize < chunk.byteLength) {
|
|
54
|
+
connected = false
|
|
55
|
+
controller.error(new Error('dsh-edge event stream client is too slow.'))
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
controller.enqueue(chunk)
|
|
59
|
+
} catch {
|
|
60
|
+
connected = false
|
|
61
|
+
}
|
|
62
|
+
})).then(() => {
|
|
63
|
+
if (connected) {
|
|
64
|
+
try {
|
|
65
|
+
controller.close()
|
|
66
|
+
} catch {
|
|
67
|
+
connected = false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}, (error: unknown) => {
|
|
71
|
+
onFailure(error)
|
|
72
|
+
if (connected) {
|
|
73
|
+
connected = false
|
|
74
|
+
controller.error(new Error('dsh-edge turn transport failed.'))
|
|
75
|
+
}
|
|
76
|
+
}).finally(settle)
|
|
77
|
+
},
|
|
78
|
+
cancel() {
|
|
79
|
+
connected = false
|
|
80
|
+
},
|
|
81
|
+
}, {
|
|
82
|
+
highWaterMark: MAX_LIVE_STREAM_QUEUED_BYTES,
|
|
83
|
+
size: chunk => chunk.byteLength,
|
|
84
|
+
})
|
|
85
|
+
return { stream, completion }
|
|
86
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Upstream DeepSeek Web Search composition for the Cloudflare runtime. */
|
|
2
|
+
|
|
3
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
4
|
+
import * as TimeoutPolicy from '@deepseek-ai/dsh-tool-call-timeout-policy'
|
|
5
|
+
import * as ToolWeb from '@deepseek-ai/dsh-tool-web'
|
|
6
|
+
import WebRuntime from '@deepseek-ai/dsh-web'
|
|
7
|
+
import * as DeepSeekWebSearch from '@deepseek-ai/dsh-web-search-deepseek'
|
|
8
|
+
|
|
9
|
+
/** Validate the credential-bearing Anthropic-compatible search endpoint. */
|
|
10
|
+
export function resolveEdgeSearchBaseURL(raw?: string): string {
|
|
11
|
+
const value = raw ?? DeepSeekWebSearch.DEEPSEEK_DEFAULT_BASE_URL
|
|
12
|
+
let parsed: URL
|
|
13
|
+
try {
|
|
14
|
+
parsed = new URL(value)
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error('DEEPSEEK_SEARCH_BASE_URL must be a valid HTTP(S) URL.')
|
|
17
|
+
}
|
|
18
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
19
|
+
throw new Error('DEEPSEEK_SEARCH_BASE_URL must be a valid HTTP(S) URL.')
|
|
20
|
+
}
|
|
21
|
+
if (parsed.username.length > 0 || parsed.password.length > 0) {
|
|
22
|
+
throw new Error('DEEPSEEK_SEARCH_BASE_URL must be an HTTP(S) URL without credentials.')
|
|
23
|
+
}
|
|
24
|
+
if (parsed.search.length > 0 || parsed.hash.length > 0) {
|
|
25
|
+
throw new Error('DEEPSEEK_SEARCH_BASE_URL must not contain a query or fragment.')
|
|
26
|
+
}
|
|
27
|
+
return parsed.href.replace(/\/+$/u, '')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Mount the shipped upstream search seam, provider, and search-only model tool. */
|
|
31
|
+
export async function installEdgeWebSearch(ctx: Context, rawBaseURL?: string): Promise<void> {
|
|
32
|
+
await ctx.plugin(WebRuntime, { searchProvider: DeepSeekWebSearch.DEEPSEEK_PROVIDER_ID })
|
|
33
|
+
await ctx.plugin(DeepSeekWebSearch, {
|
|
34
|
+
baseURL: resolveEdgeSearchBaseURL(rawBaseURL),
|
|
35
|
+
})
|
|
36
|
+
await ctx.plugin(TimeoutPolicy)
|
|
37
|
+
await ctx.plugin(ToolWeb, { search: true, fetch: false })
|
|
38
|
+
}
|
package/src/workspace.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/** Validation and execution helpers for the Cloudflare Computer workspace. */
|
|
2
|
+
|
|
3
|
+
import { getWorkspace } from '@cloudflare/computer'
|
|
4
|
+
import type { EdgeShellResult } from './agent.ts'
|
|
5
|
+
import {
|
|
6
|
+
DIRECT_SHELL_OUTPUT_TRUNCATED,
|
|
7
|
+
EDGE_SHELL_OUTPUT_LIMIT_BYTES,
|
|
8
|
+
} from './direct-shell-protocol.ts'
|
|
9
|
+
import { EdgeExecutionId } from './protocol.ts'
|
|
10
|
+
|
|
11
|
+
const MAX_COMMAND_BYTES = 16_384
|
|
12
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 120_000
|
|
13
|
+
const DEFAULT_MAX_COMMAND_TIMEOUT_MS = 120_000
|
|
14
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647
|
|
15
|
+
const textEncoder = new TextEncoder()
|
|
16
|
+
|
|
17
|
+
export const MAX_TEXT_FILE_BYTES = 1_048_576
|
|
18
|
+
|
|
19
|
+
/** Invalid workspace input reported to an HTTP caller. */
|
|
20
|
+
export class EdgeWorkspaceRequestError extends Error {
|
|
21
|
+
constructor(readonly status: number, message: string) {
|
|
22
|
+
super(message)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Cloudflare Computer client returned by the workspace binding. */
|
|
27
|
+
export type EdgeWorkspace = Awaited<ReturnType<typeof getWorkspace>>
|
|
28
|
+
|
|
29
|
+
/** Validated deployment policy applied to every Computer shell execution. */
|
|
30
|
+
export interface EdgeCommandTimeoutPolicy {
|
|
31
|
+
defaultTimeoutMs: number
|
|
32
|
+
maxTimeoutMs: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resolve the default and caller-selectable ceiling for Computer commands. */
|
|
36
|
+
export function resolveEdgeCommandTimeoutPolicy(
|
|
37
|
+
defaultRaw?: string,
|
|
38
|
+
maxRaw?: string,
|
|
39
|
+
): EdgeCommandTimeoutPolicy {
|
|
40
|
+
const maxTimeoutMs = resolveDeploymentTimeout(
|
|
41
|
+
maxRaw,
|
|
42
|
+
DEFAULT_MAX_COMMAND_TIMEOUT_MS,
|
|
43
|
+
'DSH_EDGE_MAX_COMMAND_TIMEOUT_MS',
|
|
44
|
+
)
|
|
45
|
+
const defaultTimeoutMs = resolveDeploymentTimeout(
|
|
46
|
+
defaultRaw,
|
|
47
|
+
DEFAULT_COMMAND_TIMEOUT_MS,
|
|
48
|
+
'DSH_EDGE_DEFAULT_COMMAND_TIMEOUT_MS',
|
|
49
|
+
)
|
|
50
|
+
if (defaultTimeoutMs > maxTimeoutMs) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'dsh-edge: DSH_EDGE_DEFAULT_COMMAND_TIMEOUT_MS must be no greater than '
|
|
53
|
+
+ 'DSH_EDGE_MAX_COMMAND_TIMEOUT_MS',
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
return { defaultTimeoutMs, maxTimeoutMs }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface EdgeWorkspaceFiles {
|
|
60
|
+
stat(path: string): Promise<{ size: number }>
|
|
61
|
+
readFile(path: string): Promise<ReadableStream<Uint8Array>>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Refuse an oversized VFS entry before and while consuming its opened byte stream. */
|
|
65
|
+
export async function readBoundedWorkspaceFile(
|
|
66
|
+
files: EdgeWorkspaceFiles,
|
|
67
|
+
path: string,
|
|
68
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
69
|
+
const entry = await files.stat(path)
|
|
70
|
+
if (entry.size > MAX_TEXT_FILE_BYTES) {
|
|
71
|
+
throw new EdgeWorkspaceRequestError(413, 'Text files are limited to 1 MiB in the prototype API.')
|
|
72
|
+
}
|
|
73
|
+
const stream = await files.readFile(path)
|
|
74
|
+
const reader = stream.getReader()
|
|
75
|
+
const chunks: Uint8Array[] = []
|
|
76
|
+
let byteLength = 0
|
|
77
|
+
try {
|
|
78
|
+
while (true) {
|
|
79
|
+
const next = await reader.read()
|
|
80
|
+
if (next.done) break
|
|
81
|
+
byteLength += next.value.byteLength
|
|
82
|
+
if (byteLength > MAX_TEXT_FILE_BYTES) {
|
|
83
|
+
chunks.length = 0
|
|
84
|
+
await reader.cancel().catch(() => undefined)
|
|
85
|
+
throw new EdgeWorkspaceRequestError(
|
|
86
|
+
413,
|
|
87
|
+
'Text files are limited to 1 MiB in the prototype API.',
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
chunks.push(next.value)
|
|
91
|
+
}
|
|
92
|
+
} finally {
|
|
93
|
+
reader.releaseLock()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const contents = new Uint8Array(byteLength)
|
|
97
|
+
let offset = 0
|
|
98
|
+
for (const chunk of chunks) {
|
|
99
|
+
contents.set(chunk, offset)
|
|
100
|
+
offset += chunk.byteLength
|
|
101
|
+
}
|
|
102
|
+
return contents
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Validate an absolute path inside the persistent workspace root. */
|
|
106
|
+
export function requireWorkspacePath(value: unknown): string {
|
|
107
|
+
if (
|
|
108
|
+
typeof value !== 'string'
|
|
109
|
+
|| (value !== '/workspace' && !value.startsWith('/workspace/'))
|
|
110
|
+
) {
|
|
111
|
+
throw new EdgeWorkspaceRequestError(400, 'A path below /workspace/ is required.')
|
|
112
|
+
}
|
|
113
|
+
if (value.includes('\0') || value.split('/').includes('..')) {
|
|
114
|
+
throw new EdgeWorkspaceRequestError(
|
|
115
|
+
400,
|
|
116
|
+
'Workspace paths cannot contain NUL bytes or parent traversal.',
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
return value
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Validate a just-bash command accepted from HTTP or a model tool call. */
|
|
123
|
+
export function requireCommand(value: unknown): string {
|
|
124
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
125
|
+
throw new EdgeWorkspaceRequestError(400, 'A non-empty command is required.')
|
|
126
|
+
}
|
|
127
|
+
if (textEncoder.encode(value).byteLength > MAX_COMMAND_BYTES) {
|
|
128
|
+
throw new EdgeWorkspaceRequestError(413, 'Commands are limited to 16 KiB.')
|
|
129
|
+
}
|
|
130
|
+
return value
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Execute one bounded just-bash command and normalize its result. */
|
|
134
|
+
export async function executeWorkspaceCommand(
|
|
135
|
+
workspace: EdgeWorkspace,
|
|
136
|
+
command: string,
|
|
137
|
+
cwd: string,
|
|
138
|
+
timeoutPolicy: EdgeCommandTimeoutPolicy,
|
|
139
|
+
timeoutMs?: number,
|
|
140
|
+
signal?: AbortSignal,
|
|
141
|
+
): Promise<EdgeShellResult> {
|
|
142
|
+
const effectiveTimeoutMs = timeoutMs ?? timeoutPolicy.defaultTimeoutMs
|
|
143
|
+
if (!Number.isInteger(effectiveTimeoutMs)
|
|
144
|
+
|| effectiveTimeoutMs <= 0
|
|
145
|
+
|| effectiveTimeoutMs > timeoutPolicy.maxTimeoutMs) {
|
|
146
|
+
throw new EdgeWorkspaceRequestError(
|
|
147
|
+
400,
|
|
148
|
+
`timeoutMs must be a positive integer no greater than ${timeoutPolicy.maxTimeoutMs}.`,
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
signal?.throwIfAborted()
|
|
152
|
+
await workspace.fs.mkdir('/workspace', { recursive: true })
|
|
153
|
+
signal?.throwIfAborted()
|
|
154
|
+
const deadline = commandDeadline(effectiveTimeoutMs)
|
|
155
|
+
using execution = await workspace.runtime.exec(command, {
|
|
156
|
+
cwd,
|
|
157
|
+
timeoutMs: effectiveTimeoutMs,
|
|
158
|
+
})
|
|
159
|
+
let interruptionRequested = false
|
|
160
|
+
const interrupt = (): Promise<void> => {
|
|
161
|
+
interruptionRequested = true
|
|
162
|
+
return execution.kill('SIGINT').catch(() => undefined)
|
|
163
|
+
}
|
|
164
|
+
const abort = (): void => {
|
|
165
|
+
void interrupt()
|
|
166
|
+
}
|
|
167
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
168
|
+
if (signal?.aborted === true) abort()
|
|
169
|
+
const stdout: Uint8Array[] = []
|
|
170
|
+
const stderr: Uint8Array[] = []
|
|
171
|
+
let retainedBytes = 0
|
|
172
|
+
let outputTruncated = false
|
|
173
|
+
let exitCode = -1
|
|
174
|
+
const reader = execution.getReader()
|
|
175
|
+
try {
|
|
176
|
+
while (true) {
|
|
177
|
+
const next = await reader.read()
|
|
178
|
+
if (next.done) break
|
|
179
|
+
const event = next.value
|
|
180
|
+
if (event.name === 'exit') {
|
|
181
|
+
deadline.complete()
|
|
182
|
+
exitCode = event.code
|
|
183
|
+
if (event.result === DIRECT_SHELL_OUTPUT_TRUNCATED) {
|
|
184
|
+
outputTruncated = true
|
|
185
|
+
interruptionRequested = true
|
|
186
|
+
}
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const remaining = EDGE_SHELL_OUTPUT_LIMIT_BYTES - retainedBytes
|
|
191
|
+
if (remaining > 0) {
|
|
192
|
+
// Copy rather than retain a subarray view: an oversized event may use
|
|
193
|
+
// a much larger backing buffer that must become collectible here.
|
|
194
|
+
const retained = event.value.slice(0, remaining)
|
|
195
|
+
if (event.name === 'stdout') stdout.push(retained)
|
|
196
|
+
else stderr.push(retained)
|
|
197
|
+
retainedBytes += retained.byteLength
|
|
198
|
+
}
|
|
199
|
+
if (event.value.byteLength > remaining && !outputTruncated) {
|
|
200
|
+
outputTruncated = true
|
|
201
|
+
await interrupt()
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
reader.releaseLock()
|
|
206
|
+
signal?.removeEventListener('abort', abort)
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
executionId: EdgeExecutionId(execution.id),
|
|
210
|
+
status: executionStatus(exitCode, interruptionRequested),
|
|
211
|
+
timedOut: deadline.timedOut,
|
|
212
|
+
exitCode,
|
|
213
|
+
stdout: decodeChunks(stdout),
|
|
214
|
+
stderr: decodeChunks(stderr),
|
|
215
|
+
outputTruncated,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function commandDeadline(timeoutMs: number): { complete(): void; readonly timedOut: boolean } {
|
|
220
|
+
const expiresAt = performance.now() + timeoutMs
|
|
221
|
+
let completedAt: number | undefined
|
|
222
|
+
return {
|
|
223
|
+
complete() { completedAt ??= performance.now() },
|
|
224
|
+
get timedOut() { return (completedAt ?? performance.now()) >= expiresAt },
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function executionStatus(
|
|
229
|
+
exitCode: number,
|
|
230
|
+
interruptionRequested: boolean,
|
|
231
|
+
): EdgeShellResult['status'] {
|
|
232
|
+
if (interruptionRequested) return 'cancelled'
|
|
233
|
+
if (exitCode === 0) return 'completed'
|
|
234
|
+
return 'failed'
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function resolveDeploymentTimeout(raw: string | undefined, fallback: number, name: string): number {
|
|
238
|
+
if (raw === undefined) return fallback
|
|
239
|
+
const value = Number(raw)
|
|
240
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
241
|
+
throw new Error(`dsh-edge: ${name} must be a positive integer`)
|
|
242
|
+
}
|
|
243
|
+
if (value > MAX_TIMER_DELAY_MS) {
|
|
244
|
+
throw new Error(`dsh-edge: ${name} must be no greater than ${MAX_TIMER_DELAY_MS}`)
|
|
245
|
+
}
|
|
246
|
+
return value
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function decodeChunks(chunks: Uint8Array[]): string {
|
|
250
|
+
const byteLength = chunks.reduce((total, chunk) => total + chunk.byteLength, 0)
|
|
251
|
+
const output = new Uint8Array(byteLength)
|
|
252
|
+
let offset = 0
|
|
253
|
+
for (const chunk of chunks) {
|
|
254
|
+
output.set(chunk, offset)
|
|
255
|
+
offset += chunk.byteLength
|
|
256
|
+
}
|
|
257
|
+
// A byte-exact cut can land inside one UTF-8 code point. The shell backend
|
|
258
|
+
// encoded these bytes from strings, so at most the final three bytes need
|
|
259
|
+
// dropping to retain a valid prefix without introducing U+FFFD expansion.
|
|
260
|
+
for (let dropped = 0; dropped <= 3 && dropped <= output.byteLength; dropped += 1) {
|
|
261
|
+
try {
|
|
262
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(
|
|
263
|
+
output.subarray(0, output.byteLength - dropped),
|
|
264
|
+
)
|
|
265
|
+
} catch {
|
|
266
|
+
// Try the preceding UTF-8 boundary.
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
throw new Error('dsh-edge: shell output was not valid UTF-8')
|
|
270
|
+
}
|
package/wrangler.jsonc
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
|
+
"name": "dsh-edge",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"compatibility_date": "2026-08-14",
|
|
6
|
+
"compatibility_flags": [
|
|
7
|
+
"nodejs_compat"
|
|
8
|
+
],
|
|
9
|
+
"minify": true,
|
|
10
|
+
"assets": {
|
|
11
|
+
"binding": "ASSETS",
|
|
12
|
+
"directory": "./dist",
|
|
13
|
+
"not_found_handling": "single-page-application",
|
|
14
|
+
"run_worker_first": [
|
|
15
|
+
"/api/*",
|
|
16
|
+
"/",
|
|
17
|
+
"/login"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"durable_objects": {
|
|
21
|
+
"bindings": [
|
|
22
|
+
{
|
|
23
|
+
"name": "DSH_EDGE_INSTANCE",
|
|
24
|
+
"class_name": "DshEdgeInstance"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"migrations": [
|
|
29
|
+
{
|
|
30
|
+
"tag": "v1",
|
|
31
|
+
"new_sqlite_classes": [
|
|
32
|
+
"DshEdgeInstance"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"observability": {
|
|
37
|
+
"enabled": true
|
|
38
|
+
},
|
|
39
|
+
"env": {
|
|
40
|
+
"isolated": {
|
|
41
|
+
"durable_objects": {
|
|
42
|
+
"bindings": [
|
|
43
|
+
{
|
|
44
|
+
"name": "DSH_EDGE_INSTANCE",
|
|
45
|
+
"class_name": "DshEdgeInstance"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"worker_loaders": [
|
|
50
|
+
{
|
|
51
|
+
"binding": "LOADER"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|