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.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +183 -0
  4. package/README.zh.md +183 -0
  5. package/THIRD_PARTY_NOTICES.md +199 -0
  6. package/dist/_headers +3 -0
  7. package/dist/assets/fonts/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  8. package/dist/assets/fonts/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  9. package/dist/assets/fonts/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  10. package/dist/assets/fonts/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  11. package/dist/assets/fonts/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  12. package/dist/assets/fonts/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  13. package/dist/assets/fonts/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  14. package/dist/assets/fonts/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  15. package/dist/assets/fonts/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  16. package/dist/assets/fonts/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  17. package/dist/assets/fonts/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  18. package/dist/assets/fonts/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  19. package/dist/assets/fonts/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  20. package/dist/assets/fonts/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  21. package/dist/assets/fonts/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  22. package/dist/assets/fonts/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  23. package/dist/assets/fonts/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  24. package/dist/assets/fonts/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  25. package/dist/assets/fonts/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  26. package/dist/assets/fonts/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  27. package/dist/assets/fonts/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  28. package/dist/assets/fonts/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  29. package/dist/assets/fonts/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  30. package/dist/assets/fonts/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  31. package/dist/assets/fonts/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  32. package/dist/assets/fonts/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  33. package/dist/assets/fonts/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  34. package/dist/assets/fonts/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  35. package/dist/assets/fonts/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  36. package/dist/assets/fonts/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  37. package/dist/assets/fonts/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  38. package/dist/assets/fonts/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  39. package/dist/assets/fonts/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  40. package/dist/assets/fonts/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  41. package/dist/assets/fonts/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  42. package/dist/assets/fonts/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  43. package/dist/assets/fonts/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  44. package/dist/assets/fonts/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  45. package/dist/assets/fonts/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  46. package/dist/assets/fonts/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  47. package/dist/assets/fonts/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  48. package/dist/assets/fonts/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  49. package/dist/assets/fonts/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  50. package/dist/assets/fonts/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  51. package/dist/assets/fonts/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  52. package/dist/assets/fonts/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  53. package/dist/assets/fonts/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  54. package/dist/assets/fonts/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  55. package/dist/assets/fonts/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  56. package/dist/assets/fonts/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  57. package/dist/assets/fonts/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  58. package/dist/assets/fonts/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  59. package/dist/assets/fonts/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  60. package/dist/assets/fonts/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  61. package/dist/assets/fonts/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  62. package/dist/assets/fonts/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  63. package/dist/assets/fonts/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  64. package/dist/assets/fonts/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  65. package/dist/assets/fonts/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  66. package/dist/assets/index-C-1AiF3k.js +112 -0
  67. package/dist/assets/index-CSGf6Qzd.css +1 -0
  68. package/dist/assets/langs/c-BIGW1oBm.js +2 -0
  69. package/dist/assets/langs/cpp-DIPi6g--.js +2 -0
  70. package/dist/assets/langs/csharp-DSvCPggb.js +2 -0
  71. package/dist/assets/langs/css-CLj8gQPS.js +2 -0
  72. package/dist/assets/langs/go-C27-OAKa.js +2 -0
  73. package/dist/assets/langs/html-CfGypltT.js +2 -0
  74. package/dist/assets/langs/ini-BEwlwnbL.js +2 -0
  75. package/dist/assets/langs/java-CylS5w8V.js +2 -0
  76. package/dist/assets/langs/kotlin-BdnUsdx6.js +2 -0
  77. package/dist/assets/langs/less-B1dDrJ26.js +2 -0
  78. package/dist/assets/langs/lua-BaeVxFsk.js +2 -0
  79. package/dist/assets/langs/markdown-Cvjx9yec.js +2 -0
  80. package/dist/assets/langs/mdx-Cmh6b_Ma.js +2 -0
  81. package/dist/assets/langs/php-D-uCvoST.js +2 -0
  82. package/dist/assets/langs/python-B6aJPvgy.js +2 -0
  83. package/dist/assets/langs/ruby-DEItuUFs.js +2 -0
  84. package/dist/assets/langs/rust-B1yitclQ.js +2 -0
  85. package/dist/assets/langs/scss-D5BDwBP9.js +2 -0
  86. package/dist/assets/langs/sql-CRqJ_cUM.js +2 -0
  87. package/dist/assets/langs/swift-D82vCrfD.js +2 -0
  88. package/dist/assets/langs/toml-vGWfd6FD.js +2 -0
  89. package/dist/assets/langs/xml-sdJ4AIDG.js +2 -0
  90. package/dist/assets/langs/yaml-Buea-lGh.js +2 -0
  91. package/dist/assets/vendor-Cjbwl5VI.js +411 -0
  92. package/dist/assets/vendor-CjyC-hUb.css +1 -0
  93. package/dist/favicon.svg +8 -0
  94. package/dist/index.html +35 -0
  95. package/dist/manifest.webmanifest +16 -0
  96. package/dist/plugins/@deepseek-ai/dsh-api-gateway/client.js +420 -0
  97. package/dist/plugins/@deepseek-ai/dsh-api-remotes/client.js +5937 -0
  98. package/dist/plugins/@deepseek-ai/dsh-client-connection/client.js +10207 -0
  99. package/dist/plugins/@deepseek-ai/dsh-client-locale/client.js +1231 -0
  100. package/dist/plugins/@deepseek-ai/dsh-client-modules/client.js +259 -0
  101. package/dist/plugins/@deepseek-ai/dsh-client-runtime/client.js +10539 -0
  102. package/dist/plugins/@deepseek-ai/dsh-client-ui-agent-preset/client.js +1725 -0
  103. package/dist/plugins/@deepseek-ai/dsh-client-ui-commands/client.js +1116 -0
  104. package/dist/plugins/@deepseek-ai/dsh-client-ui-conversation/client.js +9832 -0
  105. package/dist/plugins/@deepseek-ai/dsh-client-ui-deliverables/client.js +373 -0
  106. package/dist/plugins/@deepseek-ai/dsh-client-ui-edge/client.js +549 -0
  107. package/dist/plugins/@deepseek-ai/dsh-client-ui-input-trigger/client.js +859 -0
  108. package/dist/plugins/@deepseek-ai/dsh-client-ui-jobs/client.js +280 -0
  109. package/dist/plugins/@deepseek-ai/dsh-client-ui-layout/client.js +456 -0
  110. package/dist/plugins/@deepseek-ai/dsh-client-ui-model-selection/client.js +804 -0
  111. package/dist/plugins/@deepseek-ai/dsh-client-ui-permission-presets/client.js +461 -0
  112. package/dist/plugins/@deepseek-ai/dsh-client-ui-settings/client.js +254 -0
  113. package/dist/plugins/@deepseek-ai/dsh-client-ui-settings-general/client.js +609 -0
  114. package/dist/plugins/@deepseek-ai/dsh-client-ui-sidebar/client.js +292 -0
  115. package/dist/plugins/@deepseek-ai/dsh-client-ui-skill/client.js +327 -0
  116. package/dist/plugins/@deepseek-ai/dsh-client-ui-subagent/client.js +710 -0
  117. package/dist/plugins/@deepseek-ai/dsh-client-ui-theme/client.js +1312 -0
  118. package/dist/plugins/@deepseek-ai/dsh-client-ui-tool/client.js +1630 -0
  119. package/dist/plugins/@deepseek-ai/dsh-client-ui-trajectory/client.js +7370 -0
  120. package/dist/plugins/@deepseek-ai/dsh-client-ui-user-questions/client.js +700 -0
  121. package/dist/plugins/@deepseek-ai/dsh-client-ui-workflow-run/client.js +474 -0
  122. package/dist/plugins/@deepseek-ai/dsh-client-ui-workspace/client.js +2426 -0
  123. package/dist/plugins/@deepseek-ai/dsh-typert-registry/client.js +1372 -0
  124. package/package.json +97 -0
  125. package/scripts/assemble-web.mjs +230 -0
  126. package/scripts/bundle-size.d.mts +2 -0
  127. package/scripts/bundle-size.mjs +26 -0
  128. package/scripts/bundle.mjs +53 -0
  129. package/scripts/cli.d.mts +39 -0
  130. package/scripts/cli.mjs +496 -0
  131. package/scripts/install.d.mts +149 -0
  132. package/scripts/install.mjs +989 -0
  133. package/scripts/legal-files.mjs +24 -0
  134. package/scripts/verify-packed.mjs +32 -0
  135. package/scripts/wrangler-config.d.mts +18 -0
  136. package/scripts/wrangler-config.mjs +84 -0
  137. package/src/agent.ts +119 -0
  138. package/src/auth.ts +352 -0
  139. package/src/deepseek.ts +231 -0
  140. package/src/deployment.ts +147 -0
  141. package/src/direct-shell-core-empty.ts +10 -0
  142. package/src/direct-shell-protocol.ts +5 -0
  143. package/src/direct-shell.ts +341 -0
  144. package/src/do-session-persistence.ts +1064 -0
  145. package/src/edge-api.ts +796 -0
  146. package/src/edge-credentials.ts +56 -0
  147. package/src/edge-remotes.ts +41 -0
  148. package/src/edge-workspace-store.ts +339 -0
  149. package/src/http.ts +212 -0
  150. package/src/index.ts +196 -0
  151. package/src/instance.ts +1007 -0
  152. package/src/isolated-direct-shell-unavailable.ts +11 -0
  153. package/src/protocol.ts +39 -0
  154. package/src/release.ts +9 -0
  155. package/src/session-store.ts +1110 -0
  156. package/src/sse.ts +86 -0
  157. package/src/web-search.ts +38 -0
  158. package/src/workspace.ts +270 -0
  159. 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
+ }
@@ -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
+ }