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.
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/README.zh.md +137 -0
- package/assets/dsh-ptc-plus-banner.webp +0 -0
- package/assets/ptc-plus-repair.png +0 -0
- package/assets/ptc-plus-settings.png +0 -0
- package/client.js +366 -0
- package/cordis.patch.yml +5 -0
- package/docs/README.md +21 -0
- package/docs/adr/0001-delegate-governance-to-dsh.md +3 -0
- package/docs/adr/0002-limit-work-map-scope.md +3 -0
- package/docs/adr/0003-prefer-native-program-surfaces.md +3 -0
- package/docs/adr/0004-declare-program-bindings-at-the-host.md +5 -0
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +129 -0
- package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +45 -0
- package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +39 -0
- package/docs/adr/0008-define-agent-operating-contract.md +25 -0
- package/docs/adr/0009-bound-expensive-acceptance-default.md +39 -0
- package/docs/adr/0010-session-log-derived-recovery-tips.md +26 -0
- package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +20 -0
- package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +23 -0
- package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +21 -0
- package/docs/adr/0014-persist-cell-rewrite-policy.md +25 -0
- package/docs/adr/0015-preserve-source-positions-through-rewrites.md +25 -0
- package/docs/adr/0016-archive-resolved-review-findings.md +43 -0
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +23 -0
- package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +25 -0
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +32 -0
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +36 -0
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +68 -0
- package/docs/adr/README.md +18 -0
- package/docs/architecture.md +134 -0
- package/docs/capability-projection.md +48 -0
- package/docs/client-ui.md +47 -0
- package/docs/durability-design.md +265 -0
- package/docs/evaluation.md +62 -0
- package/docs/installation.md +82 -0
- package/docs/program-data-plane.md +61 -0
- package/docs/publishing.md +37 -0
- package/docs/runtime-reference.md +119 -0
- package/docs/value-wire.md +61 -0
- package/index.js +532 -0
- package/internal/ast-traversal.js +20 -0
- package/internal/binding-descriptors.js +95 -0
- package/internal/binding-pattern.js +54 -0
- package/internal/builtin-esm-sync.js +9 -0
- package/internal/cell-analysis.js +467 -0
- package/internal/cell-rewriter.js +520 -0
- package/internal/config-spec.js +179 -0
- package/internal/cordis-tools-owner.js +363 -0
- package/internal/diagnostic.js +133 -0
- package/internal/direct-surface-owner.js +326 -0
- package/internal/edit-transport-owner.js +287 -0
- package/internal/failure-reporting.js +139 -0
- package/internal/kernel-worker.js +484 -0
- package/internal/module-policy.js +46 -0
- package/internal/program-bindings.js +187 -0
- package/internal/record-utils.js +53 -0
- package/internal/recovery-tips.js +81 -0
- package/internal/rejected-cell-editor.js +328 -0
- package/internal/repl-convenience.js +201 -0
- package/internal/runtime-bridge-owner.js +417 -0
- package/internal/runtime-config.js +77 -0
- package/internal/runtime-contexts.js +31 -0
- package/internal/sdk-projection.js +88 -0
- package/internal/session-cell-executor.js +612 -0
- package/internal/session-journal.js +889 -0
- package/internal/session-log-view.js +80 -0
- package/internal/session-runtime.js +536 -0
- package/internal/session-state.js +45 -0
- package/internal/source-position-map.js +224 -0
- package/internal/tool-call-canonicalizer.js +339 -0
- package/internal/value-wire.js +421 -0
- package/internal/worker-client.js +218 -0
- package/internal/worker-cwd-virtualization.js +155 -0
- package/package.json +100 -0
- 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
|
+
}
|