dsh-ptc-plus 0.4.8 → 0.4.10
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 +11 -3
- package/README.md +11 -3
- package/client.js +1151 -529
- package/cordis.patch.yml +12 -2
- package/docs/README.md +1 -0
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +19 -0
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +13 -2
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +1 -1
- package/docs/adr/0023-global-user-bindings.md +1 -1
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +82 -18
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
- package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
- package/docs/architecture.md +3 -3
- package/docs/binding-review-placement-design.md +29 -21
- package/docs/client-ui.md +20 -8
- package/docs/client-visual-contract.md +48 -0
- package/docs/durability-design.md +12 -0
- package/docs/installation.md +27 -6
- package/docs/runtime-reference.md +1 -1
- package/docs/user-bindings.en.md +2 -2
- package/docs/user-bindings.md +3 -3
- package/docs/verification-optimization-work.md +26 -0
- package/docs/verification.md +2 -0
- package/index.js +28 -7
- package/internal/compiler-platform.js +5 -3
- package/internal/compiler-service.js +2 -9
- package/internal/cordis-tools-owner.js +22 -19
- package/internal/direct-surface-owner.js +2 -0
- package/internal/edit-transport-owner.js +8 -5
- package/internal/execution-provider-owner.js +157 -0
- package/internal/execution-seam-compat.js +69 -113
- package/internal/isolated-worker.js +12 -8
- package/internal/recovery-tips.js +50 -21
- package/internal/runtime-bridge-owner.js +88 -57
- package/internal/runtime-contexts.js +13 -13
- package/internal/session-events.js +40 -4
- package/internal/session-journal-recovery.js +678 -181
- package/internal/session-log-view.js +189 -43
- package/internal/session-runtime.js +58 -10
- package/internal/settings-compat.js +12 -2
- package/internal/user-bindings-owner.js +6 -5
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +13 -14
- package/src/client-authoring-view.js +222 -141
- package/src/client-binding-review.js +2 -0
- package/src/client-code-editor.js +6 -0
- package/src/client-copy.js +10 -0
- package/src/client-feature-gates.js +12 -5
- package/src/client-host-compat.js +23 -2
- package/src/client-primitives.js +249 -0
- package/src/client-repl-view.js +3 -3
- package/src/client-settings-view.js +2 -29
- package/src/client-styles.js +92 -33
- package/src/client-tool-view.js +8 -3
- package/src/client-workbench.js +3 -3
- package/src/client.js +72 -46
- package/assets/ptc-plus-bindings-en.png +0 -0
- package/assets/ptc-plus-bindings-zh.png +0 -0
- package/assets/ptc-plus-repair-en.png +0 -0
- package/assets/ptc-plus-repair-zh.png +0 -0
|
@@ -149,8 +149,9 @@ function containsCompanionSkill(directory, fileExists) {
|
|
|
149
149
|
|
|
150
150
|
/** Resolve the official Cordis companion Skill across DSH preset generations. */
|
|
151
151
|
export async function resolveCompanionSkillDirectory(agentPresets, options = {}) {
|
|
152
|
-
const fileExists = options.fileExists ?? existsSync
|
|
153
152
|
const preset = await agentPresets.resolve(CORDIS_PRESET_ID)
|
|
153
|
+
const resolution = typeof options === 'function' ? options() : options
|
|
154
|
+
const fileExists = resolution.fileExists ?? existsSync
|
|
154
155
|
if (typeof preset?.broken === 'string' && preset.broken.length > 0) {
|
|
155
156
|
throw new Error(`ptc-plus: DSH cordis preset is unavailable: ${preset.broken}`)
|
|
156
157
|
}
|
|
@@ -166,7 +167,7 @@ export async function resolveCompanionSkillDirectory(agentPresets, options = {})
|
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
for (const layout of COMPANION_PACKAGE_LAYOUTS) {
|
|
169
|
-
const pkg =
|
|
170
|
+
const pkg = resolution.packageOf?.(layout.name, resolution.packageBaseUrl)
|
|
170
171
|
if (pkg === undefined) continue
|
|
171
172
|
if (pkg?.name !== layout.name || typeof pkg.dir !== 'string' || !isAbsolute(pkg.dir)) {
|
|
172
173
|
throw new Error(`ptc-plus: DSH pluginPackages returned an invalid ${layout.name} package resource`)
|
|
@@ -175,10 +176,10 @@ export async function resolveCompanionSkillDirectory(agentPresets, options = {})
|
|
|
175
176
|
if (containsCompanionSkill(directory, fileExists)) return directory
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
if (
|
|
179
|
+
if (resolution.packageOf === undefined && resolution.resolveLegacyPackageManifest !== undefined) {
|
|
179
180
|
let manifest
|
|
180
181
|
try {
|
|
181
|
-
manifest =
|
|
182
|
+
manifest = resolution.resolveLegacyPackageManifest('@deepseek-ai/dsh-agent-presets/package.json')
|
|
182
183
|
} catch (error) {
|
|
183
184
|
if (error?.code !== 'MODULE_NOT_FOUND' && error?.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw error
|
|
184
185
|
}
|
|
@@ -320,22 +321,24 @@ export function createCordisToolsOwner(
|
|
|
320
321
|
const cordisInspectLeases = createCordisInspectLeases(ctx)
|
|
321
322
|
const scopedCordisPlugin = cordisInspectLeases.plugin(cordisPlugin)
|
|
322
323
|
const scopedSkillPlugin = exactCompanionSkillPlugin(skillFilesystemPlugin)
|
|
323
|
-
const pluginPackages = ctx.get?.('pluginPackages')
|
|
324
324
|
const packageBaseUrl = ctx.baseUrl
|
|
325
|
-
const skillResolution = companionResolution ?? {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
:
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
: undefined
|
|
338
|
-
|
|
325
|
+
const skillResolution = companionResolution ?? (() => {
|
|
326
|
+
const pluginPackages = ctx.get?.('pluginPackages')
|
|
327
|
+
return {
|
|
328
|
+
packageOf: pluginPackages === undefined
|
|
329
|
+
? undefined
|
|
330
|
+
: (name, parentURL) => {
|
|
331
|
+
if (typeof pluginPackages?.packageOf !== 'function') {
|
|
332
|
+
throw new Error('ptc-plus: cordisToolsEnabled requires the DSH pluginPackages.packageOf API')
|
|
333
|
+
}
|
|
334
|
+
return pluginPackages.packageOf(name, parentURL)
|
|
335
|
+
},
|
|
336
|
+
packageBaseUrl,
|
|
337
|
+
resolveLegacyPackageManifest: pluginPackages === undefined && typeof packageBaseUrl === 'string'
|
|
338
|
+
? specifier => createRequire(packageBaseUrl).resolve(specifier)
|
|
339
|
+
: undefined,
|
|
340
|
+
}
|
|
341
|
+
})
|
|
339
342
|
let disposed = false
|
|
340
343
|
|
|
341
344
|
const leafErrors = error => error instanceof AggregateError
|
|
@@ -464,6 +464,7 @@ export function createDirectSurfaceOwner({
|
|
|
464
464
|
toolSchemasForAgent,
|
|
465
465
|
userBindingsForAgent = async () => undefined,
|
|
466
466
|
setAgentPresentation = async () => {},
|
|
467
|
+
sessionLogView,
|
|
467
468
|
}) {
|
|
468
469
|
// Composition is anchored to Agent identity because DSH selects presentation
|
|
469
470
|
// once per composed agent. Request signals bind the exact assembly to stream
|
|
@@ -634,6 +635,7 @@ export function createDirectSurfaceOwner({
|
|
|
634
635
|
}
|
|
635
636
|
const runtimeContexts = sessionPtc
|
|
636
637
|
? sessionRuntimeContexts(agent, tipConfig, {
|
|
638
|
+
sessionLogView,
|
|
637
639
|
cordisRecoveryRequired(view) {
|
|
638
640
|
return cordisRecovery.required(agent, view)
|
|
639
641
|
},
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
editRejectedCell,
|
|
18
18
|
editRunCodeSchema,
|
|
19
19
|
} from './rejected-cell-editor.js'
|
|
20
|
-
import {
|
|
20
|
+
import { projectSessionLog } from './session-log-view.js'
|
|
21
21
|
import { RUN_CODE } from './runtime-bridge-owner.js'
|
|
22
22
|
import { isRecord } from './record-utils.js'
|
|
23
23
|
import {
|
|
@@ -67,6 +67,8 @@ export function createEditTransportOwner(ctx, {
|
|
|
67
67
|
presentationGeneration,
|
|
68
68
|
sessionId,
|
|
69
69
|
toolSchemasForAgent,
|
|
70
|
+
readSessionLog,
|
|
71
|
+
sessionLogView = projectSessionLog,
|
|
70
72
|
}) {
|
|
71
73
|
let currentDurableReplay = durableReplay
|
|
72
74
|
const editExecutionMetadata = new WeakMap()
|
|
@@ -143,13 +145,14 @@ export function createEditTransportOwner(ctx, {
|
|
|
143
145
|
definition.execute = async (args, exec) => {
|
|
144
146
|
const agent = exec?.agent
|
|
145
147
|
const callId = typeof exec?.callId === 'string' ? exec.callId : undefined
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
+
const sessionLog = await readSessionLog(agent?.session, exec.signal)
|
|
149
|
+
const persistedTargetCallSeq = liveToolCallSeq(sessionLog, callId, EDIT_RUN_CODE)
|
|
150
|
+
if (persistedTargetCallSeq === undefined && sessionLogView(agent).editableRun !== undefined) {
|
|
148
151
|
throw new Error('ptc-plus: edit_run_code requires a unique persisted tool/call event')
|
|
149
152
|
}
|
|
150
153
|
const target = persistedTargetCallSeq === undefined
|
|
151
154
|
? undefined
|
|
152
|
-
:
|
|
155
|
+
: sessionLogView(agent, { callId, callSeq: persistedTargetCallSeq }).requestedEditTarget
|
|
153
156
|
if (target?.source === undefined || target.callSeq === undefined) {
|
|
154
157
|
return { edited: false, reason: 'no run_code cell is currently eligible for safe editing' }
|
|
155
158
|
}
|
|
@@ -190,7 +193,7 @@ export function createEditTransportOwner(ctx, {
|
|
|
190
193
|
agent,
|
|
191
194
|
signal: exec.signal,
|
|
192
195
|
})
|
|
193
|
-
const tentative = await executeTentative(persistedCallSeq, dispatch)
|
|
196
|
+
const tentative = await executeTentative(persistedCallSeq, dispatch, sessionLog)
|
|
194
197
|
const inner = tentative.result
|
|
195
198
|
finalizeTentative = tentative.finalize
|
|
196
199
|
const journalValue = isRecord(inner?.meta) && Object.hasOwn(inner.meta, JOURNAL_KEY)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createExecutionSeam, EXECUTION_SEAM_SERVICE } from './execution-seam-compat.js'
|
|
2
|
+
|
|
3
|
+
export const name = 'ptc-plus-execution-provider'
|
|
4
|
+
export const inject = ['loader']
|
|
5
|
+
|
|
6
|
+
export function apply(ctx, config = {}) {
|
|
7
|
+
const ownerFiber = ctx.fiber
|
|
8
|
+
const { originalEntryId = 'ptc-runtime', isolation = 'ptc-plus-original' } = config
|
|
9
|
+
if (typeof originalEntryId !== 'string' || originalEntryId.length === 0
|
|
10
|
+
|| typeof isolation !== 'string' || isolation.length === 0) {
|
|
11
|
+
throw new TypeError('ptc-plus: execution provider requires an original entry id and named isolation')
|
|
12
|
+
}
|
|
13
|
+
// Profile rows belong to the Include's tree; the global Loader resolves root IDs.
|
|
14
|
+
const entryTree = ownerFiber.entry?.parent.tree ?? ctx.loader
|
|
15
|
+
const originalEntry = entryTree.resolve(originalEntryId)
|
|
16
|
+
const isolated = () => originalEntry.options.isolate?.[EXECUTION_SEAM_SERVICE] === isolation
|
|
17
|
+
if (!isolated()) throw new Error(`ptc-plus: original entry ${originalEntryId} must isolate ${EXECUTION_SEAM_SERVICE} as ${isolation}`)
|
|
18
|
+
const originalContext = originalEntry.context.extend({ [originalEntry.constructor.key]: undefined })
|
|
19
|
+
let watcher
|
|
20
|
+
let live
|
|
21
|
+
let stopped = false
|
|
22
|
+
let suspended = false
|
|
23
|
+
let generation
|
|
24
|
+
const pending = new Set()
|
|
25
|
+
let resolveAttachment
|
|
26
|
+
let rejectAttachment
|
|
27
|
+
const attachment = new Promise((resolve, reject) => {
|
|
28
|
+
resolveAttachment = resolve
|
|
29
|
+
rejectAttachment = reject
|
|
30
|
+
})
|
|
31
|
+
const readOriginal = () => {
|
|
32
|
+
const original = isolated() ? originalContext.get(EXECUTION_SEAM_SERVICE) : undefined
|
|
33
|
+
if (stopped || original === undefined) {
|
|
34
|
+
throw new Error(`ptc-plus: original ${EXECUTION_SEAM_SERVICE} generation is unavailable`)
|
|
35
|
+
}
|
|
36
|
+
return original
|
|
37
|
+
}
|
|
38
|
+
const release = () => {
|
|
39
|
+
const entry = live
|
|
40
|
+
if (entry === undefined) return
|
|
41
|
+
live = undefined
|
|
42
|
+
entry.seam.retire()
|
|
43
|
+
const unregistration = entry.unregister?.()
|
|
44
|
+
const disposal = Promise.allSettled([unregistration, entry.disposeChild?.()]).then(results => {
|
|
45
|
+
const failures = results.filter(result => result.status === 'rejected').map(result => result.reason)
|
|
46
|
+
if (failures.length > 0) throw new AggregateError(failures, 'ptc-plus: execution provider disposal failed')
|
|
47
|
+
})
|
|
48
|
+
pending.add(disposal)
|
|
49
|
+
disposal.then(() => pending.delete(disposal), error => {
|
|
50
|
+
pending.delete(disposal)
|
|
51
|
+
ctx.logger.warn(error)
|
|
52
|
+
})
|
|
53
|
+
return disposal
|
|
54
|
+
}
|
|
55
|
+
const suspend = () => {
|
|
56
|
+
suspended = true
|
|
57
|
+
const disposal = release()
|
|
58
|
+
const previous = watcher
|
|
59
|
+
watcher = undefined
|
|
60
|
+
return Promise.all([disposal, previous?.dispose()])
|
|
61
|
+
}
|
|
62
|
+
const publish = () => {
|
|
63
|
+
const resolutions = new WeakMap()
|
|
64
|
+
const delegate = {
|
|
65
|
+
get language() { return readOriginal().language },
|
|
66
|
+
get isolation() { return readOriginal().isolation },
|
|
67
|
+
get executionInstructions() { return readOriginal().executionInstructions },
|
|
68
|
+
get sandboxMode() { return readOriginal().sandboxMode },
|
|
69
|
+
get timeout() { return readOriginal().timeout },
|
|
70
|
+
resolve(request) {
|
|
71
|
+
const original = readOriginal()
|
|
72
|
+
const spec = original.resolve(request)
|
|
73
|
+
if (spec !== null && typeof spec === 'object') {
|
|
74
|
+
resolutions.set(spec, { original, generation })
|
|
75
|
+
}
|
|
76
|
+
return spec
|
|
77
|
+
},
|
|
78
|
+
run(spec) {
|
|
79
|
+
const resolved = resolutions.get(spec)
|
|
80
|
+
if (resolved?.generation?.active === false) throw new Error('ptc-plus: resolved spec belongs to an unavailable original generation')
|
|
81
|
+
const original = resolved?.original ?? readOriginal()
|
|
82
|
+
return original.run(spec)
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
const seam = createExecutionSeam(delegate, EXECUTION_SEAM_SERVICE)
|
|
86
|
+
const entry = { seam }
|
|
87
|
+
live = entry
|
|
88
|
+
entry.disposeChild = ctx.effect(() => {
|
|
89
|
+
const child = ctx.plugin({
|
|
90
|
+
name: 'ptc-plus-composed-runtime',
|
|
91
|
+
apply(childContext) {
|
|
92
|
+
childContext.effect(() => {
|
|
93
|
+
if (stopped || live !== entry || !isolated()) return () => {}
|
|
94
|
+
entry.unregister = childContext.provide(EXECUTION_SEAM_SERVICE, seam.provider)
|
|
95
|
+
return () => entry.unregister()
|
|
96
|
+
}, 'ptc-plus owned execution registration')
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
entry.child = child
|
|
100
|
+
return () => child.dispose()
|
|
101
|
+
}, 'ptc-plus owned execution child')
|
|
102
|
+
return entry.child.await().then(() => {
|
|
103
|
+
if (!stopped && live === entry && entry.unregister !== undefined && entry.child.state === 2) resolveAttachment()
|
|
104
|
+
}, error => {
|
|
105
|
+
rejectAttachment(error)
|
|
106
|
+
throw error
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
const watch = () => {
|
|
110
|
+
if (stopped || watcher !== undefined || !isolated()) return
|
|
111
|
+
ctx.effect(() => {
|
|
112
|
+
const observer = originalContext.inject([EXECUTION_SEAM_SERVICE], scope => {
|
|
113
|
+
if (stopped || !isolated()) return
|
|
114
|
+
createExecutionSeam(scope.get(EXECUTION_SEAM_SERVICE), EXECUTION_SEAM_SERVICE)
|
|
115
|
+
const currentGeneration = { active: true }
|
|
116
|
+
generation = currentGeneration
|
|
117
|
+
scope.effect(() => () => {
|
|
118
|
+
currentGeneration.active = false
|
|
119
|
+
if (generation === currentGeneration) generation = undefined
|
|
120
|
+
}, 'ptc-plus original generation scope')
|
|
121
|
+
if (live === undefined) return publish()
|
|
122
|
+
return live.seam.sourceChanged()
|
|
123
|
+
})
|
|
124
|
+
watcher = observer
|
|
125
|
+
return () => observer.dispose()
|
|
126
|
+
}, 'ptc-plus original execution observer')
|
|
127
|
+
}
|
|
128
|
+
ctx.on('loader/partial-dispose', (entry, _legacy, active) => {
|
|
129
|
+
if (entry === originalEntry && (!active || !isolated())) void suspend().catch(error => ctx.logger.warn(error))
|
|
130
|
+
}, { prepend: true, global: true })
|
|
131
|
+
ctx.on('loader/patch-context', (entry, next) => {
|
|
132
|
+
if (entry === originalEntry && !isolated()) void suspend().catch(error => ctx.logger.warn(error))
|
|
133
|
+
next()
|
|
134
|
+
if (entry === originalEntry && isolated() && suspended && !stopped && ownerFiber.state === 2) {
|
|
135
|
+
stopped = true
|
|
136
|
+
resolveAttachment()
|
|
137
|
+
void suspend().catch(error => ctx.logger.warn(error))
|
|
138
|
+
void ownerFiber.restart().catch(error => ctx.logger.warn(error))
|
|
139
|
+
}
|
|
140
|
+
}, { prepend: true, global: true })
|
|
141
|
+
ctx.effect(() => () => {
|
|
142
|
+
stopped = true
|
|
143
|
+
resolveAttachment()
|
|
144
|
+
const suspension = suspend()
|
|
145
|
+
return Promise.all([suspension, ...pending])
|
|
146
|
+
}, 'ptc-plus execution provider composition')
|
|
147
|
+
if (originalEntry.disabled) resolveAttachment()
|
|
148
|
+
const timer = setTimeout(() => rejectAttachment(new Error(
|
|
149
|
+
`ptc-plus: original execution entry ${originalEntryId} did not publish ${EXECUTION_SEAM_SERVICE}`,
|
|
150
|
+
)), 30_000)
|
|
151
|
+
ctx.effect(() => () => clearTimeout(timer), 'ptc-plus original execution provider wait')
|
|
152
|
+
const originalReady = originalEntry.fiber?.await() ?? Promise.resolve()
|
|
153
|
+
return originalReady.then(() => {
|
|
154
|
+
watch()
|
|
155
|
+
return attachment
|
|
156
|
+
}).finally(() => clearTimeout(timer))
|
|
157
|
+
}
|
|
@@ -1,39 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Own how PTC Plus attaches to the host's program-execution seam.
|
|
3
|
-
*
|
|
4
|
-
* DSH registers one execution seam per generation. The current release
|
|
5
|
-
* registers `ctx.ptcRuntime` (`@deepseek-ai/dsh-ptc-runtime`); the preceding one
|
|
6
|
-
* registered `ctx.codeRuntime` (`@deepseek-ai/dsh-code-runtime`). Both run one
|
|
7
|
-
* model-written program against host async bindings. They differ in the service
|
|
8
|
-
* name, in the call shape (`resolve(request)` then `run(spec)` against a single
|
|
9
|
-
* `run(request)`), and in the capability descriptors a provider publishes.
|
|
10
|
-
* Which seam is live is decided by the registered service, never by a version
|
|
11
|
-
* string, so a deployment on either generation obtains the same behavior.
|
|
12
|
-
*
|
|
13
|
-
* PTC Plus replaces the execution of session cells with its own persistent
|
|
14
|
-
* REPL. The object DSH describes to the model is therefore the plugin's
|
|
15
|
-
* execution, not the wrapped provider's, so `takeOver()` answers the
|
|
16
|
-
* descriptors the plugin can honor and withholds the ones it cannot. The REPL
|
|
17
|
-
* runs every cell in one long-lived worker rather than a fresh process per
|
|
18
|
-
* call, applies the session's configured budgets rather than a per-call host
|
|
19
|
-
* deadline, and performs no file confinement. DSH gates `timeoutMs` on
|
|
20
|
-
* `timeout` and resolves or escalates file policy on `sandboxMode`, so
|
|
21
|
-
* withholding those descriptors keeps DSH's authority decisions with DSH
|
|
22
|
-
* instead of having this plugin accept an input it would then ignore.
|
|
23
|
-
*
|
|
24
|
-
* A seam-neutral request carries exactly `program`, `bindings`, and `signal`:
|
|
25
|
-
* the fields PTC Plus consumes. A resolved spec's directory, deadline, and
|
|
26
|
-
* authority describe the provider whose execution was replaced, and are neither
|
|
27
|
-
* forwarded to the session kernel nor re-derived here.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
1
|
/** Cordis service name registered by the current DSH generation. */
|
|
31
2
|
export const EXECUTION_SEAM_SERVICE = 'ptcRuntime'
|
|
32
3
|
|
|
33
|
-
/**
|
|
4
|
+
/** Historical request-only service name, retained with fixture evidence. */
|
|
34
5
|
export const LEGACY_EXECUTION_SEAM_SERVICE = 'codeRuntime'
|
|
35
6
|
|
|
36
7
|
const SEAM_SERVICES = Object.freeze([EXECUTION_SEAM_SERVICE, LEGACY_EXECUTION_SEAM_SERVICE])
|
|
8
|
+
const PLUGIN_EXECUTION_SEAM = Symbol.for('dsh-ptc-plus.execution-seam')
|
|
37
9
|
|
|
38
10
|
/**
|
|
39
11
|
* How long the plugin waits for the host's execution seam before it reports the
|
|
@@ -48,34 +20,12 @@ const SEAM_SERVICES = Object.freeze([EXECUTION_SEAM_SERVICE, LEGACY_EXECUTION_SE
|
|
|
48
20
|
*/
|
|
49
21
|
const SEAM_ATTACHMENT_TIMEOUT_MS = 30_000
|
|
50
22
|
|
|
51
|
-
/**
|
|
52
|
-
* Descriptors the plugin replaces with the capabilities of its own execution.
|
|
53
|
-
* `executionInstructions` describes the wrapped provider's program model, which
|
|
54
|
-
* no longer applies; `timeout` and `sandboxMode` gate host inputs the plugin
|
|
55
|
-
* cannot honor. Withdrawal is unconditional because the rule is a property of
|
|
56
|
-
* the plugin's execution, not of a generation: the preceding generation's
|
|
57
|
-
* contract defines none of these members and nothing in its install closure
|
|
58
|
-
* reads them, so withdrawing there is inert, while a provider that published
|
|
59
|
-
* them stays unable to outlive the takeover.
|
|
60
|
-
*/
|
|
61
|
-
const WITHHELD_DESCRIPTORS = Object.freeze(['executionInstructions', 'sandboxMode', 'timeout'])
|
|
62
|
-
|
|
63
23
|
/** Read a registered service without declaring an injection requirement. */
|
|
64
24
|
function readService(ctx, name) {
|
|
65
25
|
if (typeof ctx?.get === 'function') return ctx.get(name)
|
|
66
26
|
return ctx?.[name]
|
|
67
27
|
}
|
|
68
28
|
|
|
69
|
-
function descriptorsMatch(left, right) {
|
|
70
|
-
if (left === undefined || right === undefined) return left === right
|
|
71
|
-
return left.value === right.value
|
|
72
|
-
&& left.get === right.get
|
|
73
|
-
&& left.set === right.set
|
|
74
|
-
&& left.writable === right.writable
|
|
75
|
-
&& left.enumerable === right.enumerable
|
|
76
|
-
&& left.configurable === right.configurable
|
|
77
|
-
}
|
|
78
|
-
|
|
79
29
|
/**
|
|
80
30
|
* Build the seam handle for one live execution service.
|
|
81
31
|
* @param {object} service - the registered `ctx.ptcRuntime` or `ctx.codeRuntime`.
|
|
@@ -85,6 +35,7 @@ export function createExecutionSeam(service, serviceName) {
|
|
|
85
35
|
if (service === null || typeof service !== 'object') {
|
|
86
36
|
throw new TypeError(`ptc-plus: ${serviceName} must be an execution service object`)
|
|
87
37
|
}
|
|
38
|
+
if (typeof service[PLUGIN_EXECUTION_SEAM] === 'function') return service[PLUGIN_EXECUTION_SEAM]()
|
|
88
39
|
const current = serviceName === EXECUTION_SEAM_SERVICE
|
|
89
40
|
const upstreamRun = service.run
|
|
90
41
|
if (typeof upstreamRun !== 'function') {
|
|
@@ -95,82 +46,87 @@ export function createExecutionSeam(service, serviceName) {
|
|
|
95
46
|
throw new TypeError('ptc-plus: ptcRuntime.resolve must be a function')
|
|
96
47
|
}
|
|
97
48
|
let installed
|
|
98
|
-
|
|
49
|
+
const sourceListeners = new Set()
|
|
50
|
+
let available = true
|
|
51
|
+
const requireAvailable = () => {
|
|
52
|
+
if (!available) throw new Error(`ptc-plus: original ${serviceName} generation is unavailable`)
|
|
53
|
+
}
|
|
54
|
+
const requireCompatible = () => {
|
|
55
|
+
requireAvailable()
|
|
56
|
+
if (installed !== undefined && service.language !== installed.language) {
|
|
57
|
+
throw new Error(`ptc-plus: current original language ${JSON.stringify(service.language)} is incompatible with the active ${JSON.stringify(installed.language)} runtime`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const provider = Object.freeze({
|
|
61
|
+
get language() { requireCompatible(); return service.language },
|
|
62
|
+
get isolation() { requireCompatible(); return installed === undefined ? service.isolation : 'worker-thread' },
|
|
63
|
+
get executionInstructions() { requireCompatible(); return installed === undefined ? service.executionInstructions ?? '' : '' },
|
|
64
|
+
get sandboxMode() { requireCompatible(); return installed === undefined ? service.sandboxMode : undefined },
|
|
65
|
+
get timeout() { requireCompatible(); return installed === undefined ? service.timeout : undefined },
|
|
66
|
+
resolve(request) {
|
|
67
|
+
requireCompatible()
|
|
68
|
+
return current ? upstreamResolve.call(service, request) : request
|
|
69
|
+
},
|
|
70
|
+
run(input) {
|
|
71
|
+
requireCompatible()
|
|
72
|
+
return installed === undefined ? upstreamRun.call(service, input) : installed.execute(input)
|
|
73
|
+
},
|
|
74
|
+
[PLUGIN_EXECUTION_SEAM]() { return seam },
|
|
75
|
+
})
|
|
76
|
+
const seam = Object.freeze({
|
|
99
77
|
serviceName,
|
|
100
|
-
language
|
|
78
|
+
get language() { requireAvailable(); return service.language },
|
|
79
|
+
provider,
|
|
80
|
+
get active() { return installed !== undefined && service.language === installed.language },
|
|
81
|
+
onSourceChange(listener) {
|
|
82
|
+
sourceListeners.add(listener)
|
|
83
|
+
return () => sourceListeners.delete(listener)
|
|
84
|
+
},
|
|
85
|
+
sourceChanged() {
|
|
86
|
+
return Promise.all([...sourceListeners].map(listener => listener())).then(() => undefined)
|
|
87
|
+
},
|
|
101
88
|
/**
|
|
102
89
|
* Run one program through the provider this plugin wraps. The current
|
|
103
90
|
* generation resolves the request before running it, exactly as its own
|
|
104
91
|
* consumer does; the preceding generation accepts the request directly.
|
|
105
92
|
*/
|
|
106
93
|
invokeUpstream(request) {
|
|
94
|
+
requireCompatible()
|
|
107
95
|
return current
|
|
108
96
|
? upstreamRun.call(service, upstreamResolve.call(service, request))
|
|
109
97
|
: upstreamRun.call(service, request)
|
|
110
98
|
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (installed !== undefined) throw new Error('ptc-plus: execution seam already
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const requested = new Map()
|
|
122
|
-
requested.set('run', {
|
|
123
|
-
configurable: true,
|
|
124
|
-
writable: true,
|
|
125
|
-
value: current
|
|
126
|
-
? spec => execute({
|
|
127
|
-
program: spec.program,
|
|
128
|
-
bindings: spec.bindings,
|
|
129
|
-
signal: spec.signal,
|
|
130
|
-
})
|
|
131
|
-
: request => execute(request),
|
|
132
|
-
})
|
|
133
|
-
for (const name of WITHHELD_DESCRIPTORS) {
|
|
134
|
-
requested.set(name, { configurable: true, value: name === 'executionInstructions' ? '' : undefined })
|
|
135
|
-
}
|
|
136
|
-
const extensible = Object.isExtensible(service)
|
|
137
|
-
for (const [name, descriptor] of requested) {
|
|
138
|
-
const previous = Object.getOwnPropertyDescriptor(service, name)
|
|
139
|
-
saved.set(name, previous)
|
|
140
|
-
const next = { enumerable: previous?.enumerable ?? false, ...descriptor }
|
|
141
|
-
requested.set(name, next)
|
|
142
|
-
const probe = {}
|
|
143
|
-
if (previous !== undefined) Object.defineProperty(probe, name, previous)
|
|
144
|
-
if (!extensible) Object.preventExtensions(probe)
|
|
145
|
-
Object.defineProperty(probe, name, next)
|
|
146
|
-
}
|
|
147
|
-
try {
|
|
148
|
-
for (const [name, descriptor] of requested) {
|
|
149
|
-
Object.defineProperty(service, name, descriptor)
|
|
150
|
-
written.set(name, Object.getOwnPropertyDescriptor(service, name))
|
|
151
|
-
}
|
|
152
|
-
} catch (error) {
|
|
153
|
-
for (const name of [...written.keys()].reverse()) {
|
|
154
|
-
if (!descriptorsMatch(Object.getOwnPropertyDescriptor(service, name), written.get(name))) continue
|
|
155
|
-
const descriptor = saved.get(name)
|
|
156
|
-
if (descriptor === undefined) delete service[name]
|
|
157
|
-
else Object.defineProperty(service, name, descriptor)
|
|
158
|
-
}
|
|
159
|
-
throw error
|
|
160
|
-
}
|
|
99
|
+
runUpstream(spec) {
|
|
100
|
+
requireCompatible()
|
|
101
|
+
return upstreamRun.call(service, spec)
|
|
102
|
+
},
|
|
103
|
+
installExecute(execute) {
|
|
104
|
+
requireAvailable()
|
|
105
|
+
if (typeof execute !== 'function') throw new TypeError('ptc-plus: execution entry must be a function')
|
|
106
|
+
if (installed !== undefined) throw new Error('ptc-plus: execution seam already installed')
|
|
107
|
+
const entry = { execute, language: service.language }
|
|
108
|
+
installed = entry
|
|
161
109
|
const restore = () => {
|
|
162
|
-
if (installed !==
|
|
110
|
+
if (installed !== entry) return
|
|
163
111
|
installed = undefined
|
|
164
|
-
for (const [name, descriptor] of saved) {
|
|
165
|
-
if (!descriptorsMatch(Object.getOwnPropertyDescriptor(service, name), written.get(name))) continue
|
|
166
|
-
if (descriptor === undefined) delete service[name]
|
|
167
|
-
else Object.defineProperty(service, name, descriptor)
|
|
168
|
-
}
|
|
169
112
|
}
|
|
170
|
-
installed = restore
|
|
171
113
|
return restore
|
|
172
114
|
},
|
|
115
|
+
retire() {
|
|
116
|
+
available = false
|
|
117
|
+
installed = undefined
|
|
118
|
+
sourceListeners.clear()
|
|
119
|
+
},
|
|
173
120
|
})
|
|
121
|
+
return seam
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function registeredExecutionSeam(service, serviceName) {
|
|
125
|
+
const seam = createExecutionSeam(service, serviceName)
|
|
126
|
+
if (typeof service[PLUGIN_EXECUTION_SEAM] !== 'function') {
|
|
127
|
+
throw new Error(`ptc-plus: ${serviceName} requires the public execution-provider bundle composition`)
|
|
128
|
+
}
|
|
129
|
+
return seam
|
|
174
130
|
}
|
|
175
131
|
|
|
176
132
|
/**
|
|
@@ -259,7 +215,7 @@ export function installExecutionSeam(ctx, {
|
|
|
259
215
|
entry = { name, scope }
|
|
260
216
|
live = entry
|
|
261
217
|
registerLifecycle(entry, name, scope)
|
|
262
|
-
const attached = attach(scope,
|
|
218
|
+
const attached = attach(scope, registeredExecutionSeam(scope?.[name] ?? readService(ctx, name), name))
|
|
263
219
|
entry.attached = attached
|
|
264
220
|
return attached
|
|
265
221
|
})
|
|
@@ -292,7 +248,7 @@ export function installExecutionSeam(ctx, {
|
|
|
292
248
|
const entry = { name, scope }
|
|
293
249
|
live = entry
|
|
294
250
|
try {
|
|
295
|
-
entry.attached = attach(scope,
|
|
251
|
+
entry.attached = attach(scope, registeredExecutionSeam(scope?.[name] ?? readService(ctx, name), name))
|
|
296
252
|
} catch (error) {
|
|
297
253
|
live = undefined
|
|
298
254
|
throw error
|
|
@@ -15,6 +15,17 @@ const ISOLATED_STOP_MS = 5000
|
|
|
15
15
|
/** Bound on the helper's output pipes really ending after its process exited. */
|
|
16
16
|
const ISOLATED_OUTPUT_MS = 5000
|
|
17
17
|
|
|
18
|
+
export function isolatedWorkerEntry(entry) {
|
|
19
|
+
if (!(entry instanceof URL) && (typeof entry !== 'string' || entry.length === 0)) {
|
|
20
|
+
throw new TypeError('isolated worker entry must be a URL or a non-empty string filename')
|
|
21
|
+
}
|
|
22
|
+
return entry instanceof URL
|
|
23
|
+
? entry.href
|
|
24
|
+
: entry.slice(0, 5).toLowerCase() === 'file:'
|
|
25
|
+
? new URL(entry).href
|
|
26
|
+
: pathToFileURL(resolve(entry)).href
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
/**
|
|
19
30
|
* A worker-shaped transport whose worker lives in a killable helper process.
|
|
20
31
|
*
|
|
@@ -29,17 +40,10 @@ export class IsolatedWorker extends EventEmitter {
|
|
|
29
40
|
super()
|
|
30
41
|
// The entry follows the worker constructor's contract: a URL or a string
|
|
31
42
|
// filename. Anything else fails here, before a helper process exists.
|
|
32
|
-
if (!(entry instanceof URL) && (typeof entry !== 'string' || entry.length === 0)) {
|
|
33
|
-
throw new TypeError('isolated worker entry must be a URL or a non-empty string filename')
|
|
34
|
-
}
|
|
35
43
|
// The init message crosses advanced IPC, which cannot rebuild a URL object, so
|
|
36
44
|
// the boundary normalizes it to one absolute file URL string. A path follows
|
|
37
45
|
// the worker constructor's rule: resolved from the current working directory.
|
|
38
|
-
const entrySpec = entry
|
|
39
|
-
? entry.href
|
|
40
|
-
: entry.slice(0, 5).toLowerCase() === 'file:'
|
|
41
|
-
? new URL(entry).href
|
|
42
|
-
: pathToFileURL(resolve(entry)).href
|
|
46
|
+
const entrySpec = isolatedWorkerEntry(entry)
|
|
43
47
|
this.stdout = new PassThrough()
|
|
44
48
|
this.stderr = new PassThrough()
|
|
45
49
|
this.protocol = protocol
|