dsh-ptc-plus 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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/README.zh.md +137 -0
  4. package/assets/dsh-ptc-plus-banner.webp +0 -0
  5. package/assets/ptc-plus-repair.png +0 -0
  6. package/assets/ptc-plus-settings.png +0 -0
  7. package/client.js +366 -0
  8. package/cordis.patch.yml +5 -0
  9. package/docs/README.md +21 -0
  10. package/docs/adr/0001-delegate-governance-to-dsh.md +3 -0
  11. package/docs/adr/0002-limit-work-map-scope.md +3 -0
  12. package/docs/adr/0003-prefer-native-program-surfaces.md +3 -0
  13. package/docs/adr/0004-declare-program-bindings-at-the-host.md +5 -0
  14. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +129 -0
  15. package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +45 -0
  16. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +39 -0
  17. package/docs/adr/0008-define-agent-operating-contract.md +25 -0
  18. package/docs/adr/0009-bound-expensive-acceptance-default.md +39 -0
  19. package/docs/adr/0010-session-log-derived-recovery-tips.md +26 -0
  20. package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +20 -0
  21. package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +23 -0
  22. package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +21 -0
  23. package/docs/adr/0014-persist-cell-rewrite-policy.md +25 -0
  24. package/docs/adr/0015-preserve-source-positions-through-rewrites.md +25 -0
  25. package/docs/adr/0016-archive-resolved-review-findings.md +43 -0
  26. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +23 -0
  27. package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +25 -0
  28. package/docs/adr/0019-plugin-settings-and-kill-switch.md +32 -0
  29. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +36 -0
  30. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +68 -0
  31. package/docs/adr/README.md +18 -0
  32. package/docs/architecture.md +134 -0
  33. package/docs/capability-projection.md +48 -0
  34. package/docs/client-ui.md +47 -0
  35. package/docs/durability-design.md +265 -0
  36. package/docs/evaluation.md +62 -0
  37. package/docs/installation.md +82 -0
  38. package/docs/program-data-plane.md +61 -0
  39. package/docs/publishing.md +37 -0
  40. package/docs/runtime-reference.md +119 -0
  41. package/docs/value-wire.md +61 -0
  42. package/index.js +532 -0
  43. package/internal/ast-traversal.js +20 -0
  44. package/internal/binding-descriptors.js +95 -0
  45. package/internal/binding-pattern.js +54 -0
  46. package/internal/builtin-esm-sync.js +9 -0
  47. package/internal/cell-analysis.js +467 -0
  48. package/internal/cell-rewriter.js +520 -0
  49. package/internal/config-spec.js +179 -0
  50. package/internal/cordis-tools-owner.js +363 -0
  51. package/internal/diagnostic.js +133 -0
  52. package/internal/direct-surface-owner.js +326 -0
  53. package/internal/edit-transport-owner.js +287 -0
  54. package/internal/failure-reporting.js +139 -0
  55. package/internal/kernel-worker.js +484 -0
  56. package/internal/module-policy.js +46 -0
  57. package/internal/program-bindings.js +187 -0
  58. package/internal/record-utils.js +53 -0
  59. package/internal/recovery-tips.js +81 -0
  60. package/internal/rejected-cell-editor.js +328 -0
  61. package/internal/repl-convenience.js +201 -0
  62. package/internal/runtime-bridge-owner.js +417 -0
  63. package/internal/runtime-config.js +77 -0
  64. package/internal/runtime-contexts.js +31 -0
  65. package/internal/sdk-projection.js +88 -0
  66. package/internal/session-cell-executor.js +612 -0
  67. package/internal/session-journal.js +889 -0
  68. package/internal/session-log-view.js +80 -0
  69. package/internal/session-runtime.js +536 -0
  70. package/internal/session-state.js +45 -0
  71. package/internal/source-position-map.js +224 -0
  72. package/internal/tool-call-canonicalizer.js +339 -0
  73. package/internal/value-wire.js +421 -0
  74. package/internal/worker-client.js +218 -0
  75. package/internal/worker-cwd-virtualization.js +155 -0
  76. package/package.json +100 -0
  77. package/src/client.js +165 -0
