dsh-ptc-plus 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +164 -78
- package/README.md +165 -79
- package/assets/dsh-ptc-plus-banner-en.webp +0 -0
- package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
- package/assets/ptc-plus-icon.svg +5 -0
- package/client.js +854 -336
- package/compiler-core.cjs +161 -157
- package/compiler-core.cjs.map +3 -3
- package/docs/README.md +4 -1
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +8 -7
- package/docs/adr/0008-define-agent-operating-contract.md +2 -2
- package/docs/adr/0010-session-log-derived-recovery-tips.md +7 -0
- package/docs/adr/0016-archive-resolved-review-findings.md +3 -3
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +3 -3
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -0
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +14 -3
- package/docs/adr/0023-global-user-bindings.md +7 -7
- package/docs/adr/0025-use-versioned-logical-binding-identities.md +3 -3
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +23 -7
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +9 -5
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +26 -6
- package/docs/adr/0031-close-semantic-obligations-by-owner.md +29 -0
- package/docs/adr/README.md +4 -4
- package/docs/architecture.md +15 -10
- package/docs/binding-review-placement-design.md +1 -1
- package/docs/client-ui.md +9 -5
- package/docs/durability-design.md +12 -9
- package/docs/installation.md +27 -9
- package/docs/runtime-reference.md +23 -12
- package/docs/semantic-validation.md +7 -3
- package/docs/stateful-binding-updates-proposal.md +8 -4
- package/docs/user-bindings.en.md +10 -7
- package/docs/user-bindings.md +9 -6
- package/docs/value-wire.md +1 -1
- package/docs/verification-optimization-work.md +41 -0
- package/docs/verification.md +53 -2
- package/index.js +39 -34
- package/internal/async-local-storage-intrinsics.js +43 -0
- package/internal/binding-declaration-contract.js +8 -0
- package/internal/binding-source-analysis.js +381 -64
- package/internal/binding-update-policy.js +7 -1
- package/internal/callable-source-facts.js +37 -1
- package/internal/cell-analysis.js +85 -17
- package/internal/compiler-intrinsics.js +9 -43
- package/internal/compiler-module-links.js +7 -4
- package/internal/config-spec.js +1 -1
- package/internal/cordis-tools-owner.js +20 -10
- package/internal/dynamic-environment-integration.js +8 -4
- package/internal/dynamic-environment-runtime.js +24 -3
- package/internal/dynamic-native-calls.js +4 -1
- package/internal/dynamic-native-runtime.js +15 -1
- package/internal/edit-transport-owner.js +80 -20
- package/internal/execution-seam-compat.js +192 -30
- package/internal/failure-reporting.js +38 -30
- package/internal/isolated-worker.js +17 -10
- package/internal/kernel-worker.js +645 -393
- package/internal/message-sources.js +14 -0
- package/internal/module-compilation.js +7 -5
- package/internal/module-transform-contract.js +32 -2
- package/internal/record-utils.js +24 -15
- package/internal/repl-memory-projection.js +10 -0
- package/internal/repl-scope-normalizer.js +72 -35
- package/internal/repl-value-observer.js +145 -46
- package/internal/runtime-config.js +62 -2
- package/internal/runtime-intrinsics.js +104 -0
- package/internal/runtime-messages.js +6 -4
- package/internal/session-cell-executor.js +26 -2
- package/internal/session-journal-recovery.js +267 -67
- package/internal/session-journal-schema.js +30 -13
- package/internal/session-journal.js +485 -44
- package/internal/session-log-view.js +5 -4
- package/internal/session-runtime.js +46 -21
- package/internal/session-state.js +7 -29
- package/internal/settings-compat.js +66 -1
- package/internal/source-position-map.js +38 -0
- package/internal/stateful-root-compiler.js +3 -1
- package/internal/typescript-transform.js +4 -2
- package/internal/user-binding-console-worker.js +115 -41
- package/internal/user-binding-draft-projection.js +3 -2
- package/internal/user-bindings-owner.js +2 -1
- package/internal/user-bindings.js +16 -3
- package/internal/validated-parse-repair.js +6 -2
- package/internal/value-wire.js +189 -94
- package/internal/worker-client.js +99 -8
- package/internal/worker-cwd-virtualization.js +82 -54
- package/internal/worker-environment.js +20 -0
- package/internal/worker-output-capture.js +187 -0
- package/internal/worker-output-fence.js +26 -0
- package/internal/worker-realm-surfaces.js +197 -0
- package/internal/worker-repl-realm.js +140 -0
- package/package.json +37 -4
- package/src/client-activity.js +12 -2
- package/src/client-authoring-view.js +121 -62
- package/src/client-console.js +4 -2
- package/src/client-copy.js +4 -2
- package/src/client-feature-gates.js +7 -9
- package/src/client-host-compat.js +104 -27
- package/src/client-repl-view.js +3 -1
- package/src/client-styles.js +12 -1
- package/src/client-tool-view.js +3 -1
- package/src/client-transport.js +2 -2
- package/src/client-workbench.js +4 -3
- package/src/client.js +56 -17
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
+
import { randomBytes } from 'node:crypto'
|
|
2
3
|
import { tmpdir } from 'node:os'
|
|
3
4
|
import { isAbsolute, join } from 'node:path'
|
|
4
5
|
import { messageOf, processExitDescription } from './failure-reporting.js'
|
|
5
6
|
import { IsolatedOwner } from './isolated-worker.js'
|
|
6
7
|
import { normalizeWorkerEnvironment } from './worker-environment.js'
|
|
8
|
+
import { WorkerOutputCapture } from './worker-output-capture.js'
|
|
9
|
+
import { stripOutputFenceMarkers } from './worker-output-fence.js'
|
|
7
10
|
|
|
8
11
|
export { normalizeWorkerEnvironment }
|
|
9
12
|
|
|
@@ -11,16 +14,21 @@ const KERNEL_HELPER = new URL('./kernel-child.js', import.meta.url)
|
|
|
11
14
|
|
|
12
15
|
/** Owns one session kernel's worker process, private port, and scratch directory. */
|
|
13
16
|
export class WorkerClient {
|
|
14
|
-
constructor({ workerUrl, cwd, onMessage, onFailure, compilerCache }) {
|
|
17
|
+
constructor({ workerUrl, cwd, onMessage, onFailure, onUnmatchedOutputFailure, compilerCache }) {
|
|
15
18
|
this.workerUrl = workerUrl
|
|
16
19
|
this.cwd = cwd
|
|
17
20
|
this.onMessage = onMessage
|
|
18
21
|
this.onFailure = onFailure
|
|
22
|
+
this.onUnmatchedOutputFailure = onUnmatchedOutputFailure ?? (() => false)
|
|
19
23
|
this.compilerCache = compilerCache
|
|
20
24
|
this.worker = undefined
|
|
21
25
|
this.workerLimit = undefined
|
|
22
26
|
this.workerReady = undefined
|
|
23
27
|
this.port = undefined
|
|
28
|
+
this.outputCapture = undefined
|
|
29
|
+
this.outputCompletionPending = undefined
|
|
30
|
+
this.deferredOutputMessages = []
|
|
31
|
+
this.pendingOutputFailure = undefined
|
|
24
32
|
this.scratchReady = undefined
|
|
25
33
|
this.stderrTails = new WeakMap()
|
|
26
34
|
// One owner keeps every started kernel transport, including startup failures
|
|
@@ -32,13 +40,18 @@ export class WorkerClient {
|
|
|
32
40
|
|
|
33
41
|
/** Last non-empty stderr line of a failed worker, capped for diagnostics. */
|
|
34
42
|
stderrDetail(worker) {
|
|
35
|
-
const tail = this.stderrTails.get(worker)?.join('').trim()
|
|
43
|
+
const tail = stripOutputFenceMarkers(this.stderrTails.get(worker)?.join('')).trim()
|
|
36
44
|
if (tail === undefined || tail.length === 0) return undefined
|
|
37
45
|
const lastLine = tail.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0).slice(-1)[0]
|
|
38
46
|
return lastLine === undefined ? undefined : lastLine.slice(0, 300)
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
async ensure(maxOldGenerationSizeMb) {
|
|
50
|
+
if (this.pendingOutputFailure !== undefined) {
|
|
51
|
+
const message = this.pendingOutputFailure
|
|
52
|
+
this.pendingOutputFailure = undefined
|
|
53
|
+
throw new Error(`worker output attribution failed: ${message}`)
|
|
54
|
+
}
|
|
42
55
|
if (this.workerLimit !== undefined && this.workerLimit !== maxOldGenerationSizeMb) {
|
|
43
56
|
throw new Error('ptc-plus: session worker memory limit differs from the submitted cell configuration')
|
|
44
57
|
}
|
|
@@ -86,15 +99,21 @@ export class WorkerClient {
|
|
|
86
99
|
this.workerLimit = undefined
|
|
87
100
|
throw error
|
|
88
101
|
}
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
const outputCapture = new WorkerOutputCapture()
|
|
103
|
+
this.outputCapture = outputCapture
|
|
91
104
|
const stderrTail = []
|
|
105
|
+
worker.stdout.on?.('data', chunk => {
|
|
106
|
+
this.handleCapturedOutput(worker, outputCapture.push('stdout', chunk))
|
|
107
|
+
})
|
|
92
108
|
worker.stderr.on?.('data', (chunk) => {
|
|
93
109
|
const text = typeof chunk === 'string' ? chunk : String(chunk ?? '')
|
|
94
110
|
stderrTail.push(text)
|
|
95
111
|
const bounded = Buffer.from(stderrTail.join('')).subarray(-2048).toString('utf8')
|
|
96
112
|
stderrTail.splice(0, stderrTail.length, bounded)
|
|
113
|
+
this.handleCapturedOutput(worker, outputCapture.push('stderr', chunk))
|
|
97
114
|
})
|
|
115
|
+
worker.stdout.resume()
|
|
116
|
+
worker.stderr.resume()
|
|
98
117
|
this.stderrTails.set(worker, stderrTail)
|
|
99
118
|
worker.on('error', error => this.fail(worker, `worker error: ${messageOf(error)}`))
|
|
100
119
|
worker.on('exit', (code, signal) => {
|
|
@@ -135,9 +154,7 @@ export class WorkerClient {
|
|
|
135
154
|
return
|
|
136
155
|
}
|
|
137
156
|
this.port = message.port
|
|
138
|
-
this.port.on('message',
|
|
139
|
-
if (worker === this.worker) this.onMessage(value)
|
|
140
|
-
})
|
|
157
|
+
this.port.on('message', value => this.handleWorkerMessage(worker, outputCapture, value))
|
|
141
158
|
resolve(worker)
|
|
142
159
|
})
|
|
143
160
|
})
|
|
@@ -145,7 +162,17 @@ export class WorkerClient {
|
|
|
145
162
|
}
|
|
146
163
|
|
|
147
164
|
post(message) {
|
|
148
|
-
|
|
165
|
+
if (message?.type !== 'run') {
|
|
166
|
+
this.port.postMessage(message)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
const outputFence = randomBytes(24).toString('hex')
|
|
170
|
+
const failure = this.outputCapture?.begin(message.id, message.maxOutputBytes, outputFence)
|
|
171
|
+
if (failure !== undefined) {
|
|
172
|
+
this.handleCapturedOutput(this.worker, failure)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
this.port.postMessage({ ...message, outputFence })
|
|
149
176
|
}
|
|
150
177
|
|
|
151
178
|
/** Best-effort reply for a request whose lease may outlive a worker reset. */
|
|
@@ -153,11 +180,68 @@ export class WorkerClient {
|
|
|
153
180
|
this.port?.postMessage(message)
|
|
154
181
|
}
|
|
155
182
|
|
|
183
|
+
handleWorkerMessage(worker, outputCapture, value) {
|
|
184
|
+
if (worker !== this.worker) return
|
|
185
|
+
const outputProtocol = value?.type === 'output-start'
|
|
186
|
+
|| value?.type === 'late-output'
|
|
187
|
+
|| value?.type === 'done'
|
|
188
|
+
if (this.outputCompletionPending !== undefined && !outputProtocol) {
|
|
189
|
+
this.deferredOutputMessages.push(value)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
const captured = value?.type === 'output-start'
|
|
193
|
+
? outputCapture.start(value)
|
|
194
|
+
: value?.type === 'late-output'
|
|
195
|
+
? outputCapture.lateOutput()
|
|
196
|
+
: value?.type === 'done'
|
|
197
|
+
? outputCapture.complete(value)
|
|
198
|
+
: value
|
|
199
|
+
if (value?.type === 'done' && captured === undefined) {
|
|
200
|
+
this.outputCompletionPending = value.id
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
this.handleCapturedOutput(worker, captured)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
handleCapturedOutput(worker, message) {
|
|
207
|
+
if (message === undefined || worker !== this.worker) return
|
|
208
|
+
if (message.type === 'worker-output-started') {
|
|
209
|
+
this.port?.postMessage({
|
|
210
|
+
type: 'output-start-ack',
|
|
211
|
+
id: message.id,
|
|
212
|
+
outputFence: message.outputFence,
|
|
213
|
+
})
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
const releasesDeferred = message.type === 'done' && message.id === this.outputCompletionPending
|
|
217
|
+
const discardsDeferred = message.id === this.outputCompletionPending
|
|
218
|
+
&& (message.type === 'worker-output-error' || message.type === 'output-limit')
|
|
219
|
+
if (releasesDeferred || discardsDeferred) this.outputCompletionPending = undefined
|
|
220
|
+
const consumed = this.onMessage(message) === true
|
|
221
|
+
if (message.type === 'worker-output-error' && worker === this.worker) {
|
|
222
|
+
const failureConsumed = consumed || this.onUnmatchedOutputFailure(
|
|
223
|
+
`worker output attribution failed: ${message.message}`,
|
|
224
|
+
) === true
|
|
225
|
+
if (!failureConsumed) this.pendingOutputFailure = message.message
|
|
226
|
+
void this.reset(worker).catch(error => this.onFailure(`worker output reset failed: ${messageOf(error)}`))
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
if (releasesDeferred) {
|
|
230
|
+
const deferred = this.deferredOutputMessages
|
|
231
|
+
this.deferredOutputMessages = []
|
|
232
|
+
for (const value of deferred) this.onMessage(value)
|
|
233
|
+
} else if (discardsDeferred) this.deferredOutputMessages = []
|
|
234
|
+
}
|
|
235
|
+
|
|
156
236
|
fail(worker, message) {
|
|
157
237
|
if (worker !== this.worker) return
|
|
158
238
|
this.worker = undefined
|
|
159
239
|
this.workerLimit = undefined
|
|
160
240
|
this.workerReady = undefined
|
|
241
|
+
this.outputCapture = undefined
|
|
242
|
+
this.outputCompletionPending = undefined
|
|
243
|
+
this.deferredOutputMessages = []
|
|
244
|
+
this.pendingOutputFailure = undefined
|
|
161
245
|
this.port?.close()
|
|
162
246
|
this.port = undefined
|
|
163
247
|
const detail = this.stderrDetail(worker)
|
|
@@ -170,6 +254,9 @@ export class WorkerClient {
|
|
|
170
254
|
this.worker = undefined
|
|
171
255
|
this.workerLimit = undefined
|
|
172
256
|
this.workerReady = undefined
|
|
257
|
+
this.outputCapture = undefined
|
|
258
|
+
this.outputCompletionPending = undefined
|
|
259
|
+
this.deferredOutputMessages = []
|
|
173
260
|
this.port = undefined
|
|
174
261
|
}
|
|
175
262
|
await this.owner.stop(this.hostIds.get(worker))
|
|
@@ -184,6 +271,10 @@ export class WorkerClient {
|
|
|
184
271
|
this.worker = undefined
|
|
185
272
|
this.workerLimit = undefined
|
|
186
273
|
this.workerReady = undefined
|
|
274
|
+
this.outputCapture = undefined
|
|
275
|
+
this.outputCompletionPending = undefined
|
|
276
|
+
this.deferredOutputMessages = []
|
|
277
|
+
this.pendingOutputFailure = undefined
|
|
187
278
|
this.port = undefined
|
|
188
279
|
if (worker !== undefined) this.hostIds.delete(worker)
|
|
189
280
|
const failures = []
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { isAbsolute, parse, resolve, sep } from 'node:path'
|
|
2
2
|
import { promisify } from 'node:util'
|
|
3
|
+
import { runtimeIntrinsics as internal } from './runtime-intrinsics.js'
|
|
4
|
+
import {
|
|
5
|
+
WORKER_REALM_MUTATION,
|
|
6
|
+
assignWorkerRealmProperty,
|
|
7
|
+
installProjectedProcessCwd,
|
|
8
|
+
} from './worker-realm-surfaces.js'
|
|
9
|
+
|
|
10
|
+
const { Reflect: privateReflect, appendArray, bufferConcat, bufferIsBuffer, bufferToString,
|
|
11
|
+
copyArray, isArray, regexpTest } = internal
|
|
12
|
+
const TRAILING_SEPARATOR = /[\\/]$/
|
|
3
13
|
|
|
4
14
|
const FILE_SYSTEM_PATH_ARGUMENTS = Object.freeze({
|
|
5
15
|
access: [0], accessSync: [0], appendFile: [0], appendFileSync: [0],
|
|
@@ -34,7 +44,7 @@ const CHILD_PROCESS_PROJECTED_PROPERTIES = Object.freeze({
|
|
|
34
44
|
function wrapBuiltinCallable(original, projectArguments, projectedProperties = []) {
|
|
35
45
|
const wrapped = {
|
|
36
46
|
invoke(...args) {
|
|
37
|
-
return
|
|
47
|
+
return privateReflect.apply(original, this, projectArguments(args))
|
|
38
48
|
},
|
|
39
49
|
}.invoke
|
|
40
50
|
const descriptors = Object.getOwnPropertyDescriptors(original)
|
|
@@ -56,83 +66,100 @@ function wrapBuiltinCallable(original, projectArguments, projectedProperties = [
|
|
|
56
66
|
function wrapBuiltin(owner, name, projectArguments, projectedProperties) {
|
|
57
67
|
const original = owner?.[name]
|
|
58
68
|
if (typeof original !== 'function') return
|
|
59
|
-
|
|
69
|
+
assignWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'cwd-projected Node builtins',
|
|
70
|
+
owner, name, wrapBuiltinCallable(original, projectArguments, projectedProperties))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function projectChildProcessArguments(name, args, sessionCwd) {
|
|
74
|
+
const sessionChildOptions = options => (
|
|
75
|
+
options === null || typeof options !== 'object' || options.cwd !== undefined
|
|
76
|
+
? options
|
|
77
|
+
: { ...options, cwd: sessionCwd }
|
|
78
|
+
)
|
|
79
|
+
const withDefaultOptions = (index) => {
|
|
80
|
+
const next = copyArray(args)
|
|
81
|
+
next[index] = sessionChildOptions(next[index]) ?? { cwd: sessionCwd }
|
|
82
|
+
return next
|
|
83
|
+
}
|
|
84
|
+
const insertDefaultOptions = (index) => {
|
|
85
|
+
const next = copyArray(args, 0, index)
|
|
86
|
+
appendArray(next, { cwd: sessionCwd })
|
|
87
|
+
return copyArray(args, index, args.length, next)
|
|
88
|
+
}
|
|
89
|
+
if (name === 'exec' || name === 'execSync') {
|
|
90
|
+
const next = copyArray(args)
|
|
91
|
+
if (typeof next[1] === 'function') return insertDefaultOptions(1)
|
|
92
|
+
else next[1] = sessionChildOptions(next[1]) ?? { cwd: sessionCwd }
|
|
93
|
+
return next
|
|
94
|
+
}
|
|
95
|
+
if (name === 'execFile' || name === 'execFileSync') {
|
|
96
|
+
if (args.length === 1 || typeof args[1] === 'function') {
|
|
97
|
+
return insertDefaultOptions(1)
|
|
98
|
+
}
|
|
99
|
+
if (typeof args[2] === 'function') {
|
|
100
|
+
if (args[1] !== undefined && !isArray(args[1])) return withDefaultOptions(1)
|
|
101
|
+
return insertDefaultOptions(2)
|
|
102
|
+
}
|
|
103
|
+
if (args[1] !== undefined && !isArray(args[1])) return withDefaultOptions(1)
|
|
104
|
+
return withDefaultOptions(2)
|
|
105
|
+
}
|
|
106
|
+
return args.length === 1 || (args[1] !== undefined && !isArray(args[1]))
|
|
107
|
+
? withDefaultOptions(1)
|
|
108
|
+
: withDefaultOptions(2)
|
|
60
109
|
}
|
|
61
110
|
|
|
62
111
|
/** Install one session's cwd projection into Node builtins loaded by the worker. */
|
|
63
|
-
export function installWorkerCwdVirtualization(sessionCwd, originalRequire) {
|
|
64
|
-
if (sessionCwd === undefined)
|
|
112
|
+
export function installWorkerCwdVirtualization(sessionCwd, originalRequire, markVolatile) {
|
|
113
|
+
if (sessionCwd === undefined) {
|
|
114
|
+
const nativeCwd = process.cwd
|
|
115
|
+
assignWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'process.cwd', process, 'cwd', () => {
|
|
116
|
+
markVolatile('process.cwd')
|
|
117
|
+
return privateReflect.apply(nativeCwd, process, [])
|
|
118
|
+
})
|
|
119
|
+
return
|
|
120
|
+
}
|
|
65
121
|
const nativeResolve = resolve
|
|
66
|
-
const bufferPrefix = Buffer.from(sessionCwd + (
|
|
122
|
+
const bufferPrefix = Buffer.from(sessionCwd + (regexpTest(TRAILING_SEPARATOR, sessionCwd) ? '' : sep))
|
|
67
123
|
const sessionPath = (value) => {
|
|
68
124
|
if (typeof value === 'string') return isAbsolute(value) ? value : nativeResolve(sessionCwd, value)
|
|
69
|
-
if (
|
|
70
|
-
const absolute = isAbsolute(
|
|
71
|
-
return absolute ? value :
|
|
125
|
+
if (bufferIsBuffer(value)) {
|
|
126
|
+
const absolute = isAbsolute(bufferToString(value, 'utf8', 0, 3))
|
|
127
|
+
return absolute ? value : bufferConcat([bufferPrefix, value])
|
|
72
128
|
}
|
|
73
129
|
return value
|
|
74
130
|
}
|
|
75
131
|
const sessionPathPrefix = (value) => {
|
|
76
132
|
if (typeof value !== 'string' || isAbsolute(value) || parse(value).root !== '') return sessionPath(value)
|
|
77
|
-
return sessionCwd + (
|
|
133
|
+
return sessionCwd + (regexpTest(TRAILING_SEPARATOR, sessionCwd) ? '' : sep) + value
|
|
78
134
|
}
|
|
79
135
|
const projectPaths = (args, indices) => {
|
|
80
|
-
const next =
|
|
81
|
-
for (
|
|
136
|
+
const next = copyArray(args)
|
|
137
|
+
for (let offset = 0; offset < indices.length; offset += 1) {
|
|
138
|
+
const index = indices[offset]
|
|
139
|
+
next[index] = sessionPath(next[index])
|
|
140
|
+
}
|
|
82
141
|
return next
|
|
83
142
|
}
|
|
84
143
|
const projectPrefix = (args) => {
|
|
85
|
-
const next =
|
|
144
|
+
const next = copyArray(args)
|
|
86
145
|
next[0] = sessionPathPrefix(next[0])
|
|
87
146
|
return next
|
|
88
147
|
}
|
|
89
|
-
const sessionChildOptions = options => (
|
|
90
|
-
options === null || typeof options !== 'object' || options.cwd !== undefined
|
|
91
|
-
? options
|
|
92
|
-
: { ...options, cwd: sessionCwd }
|
|
93
|
-
)
|
|
94
|
-
const withDefaultOptions = (args, index) => {
|
|
95
|
-
const next = [...args]
|
|
96
|
-
next[index] = sessionChildOptions(next[index]) ?? { cwd: sessionCwd }
|
|
97
|
-
return next
|
|
98
|
-
}
|
|
99
|
-
const childProcessOptions = (name, args) => {
|
|
100
|
-
if (name === 'exec' || name === 'execSync') {
|
|
101
|
-
const next = [...args]
|
|
102
|
-
if (typeof next[1] === 'function') next.splice(1, 0, { cwd: sessionCwd })
|
|
103
|
-
else next[1] = sessionChildOptions(next[1]) ?? { cwd: sessionCwd }
|
|
104
|
-
return next
|
|
105
|
-
}
|
|
106
|
-
if (name === 'execFile' || name === 'execFileSync') {
|
|
107
|
-
if (args.length === 1 || typeof args[1] === 'function') {
|
|
108
|
-
const next = [...args]
|
|
109
|
-
next.splice(1, 0, { cwd: sessionCwd })
|
|
110
|
-
return next
|
|
111
|
-
}
|
|
112
|
-
if (typeof args[2] === 'function') {
|
|
113
|
-
if (args[1] !== undefined && !Array.isArray(args[1])) return withDefaultOptions(args, 1)
|
|
114
|
-
const next = [...args]
|
|
115
|
-
next.splice(2, 0, { cwd: sessionCwd })
|
|
116
|
-
return next
|
|
117
|
-
}
|
|
118
|
-
if (args[1] !== undefined && !Array.isArray(args[1])) return withDefaultOptions(args, 1)
|
|
119
|
-
return withDefaultOptions(args, 2)
|
|
120
|
-
}
|
|
121
|
-
return args.length === 1 || (args[1] !== undefined && !Array.isArray(args[1]))
|
|
122
|
-
? withDefaultOptions(args, 1)
|
|
123
|
-
: withDefaultOptions(args, 2)
|
|
124
|
-
}
|
|
125
148
|
const sessionGlobOptions = (args) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
149
|
+
if (typeof args[1] === 'function') {
|
|
150
|
+
const next = copyArray(args, 0, 1)
|
|
151
|
+
appendArray(next, { cwd: sessionCwd })
|
|
152
|
+
return copyArray(args, 1, args.length, next)
|
|
153
|
+
}
|
|
154
|
+
const next = copyArray(args)
|
|
155
|
+
if (next[1] === undefined) next[1] = { cwd: sessionCwd }
|
|
129
156
|
else if (next[1] !== null && typeof next[1] === 'object' && next[1].cwd === undefined) {
|
|
130
157
|
next[1] = { ...next[1], cwd: sessionCwd }
|
|
131
158
|
}
|
|
132
159
|
return next
|
|
133
160
|
}
|
|
134
161
|
|
|
135
|
-
process
|
|
162
|
+
installProjectedProcessCwd(process, sessionCwd)
|
|
136
163
|
const fs = originalRequire('node:fs')
|
|
137
164
|
const path = originalRequire('node:path')
|
|
138
165
|
for (const [name, indices] of Object.entries(FILE_SYSTEM_PATH_ARGUMENTS)) {
|
|
@@ -147,9 +174,10 @@ export function installWorkerCwdVirtualization(sessionCwd, originalRequire) {
|
|
|
147
174
|
wrapBuiltin(fs, name, sessionGlobOptions)
|
|
148
175
|
wrapBuiltin(fs.promises, name, sessionGlobOptions)
|
|
149
176
|
}
|
|
150
|
-
wrapBuiltin(path, 'resolve', args =>
|
|
177
|
+
wrapBuiltin(path, 'resolve', args => copyArray(args, 0, args.length, [sessionCwd]))
|
|
151
178
|
const childProcess = originalRequire('node:child_process')
|
|
152
179
|
for (const name of ['exec', 'execFile', 'execFileSync', 'execSync', 'fork', 'spawn', 'spawnSync']) {
|
|
153
|
-
wrapBuiltin(childProcess, name,
|
|
180
|
+
wrapBuiltin(childProcess, name,
|
|
181
|
+
args => projectChildProcessArguments(name, args, sessionCwd), CHILD_PROCESS_PROJECTED_PROPERTIES[name])
|
|
154
182
|
}
|
|
155
183
|
}
|
|
@@ -23,6 +23,26 @@ export const HOST_ONLY_ENVIRONMENT_NAMES = new Set([
|
|
|
23
23
|
'dsh_ptc_compiler_bytecode',
|
|
24
24
|
])
|
|
25
25
|
|
|
26
|
+
/** Detect whether the Host executable needs Electron's Node startup mode. */
|
|
27
|
+
export function isElectronHost(versions = process.versions) {
|
|
28
|
+
return typeof versions?.electron === 'string'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Overlay only the helper process environment needed to boot an Electron executable as Node. */
|
|
32
|
+
export function helperProcessEnvironment(source, platform = process.platform, electronHost = isElectronHost()) {
|
|
33
|
+
if (!electronHost) return source
|
|
34
|
+
const environment = { ...(source ?? process.env) }
|
|
35
|
+
if (platform === 'win32') {
|
|
36
|
+
for (const name of Object.keys(environment)) {
|
|
37
|
+
if (name !== 'ELECTRON_RUN_AS_NODE' && name.toLowerCase() === 'electron_run_as_node') {
|
|
38
|
+
delete environment[name]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
environment.ELECTRON_RUN_AS_NODE = '1'
|
|
43
|
+
return environment
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
/** Project the host environment once without losing Windows case variants. */
|
|
27
47
|
export function normalizeWorkerEnvironment(source, platform = process.platform) {
|
|
28
48
|
if (platform !== 'win32') {
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OUTPUT_FENCE_ALLOWANCE_BYTES,
|
|
3
|
+
outputFenceMarker,
|
|
4
|
+
validOutputFenceToken,
|
|
5
|
+
} from './worker-output-fence.js'
|
|
6
|
+
|
|
7
|
+
function bytes(value) {
|
|
8
|
+
return Buffer.isBuffer(value) ? Buffer.from(value) : Buffer.from(String(value ?? ''))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function nonEmptyText(value) {
|
|
12
|
+
return value.length === 0 ? [] : [value.toString('utf8')]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Correlate helper stdout/stderr with one kernel completion using dual fences. */
|
|
16
|
+
export class WorkerOutputCapture {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.armed = false
|
|
19
|
+
this.round = undefined
|
|
20
|
+
this.settledId = undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
begin(id, maxOutputBytes, outputFence) {
|
|
24
|
+
this.armed = true
|
|
25
|
+
if (this.round !== undefined) {
|
|
26
|
+
return { type: 'worker-output-error', id, message: 'worker output rounds overlapped' }
|
|
27
|
+
}
|
|
28
|
+
if (!validOutputFenceToken(outputFence)) {
|
|
29
|
+
return { type: 'worker-output-error', id, message: 'host created an invalid worker output fence' }
|
|
30
|
+
}
|
|
31
|
+
this.round = {
|
|
32
|
+
id,
|
|
33
|
+
maxOutputBytes,
|
|
34
|
+
outputFence,
|
|
35
|
+
stdout: [],
|
|
36
|
+
stderr: [],
|
|
37
|
+
bytes: 0,
|
|
38
|
+
started: false,
|
|
39
|
+
startMessage: false,
|
|
40
|
+
completion: undefined,
|
|
41
|
+
}
|
|
42
|
+
return undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
push(channel, chunk) {
|
|
46
|
+
if (!this.armed) return undefined
|
|
47
|
+
if (this.round === undefined) {
|
|
48
|
+
this.armed = false
|
|
49
|
+
return {
|
|
50
|
+
type: 'worker-output-error',
|
|
51
|
+
id: this.settledId,
|
|
52
|
+
message: 'a settled cell produced descriptor output after its output fence; the worker was reset to stop unattributed output',
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const value = bytes(chunk)
|
|
56
|
+
this.round[channel].push(value)
|
|
57
|
+
this.round.bytes += value.length
|
|
58
|
+
if (this.round.bytes > this.round.maxOutputBytes + OUTPUT_FENCE_ALLOWANCE_BYTES) {
|
|
59
|
+
const id = this.round.id
|
|
60
|
+
this.round = undefined
|
|
61
|
+
return { type: 'output-limit', id, logs: [] }
|
|
62
|
+
}
|
|
63
|
+
return this.round.started ? this.#finish() : this.#start()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
start(message) {
|
|
67
|
+
if (this.round === undefined || message?.id !== this.round.id) {
|
|
68
|
+
const id = this.round?.id ?? message?.id
|
|
69
|
+
this.round = undefined
|
|
70
|
+
return { type: 'worker-output-error', id, message: 'kernel output start has no matching output round' }
|
|
71
|
+
}
|
|
72
|
+
if (message.outputFence !== this.round.outputFence) {
|
|
73
|
+
const id = this.round.id
|
|
74
|
+
this.round = undefined
|
|
75
|
+
return { type: 'worker-output-error', id, message: 'kernel output start has a mismatched output fence' }
|
|
76
|
+
}
|
|
77
|
+
if (this.round.startMessage) {
|
|
78
|
+
const id = this.round.id
|
|
79
|
+
this.round = undefined
|
|
80
|
+
return { type: 'worker-output-error', id, message: 'kernel repeated an output start' }
|
|
81
|
+
}
|
|
82
|
+
this.round.startMessage = true
|
|
83
|
+
return this.#start()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
complete(message) {
|
|
87
|
+
if (this.round === undefined || message?.id !== this.round.id) {
|
|
88
|
+
const id = this.round?.id ?? message?.id
|
|
89
|
+
this.round = undefined
|
|
90
|
+
return { type: 'worker-output-error', id, message: 'kernel completion has no matching output round' }
|
|
91
|
+
}
|
|
92
|
+
if (!this.round.started || message.outputFence !== this.round.outputFence) {
|
|
93
|
+
const id = this.round.id
|
|
94
|
+
this.round = undefined
|
|
95
|
+
return { type: 'worker-output-error', id, message: 'kernel completion has a mismatched output fence' }
|
|
96
|
+
}
|
|
97
|
+
if (this.round.completion !== undefined) {
|
|
98
|
+
const id = this.round.id
|
|
99
|
+
this.round = undefined
|
|
100
|
+
return { type: 'worker-output-error', id, message: 'kernel repeated a completion' }
|
|
101
|
+
}
|
|
102
|
+
this.round.completion = message
|
|
103
|
+
return this.#finish()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
lateOutput() {
|
|
107
|
+
if (this.round !== undefined) {
|
|
108
|
+
const id = this.round.id
|
|
109
|
+
this.round = undefined
|
|
110
|
+
return {
|
|
111
|
+
type: 'worker-output-error',
|
|
112
|
+
id,
|
|
113
|
+
message: 'a settled cell produced background output during this cell; the worker was reset to prevent cross-cell attribution',
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (this.armed) {
|
|
117
|
+
this.armed = false
|
|
118
|
+
return {
|
|
119
|
+
type: 'worker-output-error',
|
|
120
|
+
id: this.settledId,
|
|
121
|
+
message: 'a settled cell produced background output after its output fence; the worker was reset to prevent cross-cell attribution',
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return undefined
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#start() {
|
|
128
|
+
const round = this.round
|
|
129
|
+
if (round?.startMessage !== true) return undefined
|
|
130
|
+
const stdout = Buffer.concat(round.stdout)
|
|
131
|
+
const stderr = Buffer.concat(round.stderr)
|
|
132
|
+
const stdoutFence = Buffer.from(outputFenceMarker(round.outputFence, 'stdout', 'start'))
|
|
133
|
+
const stderrFence = Buffer.from(outputFenceMarker(round.outputFence, 'stderr', 'start'))
|
|
134
|
+
const stdoutAt = stdout.indexOf(stdoutFence)
|
|
135
|
+
const stderrAt = stderr.indexOf(stderrFence)
|
|
136
|
+
if (stdoutAt < 0 || stderrAt < 0) return undefined
|
|
137
|
+
const stdoutAfter = stdout.subarray(stdoutAt + stdoutFence.length)
|
|
138
|
+
const stderrAfter = stderr.subarray(stderrAt + stderrFence.length)
|
|
139
|
+
if (stdoutAt > 0 || stderrAt > 0 || stdoutAfter.length > 0 || stderrAfter.length > 0) {
|
|
140
|
+
const id = round.id
|
|
141
|
+
this.round = undefined
|
|
142
|
+
return {
|
|
143
|
+
type: 'worker-output-error',
|
|
144
|
+
id,
|
|
145
|
+
message: 'a settled cell produced descriptor output before the next cell started; the worker was reset to prevent cross-cell attribution',
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
round.stdout = []
|
|
149
|
+
round.stderr = []
|
|
150
|
+
round.bytes = 0
|
|
151
|
+
round.started = true
|
|
152
|
+
return { type: 'worker-output-started', id: round.id, outputFence: round.outputFence }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#finish() {
|
|
156
|
+
const round = this.round
|
|
157
|
+
if (round?.completion === undefined) return undefined
|
|
158
|
+
const stdout = Buffer.concat(round.stdout)
|
|
159
|
+
const stderr = Buffer.concat(round.stderr)
|
|
160
|
+
const stdoutFence = Buffer.from(outputFenceMarker(round.outputFence, 'stdout', 'end'))
|
|
161
|
+
const stderrFence = Buffer.from(outputFenceMarker(round.outputFence, 'stderr', 'end'))
|
|
162
|
+
const stdoutAt = stdout.indexOf(stdoutFence)
|
|
163
|
+
const stderrAt = stderr.indexOf(stderrFence)
|
|
164
|
+
if (stdoutAt < 0 || stderrAt < 0) return undefined
|
|
165
|
+
const stdoutAfter = stdout.subarray(stdoutAt + stdoutFence.length)
|
|
166
|
+
const stderrAfter = stderr.subarray(stderrAt + stderrFence.length)
|
|
167
|
+
if (stdoutAfter.length > 0 || stderrAfter.length > 0) {
|
|
168
|
+
this.round = undefined
|
|
169
|
+
this.armed = false
|
|
170
|
+
return {
|
|
171
|
+
type: 'worker-output-error',
|
|
172
|
+
id: round.id,
|
|
173
|
+
message: 'a settled cell produced descriptor output after its output fence; the worker was reset to prevent cross-cell attribution',
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const descriptorLogs = [
|
|
177
|
+
...nonEmptyText(stdout.subarray(0, stdoutAt)),
|
|
178
|
+
...nonEmptyText(stderr.subarray(0, stderrAt)),
|
|
179
|
+
]
|
|
180
|
+
const rawBytes = descriptorLogs.reduce((total, value) => total + Buffer.byteLength(JSON.stringify(value)), 0)
|
|
181
|
+
this.round = undefined
|
|
182
|
+
this.settledId = round.id
|
|
183
|
+
if (rawBytes > round.maxOutputBytes) return { type: 'output-limit', id: round.id, logs: [] }
|
|
184
|
+
const logs = Array.isArray(round.completion.logs) ? [...round.completion.logs, ...descriptorLogs] : descriptorLogs
|
|
185
|
+
return { ...round.completion, logs }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { runtimeIntrinsics as internal } from './runtime-intrinsics.js'
|
|
2
|
+
|
|
3
|
+
const FENCE_PREFIX = '\u001eDSH_PTC_OUTPUT_FENCE_V1:'
|
|
4
|
+
const FENCE_SUFFIX = '\u001f'
|
|
5
|
+
const TOKEN = /^[a-f0-9]{48}$/u
|
|
6
|
+
const FENCE = /\u001eDSH_PTC_OUTPUT_FENCE_V1:(?:stdout|stderr):(?:start|end):[a-f0-9]{48}\u001f/gu
|
|
7
|
+
const { regexpTest, replaceString, toString } = internal
|
|
8
|
+
|
|
9
|
+
export const OUTPUT_FENCE_ALLOWANCE_BYTES = 512
|
|
10
|
+
|
|
11
|
+
export function validOutputFenceToken(token) {
|
|
12
|
+
return typeof token === 'string' && regexpTest(TOKEN, token)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function outputFenceMarker(token, channel, phase) {
|
|
16
|
+
if (!validOutputFenceToken(token)
|
|
17
|
+
|| (channel !== 'stdout' && channel !== 'stderr')
|
|
18
|
+
|| (phase !== 'start' && phase !== 'end')) {
|
|
19
|
+
throw new TypeError('invalid worker output fence')
|
|
20
|
+
}
|
|
21
|
+
return `${FENCE_PREFIX}${channel}:${phase}:${token}${FENCE_SUFFIX}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function stripOutputFenceMarkers(value) {
|
|
25
|
+
return replaceString(toString(value ?? ''), FENCE, '')
|
|
26
|
+
}
|