@@ -0,0 +1,139 @@
1
+ import { diagnostic } from './diagnostic.js'
2
+
3
+ export const MAX_ERROR_LOG_BYTES = 4 * 1024
4
+ export const FAILURE_HINT_THRESHOLD = 3
5
+ const BINDING_FAILURE = Symbol('binding failure')
6
+
7
+ export function markBindingFailure(error) {
8
+ Object.defineProperty(error, BINDING_FAILURE, { value: true })
9
+ return error
10
+ }
11
+
12
+ function repeatedFailureDiagnostic(kind, streak) {
13
+ if (kind === 'binding') {
14
+ return diagnostic({
15
+ code: 'PTC-W001',
16
+ severity: 'warning',
17
+ phase: 'execute',
18
+ message: `this cell failed ${streak} times with the same binding error; inspect the live binding names and change the expression before retrying`,
19
+ stateEffect: 'unchanged',
20
+ help: [
21
+ 'inspect live bindings with capabilities.tree(), capabilities.find(), or capabilities.inspect()',
22
+ 'call available typed members through tools.*',
23
+ 'do not repeat the same unresolved binding expression',
24
+ ],
25
+ })
26
+ }
27
+ return diagnostic({
28
+ code: 'PTC-W002',
29
+ severity: 'warning',
30
+ phase: 'execute',
31
+ message: `this cell failed ${streak} times with the same error; inspect the reported cause and change the approach before retrying`,
32
+ stateEffect: 'unchanged',
33
+ help: ['inspect the reported cause', 'change the inputs or approach before retrying'],
34
+ })
35
+ }
36
+
37
+ export function safeProperty(value, key) {
38
+ if ((typeof value !== 'object' || value === null) && typeof value !== 'function') return undefined
39
+ try {
40
+ return value[key]
41
+ } catch {
42
+ return undefined
43
+ }
44
+ }
45
+
46
+ export function messageOf(error, fallback = 'Unprintable error') {
47
+ const message = safeProperty(error, 'message')
48
+ if (typeof message === 'string') return message
49
+ try {
50
+ return String(error)
51
+ } catch {
52
+ return fallback
53
+ }
54
+ }
55
+
56
+ export function firstLine(value, fallback = undefined) {
57
+ if (typeof value !== 'string') return fallback
58
+ const line = value.split(/[\r\n]/, 1)[0]
59
+ return line.length > 0 ? line : fallback
60
+ }
61
+
62
+ export function oneLineMessage(error) {
63
+ return firstLine(messageOf(error), 'Unknown error').replace(/\s+\(\d+:\d+\)$/, '')
64
+ }
65
+
66
+ /** Keep only the newest log entries so an output-limit error cannot flood the model. */
67
+ export function limitLogs(logs) {
68
+ let bytes = 0
69
+ const tail = []
70
+ for (let index = logs.length - 1; index >= 0; index -= 1) {
71
+ const size = Buffer.byteLength(logs[index], 'utf8') + 1
72
+ if (bytes + size > MAX_ERROR_LOG_BYTES) break
73
+ bytes += size
74
+ tail.unshift(logs[index])
75
+ }
76
+ return tail
77
+ }
78
+
79
+ export function errorPosition(error, filename) {
80
+ const stack = safeProperty(error, 'stack')
81
+ if (typeof stack !== 'string') return undefined
82
+ const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
83
+ const match = new RegExp(`${escaped}:(\\d+):(\\d+)`).exec(stack)
84
+ if (match === null) return undefined
85
+ const line = Number(match[1])
86
+ const column = Number(match[2])
87
+ return line >= 1 && column >= 1 ? { line, column } : undefined
88
+ }
89
+
90
+ export function errorDetails(error, filename) {
91
+ const message = messageOf(error, 'Unprintable thrown value')
92
+ const rawName = safeProperty(error, 'name')
93
+ const name = typeof rawName === 'string' && rawName.length > 0 ? rawName : 'Error'
94
+ const rawToolName = safeProperty(error, 'toolName')
95
+ const toolName = name === 'ToolCallError'
96
+ ? firstLine(rawToolName)
97
+ : undefined
98
+ const candidate = safeProperty(error, 'ptcCause')
99
+ const causeMessage = firstLine(safeProperty(candidate, 'message'))
100
+ const causeCode = firstLine(safeProperty(candidate, 'code'))
101
+ const cause = causeMessage === undefined
102
+ ? undefined
103
+ : {
104
+ ...(causeCode === undefined ? {} : { code: causeCode }),
105
+ message: causeMessage,
106
+ }
107
+ const position = errorPosition(error, filename)
108
+ return {
109
+ name,
110
+ message,
111
+ ...(toolName === undefined ? {} : { toolName }),
112
+ ...(position === undefined ? {} : { position }),
113
+ ...(cause === undefined ? {} : { cause }),
114
+ }
115
+ }
116
+
117
+ export function createFailureTracker() {
118
+ let fingerprint
119
+ let kind
120
+ let streak = 0
121
+ return Object.freeze({
122
+ reset() {
123
+ fingerprint = undefined
124
+ kind = undefined
125
+ streak = 0
126
+ },
127
+ hint(error) {
128
+ const nextFingerprint = `${error.kind}\u0000${error.message}`
129
+ if (fingerprint === nextFingerprint) streak += 1
130
+ else {
131
+ fingerprint = nextFingerprint
132
+ kind = error[BINDING_FAILURE] === true ? 'binding' : 'generic'
133
+ streak = 1
134
+ }
135
+ if (streak !== FAILURE_HINT_THRESHOLD) return undefined
136
+ return repeatedFailureDiagnostic(kind, streak)
137
+ },
138
+ })
139
+ }
@@ -0,0 +1,484 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { registerHooks } from 'node:module'
3
+ import { isAbsolute, resolve } from 'node:path'
4
+ import repl from 'node:repl'
5
+ import { PassThrough } from 'node:stream'
6
+ import { pathToFileURL } from 'node:url'
7
+ import { formatWithOptions } from 'node:util'
8
+ import { MessageChannel, parentPort, workerData } from 'node:worker_threads'
9
+ import { synchronizeBuiltinEsmExports } from './builtin-esm-sync.js'
10
+ import { errorDetails, messageOf } from './failure-reporting.js'
11
+ import { AMBIENT_GLOBALS, DURABLE_IMPORTS, FORBIDDEN_IMPORTS } from './module-policy.js'
12
+ import { decodeValue, encodeValue } from './value-wire.js'
13
+ import { installWorkerCwdVirtualization } from './worker-cwd-virtualization.js'
14
+
15
+ if (parentPort === null) throw new Error('ptc-plus kernel worker started without a parent port')
16
+ const { port1, port2: channel } = new MessageChannel()
17
+
18
+ const input = new PassThrough()
19
+ const output = new PassThrough()
20
+ output.resume()
21
+ const sessionCwd = typeof workerData?.cwd === 'string' ? workerData.cwd : undefined
22
+ if (sessionCwd !== undefined && !isAbsolute(sessionCwd)) {
23
+ throw new Error(`ptc-plus session cwd must be absolute, got ${JSON.stringify(sessionCwd)}`)
24
+ }
25
+ const server = repl.start({
26
+ input,
27
+ output,
28
+ terminal: false,
29
+ prompt: '',
30
+ useGlobal: false,
31
+ ignoreUndefined: true,
32
+ })
33
+ const context = server.context
34
+ const REPL_IMPORT_CANARY = 'data:text/javascript,export default 1'
35
+ let replParent
36
+ const sessionReplParent = sessionCwd === undefined ? undefined : pathToFileURL(resolve(sessionCwd, 'repl')).href
37
+ const staticAdapterParents = new Set()
38
+ registerHooks({
39
+ resolve(specifier, context, nextResolve) {
40
+ if (specifier === REPL_IMPORT_CANARY && replParent === undefined) replParent = context.parentURL
41
+ return nextResolve(specifier, context.parentURL === replParent || staticAdapterParents.has(context.parentURL)
42
+ ? { ...context, parentURL: sessionReplParent ?? replParent }
43
+ : context)
44
+ },
45
+ })
46
+ const logScope = new AsyncLocalStorage()
47
+ const pending = new Map()
48
+ const installedGlobals = new Set()
49
+ const PROCESS_CONTROLS = new Set(['exit', 'abort', 'kill', 'chdir'])
50
+ const CELL_FRAME_SUFFIX = '\n;'
51
+ let filenameSequence = 0
52
+ let activeFilename = 'ptc-plus-repl'
53
+ const CONFORMANCE_CELL = `"use strict";
54
+ {
55
+ if (this !== globalThis) throw new Error('invalid REPL global receiver semantics')
56
+ const __ptc_canary = await Promise.resolve(1)
57
+ if (__ptc_canary !== 1) throw new Error('invalid REPL await semantics')
58
+ const __ptc_import_canary = await import(${JSON.stringify(REPL_IMPORT_CANARY)})
59
+ if (__ptc_import_canary.default !== 1) throw new Error('invalid REPL import semantics')
60
+ }`
61
+ let activeRun
62
+ let activeExecution
63
+ let pendingVolatileReason
64
+ let nextCallId = 0
65
+ let nextStaticAdapterId = 0
66
+
67
+ class StaticImportFailure {
68
+ constructor(error, position) {
69
+ this.error = error
70
+ this.position = position
71
+ }
72
+ }
73
+
74
+ class CellReturn extends Error {
75
+ constructor(value) {
76
+ super('cell returned')
77
+ this.value = value
78
+ }
79
+ }
80
+ function appendLog(...values) {
81
+ const current = logScope.getStore()
82
+ if (current?.open !== true) return
83
+ appendText(current, formatWithOptions({ colors: false, depth: 4, maxArrayLength: 100, maxStringLength: 10_000 }, ...values))
84
+ }
85
+
86
+ function appendText(current, text) {
87
+ if (current.open !== true || current.outputLimited) return
88
+ const bytes = Buffer.byteLength(JSON.stringify(text), 'utf8') + (current.logs.length === 0 ? 0 : 1)
89
+ if (current.logBytes + bytes > current.maxOutputBytes) {
90
+ current.outputLimited = true
91
+ channel.postMessage({ type: 'output-limit', id: current.id, logs: current.logs })
92
+ return
93
+ }
94
+ current.logBytes += bytes
95
+ current.logs.push(text)
96
+ }
97
+
98
+ const consoleView = Object.freeze({
99
+ log: appendLog,
100
+ info: appendLog,
101
+ warn: appendLog,
102
+ error: appendLog,
103
+ debug: appendLog,
104
+ dir: value => appendLog(value),
105
+ })
106
+ Object.defineProperty(context, 'console', { configurable: true, value: consoleView })
107
+
108
+ function captureWrite(chunk, ...rest) {
109
+ const current = logScope.getStore()
110
+ if (current?.open === true) appendText(current, typeof chunk === 'string' ? chunk : String(chunk))
111
+ const callback = [rest[0], rest[1]].find(value => typeof value === 'function')
112
+ if (callback !== undefined) queueMicrotask(() => callback(null))
113
+ return true
114
+ }
115
+ process.stdout.write = captureWrite
116
+ process.stderr.write = captureWrite
117
+
118
+ function markVolatile(reason) {
119
+ const current = activeExecution
120
+ if (current === undefined) {
121
+ pendingVolatileReason ??= reason
122
+ return
123
+ }
124
+ if (current.durability === 'volatile') return
125
+ current.durability = 'volatile'
126
+ current.volatileReason ??= reason
127
+ channel.postMessage({ type: 'volatile', id: current.id, reason: current.volatileReason })
128
+ }
129
+
130
+ function completionDurability(execution) {
131
+ return {
132
+ durability: execution.durability,
133
+ ...(execution.volatileReason === undefined ? {} : { volatileReason: execution.volatileReason }),
134
+ }
135
+ }
136
+
137
+ const originalRequire = context.require
138
+
139
+ function guardProcessControls() {
140
+ for (const property of PROCESS_CONTROLS) {
141
+ const descriptor = Object.getOwnPropertyDescriptor(process, property)
142
+ Object.defineProperty(process, property, {
143
+ configurable: false,
144
+ enumerable: descriptor?.enumerable ?? true,
145
+ writable: false,
146
+ value: () => {
147
+ throw new Error(`process.${property} is forbidden inside the REPL kernel`)
148
+ },
149
+ })
150
+ }
151
+ }
152
+
153
+ guardProcessControls()
154
+ installWorkerCwdVirtualization(sessionCwd, originalRequire)
155
+ synchronizeBuiltinEsmExports()
156
+ const originalGlobals = Object.fromEntries(
157
+ [...AMBIENT_GLOBALS].filter(name => name !== 'require')
158
+ .map(name => [name, globalThis[name]]),
159
+ )
160
+ Object.defineProperty(context, 'require', {
161
+ configurable: true,
162
+ value(specifier) {
163
+ if (FORBIDDEN_IMPORTS.has(specifier)) throw new Error(`module ${specifier} is forbidden because it exposes kernel control`)
164
+ if (!DURABLE_IMPORTS.has(specifier)) markVolatile(`require(${JSON.stringify(specifier)})`)
165
+ return originalRequire(specifier)
166
+ },
167
+ })
168
+
169
+ for (const [name, value] of Object.entries(originalGlobals)) {
170
+ Object.defineProperty(context, name, {
171
+ configurable: true,
172
+ get() {
173
+ markVolatile(`ambient ${name}`)
174
+ return value
175
+ },
176
+ set(next) {
177
+ markVolatile(`ambient ${name}`)
178
+ Object.defineProperty(context, name, { configurable: true, writable: true, value: next })
179
+ },
180
+ })
181
+ }
182
+
183
+ const capturedOutput = Object.freeze({ write: captureWrite })
184
+ const processView = new Proxy(process, {
185
+ get(target, property) {
186
+ if (property === 'stdout' || property === 'stderr') return capturedOutput
187
+ if (property === 'cwd') {
188
+ if (sessionCwd !== undefined) return () => sessionCwd
189
+ markVolatile('process.cwd')
190
+ return target.cwd.bind(target)
191
+ }
192
+ if (PROCESS_CONTROLS.has(property)) return Reflect.get(target, property, target)
193
+ markVolatile(`process.${String(property)}`)
194
+ const value = Reflect.get(target, property, target)
195
+ return typeof value === 'function' ? value.bind(target) : value
196
+ },
197
+ set(target, property, value) {
198
+ if (property === 'stdout' || property === 'stderr') return false
199
+ markVolatile(`process.${String(property)}`)
200
+ return Reflect.set(target, property, value, target)
201
+ },
202
+ ownKeys(target) {
203
+ markVolatile('process reflection')
204
+ return Reflect.ownKeys(target)
205
+ },
206
+ })
207
+ Object.defineProperty(context, 'process', { configurable: true, value: processView })
208
+
209
+ const mathDescriptors = Object.getOwnPropertyDescriptors(Math)
210
+ mathDescriptors.random = {
211
+ ...mathDescriptors.random,
212
+ value: () => {
213
+ markVolatile('Math.random')
214
+ return Math.random()
215
+ },
216
+ }
217
+ const mathView = Object.defineProperties(Object.create(Object.getPrototypeOf(Math)), mathDescriptors)
218
+ Object.defineProperty(context, 'Math', {
219
+ configurable: true,
220
+ value: Object.freeze(mathView),
221
+ })
222
+
223
+ function evaluate(program) {
224
+ return new Promise((resolve, reject) => {
225
+ // REPLServer routes runtime throws to its domain instead of the eval
226
+ // callback. Cells are serialized, so temporarily replacing that one error
227
+ // handler gives both syntax and runtime failures one settlement path.
228
+ const domain = server._domain
229
+ const prior = domain.listeners('error')
230
+ domain.removeAllListeners('error')
231
+ let settled = false
232
+ const finish = (error, value) => {
233
+ if (settled) return
234
+ settled = true
235
+ domain.removeListener('error', onError)
236
+ for (const listener of prior) domain.on('error', listener)
237
+ if (error instanceof CellReturn) {
238
+ Promise.resolve(error.value).then(
239
+ value => resolve({ hasValue: true, value }),
240
+ reject,
241
+ )
242
+ } else if (error) reject(error)
243
+ else {
244
+ Promise.resolve(value).then(
245
+ value => resolve({ hasValue: value !== undefined, value }),
246
+ reject,
247
+ )
248
+ }
249
+ }
250
+ const onError = error => finish(error)
251
+ domain.on('error', onError)
252
+ activeFilename = `ptc-plus-repl-${++filenameSequence}`
253
+ server.eval(program + CELL_FRAME_SUFFIX, context, activeFilename, finish)
254
+ })
255
+ }
256
+
257
+ function staticImportAttributes(options) {
258
+ if (options === undefined) return ''
259
+ const [keyword, attributes] = Object.entries(options)[0]
260
+ const entries = Object.entries(attributes)
261
+ .map(([key, value]) => `${JSON.stringify(key)}: ${JSON.stringify(value)}`)
262
+ return ` ${keyword} { ${entries.join(', ')} }`
263
+ }
264
+
265
+ function staticAdapterSource(load) {
266
+ const source = JSON.stringify(load.source)
267
+ const attributes = staticImportAttributes(load.options)
268
+ if (load.global === undefined) return `import ${source}${attributes};`
269
+ const requirements = load.requiredExports?.map((name, index) => {
270
+ const imported = name === 'default' ? 'default' : JSON.stringify(name)
271
+ return `${imported} as __required_${index}__`
272
+ }) ?? []
273
+ return [
274
+ `import * as namespace from ${source}${attributes};`,
275
+ ...(requirements.length === 0 ? [] : [
276
+ `export { ${requirements.join(', ')} } from ${source}${attributes};`,
277
+ ]),
278
+ 'export { namespace };',
279
+ ].join('\n')
280
+ }
281
+
282
+ async function loadStaticModule(load) {
283
+ const adapter = `data:text/javascript,${encodeURIComponent(staticAdapterSource(load))}#${++nextStaticAdapterId}`
284
+ staticAdapterParents.add(adapter)
285
+ try {
286
+ const completion = await evaluate(`import(${JSON.stringify(adapter)})`)
287
+ return completion.value.namespace
288
+ } finally {
289
+ staticAdapterParents.delete(adapter)
290
+ }
291
+ }
292
+
293
+ function callHost(runId, global, member, args, errorClass) {
294
+ if (activeRun !== runId) return Promise.reject(new Error('PTC execution lease expired'))
295
+ const id = ++nextCallId
296
+ let settle
297
+ const result = new Promise((resolve, reject) => { settle = { resolve, reject, errorClass, member } })
298
+ void result.catch(() => {})
299
+ pending.set(id, { ...settle, runId })
300
+ try {
301
+ channel.postMessage({
302
+ type: 'call', runId, id, global, member,
303
+ args: encodeValue(args, activeExecution?.valueLimits),
304
+ })
305
+ } catch (error) {
306
+ pending.delete(id)
307
+ settle.reject(error)
308
+ }
309
+ return result
310
+ }
311
+
312
+ function installBindings(message) {
313
+ for (const name of installedGlobals) delete context[name]
314
+ installedGlobals.clear()
315
+
316
+ for (const namespace of message.namespaces) {
317
+ const view = Object.create(null)
318
+ const emptyObjectMembers = new Set(namespace.emptyObjectMembers ?? [])
319
+ for (const member of namespace.members) {
320
+ Object.defineProperty(view, member, {
321
+ enumerable: true,
322
+ value: (...args) => callHost(
323
+ message.id,
324
+ namespace.global,
325
+ member,
326
+ args.length === 0 && emptyObjectMembers.has(member) ? {} : args[0],
327
+ namespace.errorClass,
328
+ ),
329
+ })
330
+ }
331
+ Object.freeze(view)
332
+ Object.defineProperty(context, namespace.global, { configurable: true, value: view })
333
+ installedGlobals.add(namespace.global)
334
+
335
+ if (namespace.errorClass !== undefined) {
336
+ const descriptor = namespace.errorClass
337
+ const BoundError = class extends Error {
338
+ constructor(member, detail, cause) {
339
+ super(detail)
340
+ this.name = descriptor.name
341
+ Object.defineProperty(this, descriptor.memberNameProperty, { enumerable: true, value: member })
342
+ if (cause !== undefined) Object.defineProperty(this, 'ptcCause', { value: cause })
343
+ }
344
+ }
345
+ Object.defineProperty(context, descriptor.name, { configurable: true, value: BoundError })
346
+ installedGlobals.add(descriptor.name)
347
+ }
348
+ }
349
+ }
350
+
351
+ async function runCell(message) {
352
+ if (activeRun !== undefined) throw new Error('kernel received overlapping cells')
353
+ activeRun = message.id
354
+ installBindings(message)
355
+ const execution = {
356
+ id: message.id,
357
+ logs: [],
358
+ open: true,
359
+ outputLimited: false,
360
+ logBytes: 2,
361
+ maxOutputBytes: message.maxOutputBytes,
362
+ valueLimits: message.valueLimits,
363
+ durability: message.durability === 'volatile' || pendingVolatileReason !== undefined ? 'volatile' : 'durable',
364
+ volatileReason: pendingVolatileReason,
365
+ }
366
+ pendingVolatileReason = undefined
367
+ activeExecution = execution
368
+ const cellGlobals = []
369
+
370
+ try {
371
+ let completion
372
+ try {
373
+ completion = await logScope.run(execution, async () => {
374
+ Object.defineProperty(context, message.returnSignal, {
375
+ configurable: true,
376
+ value: CellReturn,
377
+ })
378
+ cellGlobals.push(message.returnSignal)
379
+ for (const load of message.moduleLoads ?? []) {
380
+ let namespace
381
+ try {
382
+ namespace = await loadStaticModule(load)
383
+ } catch (error) {
384
+ throw new StaticImportFailure(error, load.position)
385
+ }
386
+ if (load.global !== undefined) {
387
+ Object.defineProperty(context, load.global, {
388
+ configurable: true,
389
+ value: namespace,
390
+ })
391
+ cellGlobals.push(load.global)
392
+ }
393
+ }
394
+ return evaluate(message.program)
395
+ })
396
+ activeRun = undefined
397
+ execution.open = false
398
+ const calls = [...pending.values()]
399
+ .filter(call => call.runId === message.id)
400
+ .map(call => new Promise(resolve => {
401
+ const originalResolve = call.resolve
402
+ const originalReject = call.reject
403
+ call.resolve = value => { originalResolve(value); resolve() }
404
+ call.reject = error => { originalReject(error); resolve() }
405
+ }))
406
+ if (calls.length > 0) await Promise.all(calls)
407
+ } catch (error) {
408
+ activeRun = undefined
409
+ execution.open = false
410
+ const failure = error instanceof StaticImportFailure ? error.error : error
411
+ const detail = errorDetails(failure, activeFilename)
412
+ const position = error instanceof StaticImportFailure ? error.position : detail.position
413
+ channel.postMessage({
414
+ type: 'done',
415
+ id: message.id,
416
+ logs: execution.logs,
417
+ error: detail.message,
418
+ errorName: detail.name,
419
+ ...(detail.toolName === undefined ? {} : { toolName: detail.toolName }),
420
+ ...(error instanceof StaticImportFailure ? { moduleLoadFailed: true } : {}),
421
+ ...(position === undefined ? {} : { position }),
422
+ ...(detail.cause === undefined ? {} : { cause: detail.cause }),
423
+ ...completionDurability(execution),
424
+ })
425
+ return
426
+ }
427
+
428
+ let response
429
+ try {
430
+ const encodedValue = completion.hasValue
431
+ ? encodeValue(completion.value, execution.valueLimits)
432
+ : undefined
433
+ response = {
434
+ type: 'done',
435
+ id: message.id,
436
+ logs: execution.logs,
437
+ hasValue: completion.hasValue,
438
+ ...(encodedValue === undefined ? {} : { value: encodedValue }),
439
+ ...completionDurability(execution),
440
+ }
441
+ } catch (error) {
442
+ const detail = messageOf(error)
443
+ response = {
444
+ type: 'done',
445
+ id: message.id,
446
+ logs: execution.logs,
447
+ invalidOutput: detail,
448
+ ...completionDurability(execution),
449
+ }
450
+ }
451
+ channel.postMessage(response)
452
+ } finally {
453
+ for (const name of cellGlobals) delete context[name]
454
+ activeRun = undefined
455
+ activeExecution = undefined
456
+ execution.open = false
457
+ }
458
+ }
459
+
460
+ channel.on('message', (message) => {
461
+ if (message?.type === 'reply') {
462
+ const call = pending.get(message.id)
463
+ if (call === undefined || call.runId !== message.runId) return
464
+ pending.delete(message.id)
465
+ if (message.ok) call.resolve(decodeValue(message.value, activeExecution?.valueLimits))
466
+ else if (call.errorClass === undefined) {
467
+ const error = new Error(message.error)
468
+ if (message.cause !== undefined) error.ptcCause = message.cause
469
+ call.reject(error)
470
+ } else call.reject(new context[call.errorClass.name](call.member, message.error, message.cause))
471
+ return
472
+ }
473
+ if (message?.type === 'run') void runCell(message)
474
+ })
475
+
476
+ try {
477
+ await evaluate(CONFORMANCE_CELL)
478
+ parentPort.postMessage({ type: 'ready', port: port1 }, [port1])
479
+ } catch (error) {
480
+ parentPort.postMessage({
481
+ type: 'startup-error',
482
+ error: `Node REPL does not satisfy the PTC Plus cell framing contract: ${messageOf(error)}`,
483
+ })
484
+ }
@@ -0,0 +1,46 @@
1
+ export const DURABLE_IMPORTS = new Set([
2
+ 'node:assert',
3
+ 'node:buffer',
4
+ 'node:querystring',
5
+ 'node:string_decoder',
6
+ 'node:stream',
7
+ 'node:util',
8
+ 'node:url',
9
+ 'node:zlib',
10
+ ])
11
+
12
+ export const AMBIENT_GLOBALS = new Set([
13
+ 'Date', 'performance', 'fetch', 'WebSocket', 'crypto', 'Intl',
14
+ 'setTimeout', 'setInterval', 'setImmediate', 'eval', 'Function', 'require',
15
+ ])
16
+
17
+ export const FORBIDDEN_IMPORTS = new Set([
18
+ 'node:worker_threads', 'worker_threads', 'node:cluster', 'cluster',
19
+ ])
20
+
21
+ export const DYNAMIC_MODULE_REASON = Object.freeze({ kind: 'dynamic-module-resolution' })
22
+
23
+ export function classifyModuleSource(source) {
24
+ if (FORBIDDEN_IMPORTS.has(source)) {
25
+ return Object.freeze({ status: 'forbidden', source })
26
+ }
27
+ if (DURABLE_IMPORTS.has(source)) return Object.freeze({ status: 'durable', source })
28
+ return Object.freeze({
29
+ status: 'volatile',
30
+ source,
31
+ reason: Object.freeze({ kind: 'module', source }),
32
+ })
33
+ }
34
+
35
+ export function renderDurabilityReason(reason) {
36
+ if (reason.kind === 'module') return `module ${reason.source}`
37
+ if (reason.kind === 'dynamic-module-resolution') return 'dynamic module resolution'
38
+ if (reason.kind === 'ambient') return `ambient ${reason.name}`
39
+ if (reason.kind === 'math-random') return 'Math.random'
40
+ if (reason.kind === 'computed-global-access') return 'computed global access'
41
+ throw new TypeError(`unknown durability reason ${JSON.stringify(reason?.kind)}`)
42
+ }
43
+
44
+ export function renderDurabilityReasons(reasons) {
45
+ return reasons.map(renderDurabilityReason).join(', ')
46
+ }