dsh-creator-mode-plus 0.3.8 → 0.3.12
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/AGENTS.md +19 -3
- package/CHANGELOG.md +42 -1
- package/README.en.md +29 -5
- package/README.md +29 -5
- package/cordis.patch.yml +3 -0
- package/docs/bridge-contract.md +27 -39
- package/docs/development-compilation.md +100 -0
- package/docs/development-integration-plan.md +199 -0
- package/docs/development-pre-wiring-baseline.md +26 -0
- package/docs/dshx-v0.7-alignment.md +25 -10
- package/docs/executor-authorization-design.md +416 -0
- package/dshx.yml +7 -0
- package/package.json +17 -3
- package/preset/skills/creator-mode-plus/SKILL.md +19 -2
- package/preset/skills/creator-mode-plus/maintenance.md +9 -12
- package/scripts/install.mjs +225 -7
- package/scripts/verify-dshx.mjs +1 -1
- package/scripts/verify-harness-install.mjs +10 -8
- package/src/client.js +1 -1
- package/src/compatibility.js +19 -7
- package/src/core-boundary.js +151 -0
- package/src/delivery.js +1 -1
- package/src/desktop-profile.js +95 -0
- package/src/development-execution.js +237 -0
- package/src/development-invocation.js +267 -0
- package/src/development-policy.js +216 -0
- package/src/development-target.js +569 -0
- package/src/development-tasks.js +342 -0
- package/src/index.js +102 -35
- package/src/runner.js +43 -25
- package/src/safety.js +50 -4
- package/src/takeover.js +3 -3
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, read-only development-target observations for Creator task binding.
|
|
3
|
+
*
|
|
4
|
+
* Host code supplies getHarnessRoot; model/tool arguments MUST NOT supply it.
|
|
5
|
+
* This module does not grant permission, resolve live Loader rows, lock source
|
|
6
|
+
* trees, or authenticate arbitrary same-process callers. A successful revalidate
|
|
7
|
+
* is only a parent-side observation. The fixed DSHX child contract accepts no
|
|
8
|
+
* expected digest/source inode: bytes may still change before the child reads or
|
|
9
|
+
* executes them. Post-execution receipts cannot turn that window into prevention.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
closeSync, constants, fstatSync, lstatSync, openSync, readlinkSync, readSync,
|
|
13
|
+
} from 'node:fs'
|
|
14
|
+
import { createHash } from 'node:crypto'
|
|
15
|
+
import { dirname, isAbsolute, join, parse as parsePath, relative, resolve, sep } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
import { types as utilTypes } from 'node:util'
|
|
18
|
+
import { isAlias, isMap, isSeq, parseDocument } from 'yaml'
|
|
19
|
+
|
|
20
|
+
export const DEVELOPMENT_TARGET_ENGINE = 'creator-plus-v1'
|
|
21
|
+
export const DEVELOPMENT_TARGET_LIMITS = Object.freeze({
|
|
22
|
+
pathBytes: 4096,
|
|
23
|
+
relativePathBytes: 1024,
|
|
24
|
+
pathComponents: 64,
|
|
25
|
+
componentVisits: 512,
|
|
26
|
+
symlinks: 16,
|
|
27
|
+
artifacts: 32,
|
|
28
|
+
metadataBytes: 256 * 1024,
|
|
29
|
+
artifactBytes: 4 * 1024 * 1024,
|
|
30
|
+
totalBytes: 16 * 1024 * 1024,
|
|
31
|
+
yamlNodes: 8192,
|
|
32
|
+
yamlDepth: 32,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Per resolver/producer: bounded live inputs, a hard real timer, and a conservative
|
|
36
|
+
// aggregate retained-code budget in addition to each existing 16 MiB/32-file read.
|
|
37
|
+
export const DEVELOPMENT_COMPILATION_LIMITS = Object.freeze({ pending: 64, ttlMs: 5 * 60_000, retainedBytes: 16 * 1024 * 1024 })
|
|
38
|
+
const signal = value => value instanceof AbortSignal
|
|
39
|
+
const synchronous = (value, code) => {
|
|
40
|
+
if (utilTypes.isPromise(value)) { void Promise.prototype.then.call(value, undefined, () => {}); fail(code, 'A synchronous private reader is required') }
|
|
41
|
+
if (typeof value?.then === 'function') fail(code, 'A synchronous private reader is required')
|
|
42
|
+
return value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const CONTROL_IDS = Object.freeze([
|
|
46
|
+
'dsh-creator-mode-plus', 'dsh-creator-mode', 'creator-mode-plus',
|
|
47
|
+
'dsh-approve-for-me', 'dsh-auto-review', 'dsh-external-plugin-devkit',
|
|
48
|
+
'dsh-creator-bridge', 'dsh-guardian', 'dsh-user-approval', 'dshx',
|
|
49
|
+
])
|
|
50
|
+
const CORE_DIRECTORIES = ['apps', 'packages', 'vendor', 'tools', 'native', 'scripts', 'website', 'node_modules', '.git']
|
|
51
|
+
const OWN_PACKAGE = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
52
|
+
const PLUGIN_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
|
|
53
|
+
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/
|
|
54
|
+
const RUNTIME_FILE = /\.(?:[cm]?[jt]sx?)$/
|
|
55
|
+
const DECLARATION_FILE = /\.d\.(?:[cm]?ts)$/
|
|
56
|
+
|
|
57
|
+
export class DevelopmentTargetError extends Error {
|
|
58
|
+
constructor(code, message, cause) {
|
|
59
|
+
super(message, cause === undefined ? undefined : { cause })
|
|
60
|
+
this.name = 'DevelopmentTargetError'
|
|
61
|
+
this.code = code
|
|
62
|
+
this.eligible = false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function fail(code, message) { throw new DevelopmentTargetError(code, message) }
|
|
67
|
+
function within(path, root) {
|
|
68
|
+
const rel = relative(root, path)
|
|
69
|
+
return rel === '' || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`))
|
|
70
|
+
}
|
|
71
|
+
function overlaps(a, b) { return within(a, b) || within(b, a) }
|
|
72
|
+
function protectedId(id) { return CONTROL_IDS.some(control => id === control || id.startsWith(`${control}-`)) }
|
|
73
|
+
function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
|
|
74
|
+
function freeze(value) {
|
|
75
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
76
|
+
for (const child of Object.values(value)) freeze(child)
|
|
77
|
+
Object.freeze(value)
|
|
78
|
+
}
|
|
79
|
+
return value
|
|
80
|
+
}
|
|
81
|
+
function signature(value) { return JSON.stringify(value) }
|
|
82
|
+
function identity(stat) {
|
|
83
|
+
if (stat.dev < 0n || stat.ino <= 0n) fail('UNSUPPORTED_IDENTITY', 'Source directory identity must have nonnegative dev and positive ino')
|
|
84
|
+
return { dev: stat.dev.toString(10), ino: stat.ino.toString(10) }
|
|
85
|
+
}
|
|
86
|
+
function fileIdentity(stat) {
|
|
87
|
+
return { ...identity(stat), size: stat.size.toString(10), mtimeNs: stat.mtimeNs.toString(10), ctimeNs: stat.ctimeNs.toString(10) }
|
|
88
|
+
}
|
|
89
|
+
function absolutePath(value, label) {
|
|
90
|
+
if (typeof value !== 'string' || value.length === 0 || !isAbsolute(value)
|
|
91
|
+
|| CONTROL_CHARACTERS.test(value) || Buffer.byteLength(value) > DEVELOPMENT_TARGET_LIMITS.pathBytes) {
|
|
92
|
+
fail('INVALID_PATH', `${label} must be a bounded absolute filesystem path`)
|
|
93
|
+
}
|
|
94
|
+
// Preserve dot components until the filesystem walk: lexical normalization
|
|
95
|
+
// before resolving a symlink would give the wrong meaning to link/../target.
|
|
96
|
+
if (value.split(sep).filter(Boolean).length > DEVELOPMENT_TARGET_LIMITS.pathComponents) fail('PATH_LIMIT', `${label} has too many components`)
|
|
97
|
+
return value
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Resolve normalized outer links with explicit link/visit limits and an identity trail. */
|
|
101
|
+
function directoryPath(value, label) {
|
|
102
|
+
let target = absolutePath(value, label)
|
|
103
|
+
if (target.split(sep).some(part => part === '.' || part === '..')) fail('AMBIGUOUS_LINK_PATH', `${label} must not contain dot traversal`)
|
|
104
|
+
let cursor = parsePath(target).root
|
|
105
|
+
let parts = target.slice(cursor.length).split(sep).filter(Boolean)
|
|
106
|
+
let visits = 0
|
|
107
|
+
let links = 0
|
|
108
|
+
const trail = []
|
|
109
|
+
let ancestors = []
|
|
110
|
+
while (parts.length) {
|
|
111
|
+
if (++visits > DEVELOPMENT_TARGET_LIMITS.componentVisits) fail('PATH_LIMIT', `${label} exceeded its component traversal budget`)
|
|
112
|
+
const part = parts.shift()
|
|
113
|
+
if (part === '.') continue
|
|
114
|
+
if (part === '..') { cursor = dirname(cursor); ancestors.pop(); continue }
|
|
115
|
+
const path = join(cursor, part)
|
|
116
|
+
const stat = lstatSync(path, { bigint: true })
|
|
117
|
+
if (stat.isSymbolicLink()) {
|
|
118
|
+
if (++links > DEVELOPMENT_TARGET_LIMITS.symlinks) fail('LINK_LIMIT', `${label} exceeded its symlink budget`)
|
|
119
|
+
const link = readlinkSync(path)
|
|
120
|
+
if (CONTROL_CHARACTERS.test(link) || Buffer.byteLength(link) > DEVELOPMENT_TARGET_LIMITS.pathBytes) fail('INVALID_PATH', `${label} has an invalid symlink target`)
|
|
121
|
+
// Node's JS realpathSync (used by DSHX) and kernel resolution can disagree
|
|
122
|
+
// on named-link/../target. Accept leading relative ../ only; never approve
|
|
123
|
+
// one interpretation while the child might select another interpretation.
|
|
124
|
+
let namedComponent = isAbsolute(link)
|
|
125
|
+
for (const component of link.split(sep).filter(Boolean)) {
|
|
126
|
+
if (component === '.' || component === '..') {
|
|
127
|
+
if (namedComponent) fail('AMBIGUOUS_LINK_PATH', `${label} has a non-normalized symlink target`)
|
|
128
|
+
} else namedComponent = true
|
|
129
|
+
}
|
|
130
|
+
trail.push({ path, kind: 'link', ...identity(stat), target: link })
|
|
131
|
+
const linked = isAbsolute(link) ? link : `${dirname(path)}${sep}${link}`
|
|
132
|
+
target = absolutePath(`${linked}${parts.length ? `${sep}${parts.join(sep)}` : ''}`, label)
|
|
133
|
+
cursor = parsePath(target).root
|
|
134
|
+
parts = target.slice(cursor.length).split(sep).filter(Boolean)
|
|
135
|
+
ancestors = []
|
|
136
|
+
} else {
|
|
137
|
+
if (!stat.isDirectory()) fail('NOT_DIRECTORY', `${label} traverses a non-directory`)
|
|
138
|
+
trail.push({ path, kind: 'directory', ...identity(stat) })
|
|
139
|
+
ancestors.push(identity(stat))
|
|
140
|
+
cursor = path
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const stat = lstatSync(cursor, { bigint: true })
|
|
144
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) fail('NOT_DIRECTORY', `${label} is not a canonical directory`)
|
|
145
|
+
return { path: cursor, identity: identity(stat), trail, ancestors }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function exactRelative(value, runtime = true) {
|
|
149
|
+
if (typeof value !== 'string' || value.length === 0 || Buffer.byteLength(value) > DEVELOPMENT_TARGET_LIMITS.relativePathBytes
|
|
150
|
+
|| isAbsolute(value) || /^[A-Za-z]:/.test(value) || value.includes('\\') || CONTROL_CHARACTERS.test(value)
|
|
151
|
+
|| /[*?{}[\]!()]/.test(value)) fail('INVALID_ARTIFACT_PATH', 'Entry/artifact must be an exact bounded package-relative path')
|
|
152
|
+
const parts = value.split('/')
|
|
153
|
+
if (parts.length > DEVELOPMENT_TARGET_LIMITS.pathComponents
|
|
154
|
+
|| parts.some(part => part === '' || part === '.' || part === '..' || part === 'node_modules' || part === '.git')) {
|
|
155
|
+
fail('INVALID_ARTIFACT_PATH', 'Entry/artifact cannot traverse directories, dependencies, or VCS metadata')
|
|
156
|
+
}
|
|
157
|
+
if (runtime && (!RUNTIME_FILE.test(value) || DECLARATION_FILE.test(value))) fail('INVALID_ARTIFACT_PATH', 'Entry/artifact must be a runtime JavaScript or TypeScript file')
|
|
158
|
+
return value
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** No symlink is accepted in ANY component below the canonical source root. */
|
|
162
|
+
function regularPath(root, relativePath) {
|
|
163
|
+
exactRelative(relativePath, false)
|
|
164
|
+
let cursor = root
|
|
165
|
+
const parts = relativePath.split('/')
|
|
166
|
+
const trail = []
|
|
167
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
168
|
+
cursor = join(cursor, parts[index])
|
|
169
|
+
const stat = lstatSync(cursor, { bigint: true })
|
|
170
|
+
if (stat.isSymbolicLink()) fail('SYMLINK_FORBIDDEN', `Metadata/artifact cannot traverse a symlink: ${relativePath}`)
|
|
171
|
+
const final = index === parts.length - 1
|
|
172
|
+
if (final ? !stat.isFile() : !stat.isDirectory()) fail('NOT_REGULAR_FILE', `Metadata/artifact is not a regular contained file: ${relativePath}`)
|
|
173
|
+
trail.push({ path: cursor, kind: final ? 'file' : 'directory', ...(final ? fileIdentity(stat) : identity(stat)) })
|
|
174
|
+
}
|
|
175
|
+
return { path: cursor, trail }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readBounded(root, path, role, budget, retainText = false) {
|
|
179
|
+
if (typeof constants.O_NOFOLLOW !== 'number' || typeof constants.O_NONBLOCK !== 'number') fail('UNSUPPORTED_SAFE_READ', 'This platform lacks no-follow/nonblocking file opens')
|
|
180
|
+
const beforePath = regularPath(root, path)
|
|
181
|
+
const fd = openSync(beforePath.path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK)
|
|
182
|
+
try {
|
|
183
|
+
const before = fstatSync(fd, { bigint: true })
|
|
184
|
+
if (!before.isFile()) fail('NOT_REGULAR_FILE', `Not a regular opened file: ${path}`)
|
|
185
|
+
const opened = fileIdentity(before)
|
|
186
|
+
const expected = beforePath.trail.at(-1)
|
|
187
|
+
if (Object.keys(opened).some(key => opened[key] !== expected[key])) fail('TARGET_CHANGED', `File changed before opening: ${path}`)
|
|
188
|
+
const limit = Math.min(role === 'metadata' ? DEVELOPMENT_TARGET_LIMITS.metadataBytes : DEVELOPMENT_TARGET_LIMITS.artifactBytes, budget.remaining)
|
|
189
|
+
if (before.size > BigInt(limit)) fail('BYTE_LIMIT', `Metadata/artifact exceeds its byte budget: ${path}`)
|
|
190
|
+
const hash = createHash('sha256')
|
|
191
|
+
const chunks = []
|
|
192
|
+
const buffer = Buffer.alloc(64 * 1024)
|
|
193
|
+
let bytes = 0
|
|
194
|
+
while (true) {
|
|
195
|
+
const count = readSync(fd, buffer, 0, Math.min(buffer.length, limit + 1 - bytes), null)
|
|
196
|
+
if (count === 0) break
|
|
197
|
+
bytes += count
|
|
198
|
+
if (bytes > limit) fail('BYTE_LIMIT', `Metadata/artifact grew beyond its byte budget: ${path}`)
|
|
199
|
+
hash.update(buffer.subarray(0, count))
|
|
200
|
+
if (role === 'metadata' || retainText) chunks.push(Buffer.from(buffer.subarray(0, count)))
|
|
201
|
+
}
|
|
202
|
+
const after = fstatSync(fd, { bigint: true })
|
|
203
|
+
const afterPath = regularPath(root, path)
|
|
204
|
+
if (signature(fileIdentity(after)) !== signature(opened) || signature(afterPath.trail) !== signature(beforePath.trail)
|
|
205
|
+
|| BigInt(bytes) !== before.size) fail('TARGET_CHANGED', `File changed while reading: ${path}`)
|
|
206
|
+
budget.remaining -= bytes
|
|
207
|
+
return {
|
|
208
|
+
file: { path, role, bytes, sha256: hash.digest('hex') },
|
|
209
|
+
trail: beforePath.trail,
|
|
210
|
+
...((role === 'metadata' || retainText) ? { text: new TextDecoder('utf-8', { fatal: true, ignoreBOM: retainText }).decode(Buffer.concat(chunks)) } : {}),
|
|
211
|
+
}
|
|
212
|
+
} finally { closeSync(fd) }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function manifestFromYaml(text) {
|
|
216
|
+
const document = parseDocument(text, { version: '1.2', schema: 'core', strict: true, uniqueKeys: true, prettyErrors: false })
|
|
217
|
+
if (document.errors.length || document.warnings.length) fail('INVALID_MANIFEST', 'dshx.yml must be unambiguous core-schema YAML')
|
|
218
|
+
const stack = [{ node: document.contents, depth: 0 }]
|
|
219
|
+
let count = 0
|
|
220
|
+
while (stack.length) {
|
|
221
|
+
const { node, depth } = stack.pop()
|
|
222
|
+
if (++count > DEVELOPMENT_TARGET_LIMITS.yamlNodes || depth > DEVELOPMENT_TARGET_LIMITS.yamlDepth) fail('MANIFEST_LIMIT', 'dshx.yml exceeds the YAML structure budget')
|
|
223
|
+
if (isAlias(node)) fail('INVALID_MANIFEST', 'dshx.yml aliases are not accepted')
|
|
224
|
+
if (isMap(node)) {
|
|
225
|
+
for (const pair of node.items) stack.push({ node: pair.key, depth: depth + 1 }, { node: pair.value, depth: depth + 1 })
|
|
226
|
+
} else if (isSeq(node)) {
|
|
227
|
+
for (const child of node.items) stack.push({ node: child, depth: depth + 1 })
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const value = document.toJS({ maxAliasCount: 0 })
|
|
231
|
+
if (!record(value)) fail('INVALID_MANIFEST', 'dshx.yml must be an object')
|
|
232
|
+
return value
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function readSnapshot(sourceRoot, pluginId, { runtimeEntry, retainCode = false } = {}) {
|
|
236
|
+
const budget = { remaining: DEVELOPMENT_TARGET_LIMITS.totalBytes }
|
|
237
|
+
const pkg = readBounded(sourceRoot, 'package.json', 'metadata', budget)
|
|
238
|
+
const yml = readBounded(sourceRoot, 'dshx.yml', 'metadata', budget)
|
|
239
|
+
let packageManifest
|
|
240
|
+
try { packageManifest = JSON.parse(pkg.text) } catch { fail('INVALID_MANIFEST', 'package.json must contain valid JSON') }
|
|
241
|
+
if (!record(packageManifest) || packageManifest.name !== pluginId) fail('IDENTITY_MISMATCH', 'package.json name must exactly match the selected plugin id')
|
|
242
|
+
const manifest = manifestFromYaml(yml.text)
|
|
243
|
+
if (manifest.id !== pluginId || manifest.profile !== 'web') fail('IDENTITY_MISMATCH', 'dshx.yml must explicitly match the selected id and web profile')
|
|
244
|
+
const entry = exactRelative(manifest.entry)
|
|
245
|
+
if (manifest.hotReload !== undefined && (!record(manifest.hotReload) || Object.keys(manifest.hotReload).some(key => key !== 'artifacts'))) {
|
|
246
|
+
fail('INVALID_MANIFEST', 'hotReload accepts only an artifacts field')
|
|
247
|
+
}
|
|
248
|
+
const configured = manifest.hotReload?.artifacts
|
|
249
|
+
const artifacts = configured === undefined ? [entry] : configured
|
|
250
|
+
if (!Array.isArray(artifacts) || artifacts.length < 1 || artifacts.length > DEVELOPMENT_TARGET_LIMITS.artifacts) fail('ARTIFACT_LIMIT', 'hotReload.artifacts must contain 1 to 32 files')
|
|
251
|
+
const checked = artifacts.map(path => exactRelative(path))
|
|
252
|
+
if (new Set(checked).size !== checked.length || !checked.includes(entry)) fail('INVALID_MANIFEST', 'hotReload.artifacts must be unique and include the declared entry')
|
|
253
|
+
const compilation = runtimeEntry !== undefined
|
|
254
|
+
if (compilation && configured === undefined) fail('COMPILATION_PROFILE_UNSUPPORTED', 'Compilation requires an explicit hotReload.artifacts closure')
|
|
255
|
+
const paths = compilation ? [...new Set([...checked, exactRelative(runtimeEntry)])] : checked
|
|
256
|
+
if (paths.length > DEVELOPMENT_TARGET_LIMITS.artifacts) fail('ARTIFACT_LIMIT', 'Declared artifacts plus registered runtimeEntry exceed 32 files')
|
|
257
|
+
if (compilation && paths.some(path => !/\.(?:[cm]?js)$/.test(path))) fail('COMPILATION_PROFILE_UNSUPPORTED', 'Only an explicit JavaScript closure is supported; no TS/JSX/build or Loader inference')
|
|
258
|
+
const runtime = [...paths].sort().map(path => readBounded(sourceRoot, path, 'artifact', budget, retainCode))
|
|
259
|
+
const all = [pkg, yml, ...runtime]
|
|
260
|
+
return {
|
|
261
|
+
observed: {
|
|
262
|
+
entry, profile: 'web', declaredArtifacts: [...checked],
|
|
263
|
+
...(compilation ? { declaredEntry: entry, resolvedRuntimeEntry: runtimeEntry } : {}),
|
|
264
|
+
files: all.map(item => item.file),
|
|
265
|
+
trails: all.map(item => item.trail),
|
|
266
|
+
},
|
|
267
|
+
...(retainCode ? { code: runtime.map(item => ({ path: item.file.path, code: item.text, sha256: item.file.sha256, bytes: item.file.bytes })) } : {}),
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function hasIdentity(ancestors, target) {
|
|
272
|
+
return ancestors.some(item => item.dev === target.dev && item.ino === target.ino)
|
|
273
|
+
}
|
|
274
|
+
function assertEligible(source, host, extensionsIdentity, pluginId) {
|
|
275
|
+
const sourceRoot = source.path
|
|
276
|
+
const harnessRoot = host.path
|
|
277
|
+
const extensions = join(harnessRoot, 'my-plugins')
|
|
278
|
+
// Identity ancestry also protects case-insensitive / normalization-insensitive
|
|
279
|
+
// filesystems: a differently spelled RUNTIME path is still the same core.
|
|
280
|
+
if (hasIdentity(host.ancestors, source.identity)
|
|
281
|
+
|| hasIdentity([extensionsIdentity], source.identity)
|
|
282
|
+
|| (hasIdentity(source.ancestors, host.identity) && !hasIdentity(source.ancestors, extensionsIdentity))
|
|
283
|
+
|| sourceRoot === extensions || (within(sourceRoot, harnessRoot) && !within(sourceRoot, extensions))) {
|
|
284
|
+
fail('PROTECTED_TARGET', 'Only my-plugins packages, not other Harness checkout directories, are development targets')
|
|
285
|
+
}
|
|
286
|
+
if (protectedId(pluginId) || sourceRoot.split(sep).some(part => protectedId(part.toLowerCase()))) fail('PROTECTED_TARGET', 'Control infrastructure is not eligible for remembered development authorization')
|
|
287
|
+
if (sourceRoot.split(sep).some(part => ['node_modules', '.git', '.dsh', '.agent-presets'].includes(part.toLowerCase()))) fail('PROTECTED_TARGET', 'Dependency, profile, or VCS storage is not a development target')
|
|
288
|
+
const control = directoryPath(OWN_PACKAGE, 'Creator control source')
|
|
289
|
+
if (CORE_DIRECTORIES.some(name => overlaps(sourceRoot, join(harnessRoot, name))) || overlaps(sourceRoot, OWN_PACKAGE)
|
|
290
|
+
|| hasIdentity(source.ancestors, control.identity) || hasIdentity(control.ancestors, source.identity)) {
|
|
291
|
+
fail('PROTECTED_TARGET', 'Harness core and Creator control sources are not eligible development targets')
|
|
292
|
+
}
|
|
293
|
+
return control.trail
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* getHarnessRoot is a synchronous Host-owned resolver, NOT a model parameter.
|
|
298
|
+
* capture failures throw DevelopmentTargetError { eligible:false, code }.
|
|
299
|
+
* Snapshots are resolver-issued object capabilities for revalidation, not grants.
|
|
300
|
+
* External source roots are supported; workspaceRoot binds the actual Agent cwd
|
|
301
|
+
* without falsely requiring that every claimed external plugin lives inside it.
|
|
302
|
+
*/
|
|
303
|
+
export function createDevelopmentTargetResolver({ getHarnessRoot, readRegisteredTarget, readCompilationSignals, producerSignal,
|
|
304
|
+
compilationTtlMs = DEVELOPMENT_COMPILATION_LIMITS.ttlMs, compilationNow = () => performance.now(),
|
|
305
|
+
} = {}) {
|
|
306
|
+
if (typeof getHarnessRoot !== 'function') throw new TypeError('A Host-owned getHarnessRoot callback is required')
|
|
307
|
+
const issued = new WeakMap(), compilations = new WeakMap(), pending = new Set()
|
|
308
|
+
let compilationDisposed = false, capturing = false, retainedBytes = 0, lastClock = 0
|
|
309
|
+
function locate(exec, pluginId) {
|
|
310
|
+
if (typeof pluginId !== 'string' || pluginId.length > 128 || !PLUGIN_ID.test(pluginId)) fail('INVALID_PLUGIN_ID', 'Select one bounded lower-case kebab-case plugin id, not a path')
|
|
311
|
+
const agent = exec?.agent
|
|
312
|
+
if (!agent || typeof agent !== 'object') fail('MISSING_AGENT', 'The real execution Agent is required')
|
|
313
|
+
const host = directoryPath(getHarnessRoot(), 'Host Harness root')
|
|
314
|
+
const workspace = directoryPath(agent.session?.header?.cwd, 'Agent session cwd')
|
|
315
|
+
// DSHX resolvePluginDir(root, id) tries root/id before my-plugins/id.
|
|
316
|
+
// Do not approve the latter while a same-name checkout entry can shadow it.
|
|
317
|
+
let shadow
|
|
318
|
+
try { shadow = lstatSync(join(host.path, pluginId), { bigint: true }) } catch (error) {
|
|
319
|
+
if (error?.code !== 'ENOENT') throw error
|
|
320
|
+
}
|
|
321
|
+
if (shadow) fail('AMBIGUOUS_PLUGIN_ROOT', 'A same-name Harness-root entry can shadow the selected my-plugins target')
|
|
322
|
+
const base = join(host.path, 'my-plugins')
|
|
323
|
+
const baseStat = lstatSync(base, { bigint: true })
|
|
324
|
+
if (baseStat.isSymbolicLink() || !baseStat.isDirectory()) fail('INVALID_PLUGIN_ROOT', 'Harness my-plugins must be a real directory')
|
|
325
|
+
const source = directoryPath(join(base, pluginId), 'my-plugins target')
|
|
326
|
+
const controlTrail = assertEligible(source, host, identity(baseStat), pluginId)
|
|
327
|
+
return {
|
|
328
|
+
agent,
|
|
329
|
+
binding: {
|
|
330
|
+
engine: DEVELOPMENT_TARGET_ENGINE,
|
|
331
|
+
harnessRoot: host.path,
|
|
332
|
+
workspaceRoot: workspace.path,
|
|
333
|
+
sourceRoot: source.path,
|
|
334
|
+
pluginId,
|
|
335
|
+
sourceDirectoryIdentity: source.identity,
|
|
336
|
+
},
|
|
337
|
+
route: { host: host.trail, workspace: workspace.trail, base: identity(baseStat), source: source.trail, control: controlTrail },
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function observe(exec, pluginId) {
|
|
341
|
+
const first = locate(exec, pluginId)
|
|
342
|
+
const firstFiles = readSnapshot(first.binding.sourceRoot, pluginId).observed
|
|
343
|
+
// A bounded second full observation catches changes during metadata parsing
|
|
344
|
+
// and hashing. Neither pass is an atomic filesystem snapshot or child lease.
|
|
345
|
+
const second = locate(exec, pluginId)
|
|
346
|
+
const secondFiles = readSnapshot(second.binding.sourceRoot, pluginId).observed
|
|
347
|
+
const firstProof = { binding: first.binding, route: first.route, files: firstFiles }
|
|
348
|
+
const secondProof = { binding: second.binding, route: second.route, files: secondFiles }
|
|
349
|
+
if (signature(firstProof) !== signature(secondProof)) fail('TARGET_CHANGED', 'Development target changed while capturing its snapshot')
|
|
350
|
+
return { ...first, observed: firstFiles, proof: signature(firstProof) }
|
|
351
|
+
}
|
|
352
|
+
function capture(exec, pluginId) {
|
|
353
|
+
try {
|
|
354
|
+
const observed = observe(exec, pluginId)
|
|
355
|
+
const snapshot = freeze({
|
|
356
|
+
eligible: true,
|
|
357
|
+
binding: observed.binding,
|
|
358
|
+
entry: observed.observed.entry,
|
|
359
|
+
profile: observed.observed.profile,
|
|
360
|
+
declaredArtifacts: observed.observed.declaredArtifacts,
|
|
361
|
+
files: observed.observed.files,
|
|
362
|
+
evidenceBoundary: 'parent-observation-only',
|
|
363
|
+
})
|
|
364
|
+
issued.set(snapshot, { agent: observed.agent, proof: observed.proof, pluginId })
|
|
365
|
+
return snapshot
|
|
366
|
+
} catch (error) {
|
|
367
|
+
if (error instanceof DevelopmentTargetError) throw error
|
|
368
|
+
throw new DevelopmentTargetError('TARGET_UNAVAILABLE', 'Development target could not be safely observed', error)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function revalidate(snapshot, exec) {
|
|
372
|
+
const prior = snapshot && typeof snapshot === 'object' ? issued.get(snapshot) : undefined
|
|
373
|
+
if (!prior) fail('UNISSUED_SNAPSHOT', 'Revalidation requires this resolver\'s original snapshot object')
|
|
374
|
+
if (prior.compilationHandle) { validateCompilation(prior.compilationHandle, exec); return snapshot }
|
|
375
|
+
if (exec?.agent !== prior.agent) fail('AGENT_MISMATCH', 'A snapshot cannot move to another Agent')
|
|
376
|
+
try {
|
|
377
|
+
const current = observe(exec, prior.pluginId)
|
|
378
|
+
if (current.proof !== prior.proof) fail('TARGET_CHANGED', 'Development target no longer matches the captured snapshot')
|
|
379
|
+
return snapshot
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (error instanceof DevelopmentTargetError && error.code === 'TARGET_CHANGED') throw error
|
|
382
|
+
throw new DevelopmentTargetError('TARGET_CHANGED', 'Development target is unavailable or changed since capture', error)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Compilation is an opt-in PRIVATE composition, not a public model argument.
|
|
386
|
+
// The registry reader resolves an exact opaque registered target lease. The
|
|
387
|
+
// signal reader resolves the same real exec -> already-entered C owner/policy
|
|
388
|
+
// witnesses; it must never look up a newest owner by Agent/session identifier.
|
|
389
|
+
function compilationReady() {
|
|
390
|
+
if (compilationDisposed) fail('COMPILATION_DISPOSED', 'Compilation capture was disposed')
|
|
391
|
+
if (typeof readRegisteredTarget !== 'function' || typeof readCompilationSignals !== 'function' || !signal(producerSignal)
|
|
392
|
+
|| utilTypes.isAsyncFunction(readRegisteredTarget) || utilTypes.isAsyncFunction(readCompilationSignals)
|
|
393
|
+
|| typeof compilationNow !== 'function' || !Number.isInteger(compilationTtlMs) || compilationTtlMs <= 0
|
|
394
|
+
|| compilationTtlMs > DEVELOPMENT_COMPILATION_LIMITS.ttlMs) fail('COMPILATION_UNAVAILABLE', 'Private registry, execution lifetimes and a bounded TTL are required')
|
|
395
|
+
if (producerSignal.aborted) fail('COMPILATION_CANCELLED', 'The producer lifetime ended')
|
|
396
|
+
}
|
|
397
|
+
function clock() {
|
|
398
|
+
let value
|
|
399
|
+
try { value = compilationNow() } catch { dispose(); fail('COMPILATION_EXPIRED', 'The capture clock is unavailable') }
|
|
400
|
+
if (!Number.isFinite(value) || value < lastClock || value < 0 || value > Number.MAX_SAFE_INTEGER - DEVELOPMENT_COMPILATION_LIMITS.ttlMs) {
|
|
401
|
+
dispose(); fail('COMPILATION_EXPIRED', 'A finite monotonic capture clock is required')
|
|
402
|
+
}
|
|
403
|
+
lastClock = value
|
|
404
|
+
return value
|
|
405
|
+
}
|
|
406
|
+
function context(exec) {
|
|
407
|
+
compilationReady()
|
|
408
|
+
if (!record(exec) || !record(exec.agent) || !record(exec.agent.session) || typeof exec.token !== 'symbol' || !signal(exec.signal)) {
|
|
409
|
+
fail('COMPILATION_CONTEXT_CHANGED', 'The original execution with its actual body signal is required')
|
|
410
|
+
}
|
|
411
|
+
const value = synchronous(readCompilationSignals(exec), 'COMPILATION_UNAVAILABLE')
|
|
412
|
+
if (!record(value)) fail('COMPILATION_UNAVAILABLE', 'The private owner/policy lifetime reader is unavailable')
|
|
413
|
+
const { ownerSignal, policySignal } = value
|
|
414
|
+
if (!signal(ownerSignal) || !signal(policySignal)) fail('COMPILATION_UNAVAILABLE', 'The private owner/policy lifetime reader is unavailable')
|
|
415
|
+
if (compilationDisposed || producerSignal.aborted || exec.signal.aborted || ownerSignal.aborted || policySignal.aborted) fail('COMPILATION_CANCELLED', 'An execution lifetime ended')
|
|
416
|
+
return { exec, agent: exec.agent, session: exec.agent.session, token: exec.token, bodySignal: exec.signal,
|
|
417
|
+
ownerSignal, policySignal, args: exec.arguments, name: exec.name,
|
|
418
|
+
callId: exec.callId, rootCallId: exec.rootCallId, parent: exec.parent }
|
|
419
|
+
}
|
|
420
|
+
function checkContext(prior, createdAt, expiresAt) {
|
|
421
|
+
const now = clock()
|
|
422
|
+
if (now < createdAt || now >= expiresAt) fail('COMPILATION_EXPIRED', 'The bounded compilation input expired')
|
|
423
|
+
const current = context(prior.exec)
|
|
424
|
+
if (Object.keys(prior).some(key => current[key] !== prior[key])) fail('COMPILATION_CONTEXT_CHANGED', 'The execution or its private lifetime binding changed')
|
|
425
|
+
}
|
|
426
|
+
function registered(lease) {
|
|
427
|
+
if (lease === null || lease === undefined) fail('REGISTERED_TARGET_UNAVAILABLE', 'An exact registered target lease is required, not a default target')
|
|
428
|
+
const raw = synchronous(readRegisteredTarget(lease), 'REGISTERED_TARGET_UNAVAILABLE')
|
|
429
|
+
if (!record(raw)) fail('REGISTERED_TARGET_UNAVAILABLE', 'The registered target lease is missing or revoked')
|
|
430
|
+
const keys = ['harnessRoot', 'sourceRoot', 'pluginId', 'sourceDirectoryIdentity', 'runtimeEntry']
|
|
431
|
+
if (Reflect.ownKeys(raw).length !== keys.length || keys.some(key => !Object.hasOwn(raw, key))) fail('REGISTERED_TARGET_UNAVAILABLE', 'The fixed physical target descriptor must contain exactly the documented fields')
|
|
432
|
+
const value = Object.fromEntries(keys.map(key => {
|
|
433
|
+
const descriptor = Object.getOwnPropertyDescriptor(raw, key)
|
|
434
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) fail('REGISTERED_TARGET_UNAVAILABLE', 'Registered physical descriptors must be data, not getters')
|
|
435
|
+
return [key, descriptor.value]
|
|
436
|
+
}))
|
|
437
|
+
const sourceIdentity = value.sourceDirectoryIdentity
|
|
438
|
+
if (!record(sourceIdentity) || !/^(?:0|[1-9][0-9]{0,39})$/.test(sourceIdentity.dev)
|
|
439
|
+
|| !/^[1-9][0-9]{0,39}$/.test(sourceIdentity.ino)
|
|
440
|
+
|| typeof sourceIdentity.dev !== 'string' || typeof sourceIdentity.ino !== 'string') fail('REGISTERED_TARGET_UNAVAILABLE', 'The registered physical directory identity is invalid')
|
|
441
|
+
const runtimeEntry = exactRelative(value.runtimeEntry)
|
|
442
|
+
if (!/\.(?:[cm]?js)$/.test(runtimeEntry)) fail('COMPILATION_PROFILE_UNSUPPORTED', 'The registered runtime entry must be one explicit package-relative JavaScript file')
|
|
443
|
+
return freeze({ harnessRoot: absolutePath(value.harnessRoot, 'Registered Harness root'),
|
|
444
|
+
sourceRoot: absolutePath(value.sourceRoot, 'Registered source root'), pluginId: value.pluginId,
|
|
445
|
+
sourceDirectoryIdentity: { dev: sourceIdentity.dev, ino: sourceIdentity.ino }, runtimeEntry })
|
|
446
|
+
}
|
|
447
|
+
function observeCompilation(exec, lease, expectedRegistration, retainCode, guard) {
|
|
448
|
+
let registration = expectedRegistration
|
|
449
|
+
function readPass() {
|
|
450
|
+
guard()
|
|
451
|
+
const selected = registered(lease)
|
|
452
|
+
guard()
|
|
453
|
+
if (registration && signature(selected) !== signature(registration)) fail('REGISTERED_TARGET_CHANGED', 'The registered runtime entry or physical target changed')
|
|
454
|
+
registration ??= selected
|
|
455
|
+
const located = locate(exec, selected.pluginId)
|
|
456
|
+
if (['harnessRoot', 'sourceRoot', 'pluginId'].some(key => located.binding[key] !== selected[key])
|
|
457
|
+
|| signature(located.binding.sourceDirectoryIdentity) !== signature(selected.sourceDirectoryIdentity)) {
|
|
458
|
+
fail('REGISTERED_TARGET_MISMATCH', 'The located target does not match the registered physical target')
|
|
459
|
+
}
|
|
460
|
+
guard()
|
|
461
|
+
const files = readSnapshot(located.binding.sourceRoot, selected.pluginId, { runtimeEntry: selected.runtimeEntry, retainCode })
|
|
462
|
+
guard()
|
|
463
|
+
return { ...located, observed: files.observed, code: files.code,
|
|
464
|
+
proof: signature({ binding: located.binding, route: located.route, files: files.observed }) }
|
|
465
|
+
}
|
|
466
|
+
const first = readPass(), second = readPass()
|
|
467
|
+
if (first.proof !== second.proof || (retainCode && first.code.some((item, index) => item.code !== second.code[index]?.code))) {
|
|
468
|
+
fail('TARGET_CHANGED', 'The registered compilation inputs changed between bounded FD reads')
|
|
469
|
+
}
|
|
470
|
+
if (signature(registered(lease)) !== signature(registration)) fail('REGISTERED_TARGET_CHANGED', 'The registration changed before capture completed')
|
|
471
|
+
guard()
|
|
472
|
+
return { ...first, registration }
|
|
473
|
+
}
|
|
474
|
+
function drop(entry, code) {
|
|
475
|
+
if (!entry || entry.closed) return false
|
|
476
|
+
entry.closed = code
|
|
477
|
+
pending.delete(entry)
|
|
478
|
+
retainedBytes -= entry.bytes
|
|
479
|
+
clearTimeout(entry.timer)
|
|
480
|
+
entry.detach?.()
|
|
481
|
+
// Leave only a light tombstone for a retained opaque handle. In particular,
|
|
482
|
+
// revoked handles retain neither code nor the real Agent/exec/lease graph.
|
|
483
|
+
for (const key of Object.keys(entry)) if (key !== 'closed') delete entry[key]
|
|
484
|
+
return true
|
|
485
|
+
}
|
|
486
|
+
function sweep() {
|
|
487
|
+
const now = clock()
|
|
488
|
+
for (const entry of [...pending]) if (now < entry.createdAt || now >= entry.expiresAt) drop(entry, 'COMPILATION_EXPIRED')
|
|
489
|
+
}
|
|
490
|
+
function dispose() {
|
|
491
|
+
if (compilationDisposed) return
|
|
492
|
+
compilationDisposed = true
|
|
493
|
+
for (const entry of [...pending]) drop(entry, 'COMPILATION_DISPOSED')
|
|
494
|
+
if (signal(producerSignal)) producerSignal.removeEventListener('abort', dispose)
|
|
495
|
+
}
|
|
496
|
+
if (signal(producerSignal)) producerSignal.addEventListener('abort', dispose, { once: true })
|
|
497
|
+
|
|
498
|
+
function captureCompilation(exec, opaqueLease) {
|
|
499
|
+
compilationReady(); sweep()
|
|
500
|
+
if (capturing || pending.size >= DEVELOPMENT_COMPILATION_LIMITS.pending) fail('COMPILATION_LIMIT', 'Compilation input capacity is exhausted or capture is reentrant')
|
|
501
|
+
capturing = true
|
|
502
|
+
let created
|
|
503
|
+
try {
|
|
504
|
+
const prior = context(exec), createdAt = clock(), expiresAt = createdAt + compilationTtlMs
|
|
505
|
+
const guard = () => checkContext(prior, createdAt, expiresAt)
|
|
506
|
+
const observed = observeCompilation(exec, opaqueLease, undefined, true, guard)
|
|
507
|
+
const bytes = observed.code.reduce((sum, item) => sum + Buffer.byteLength(item.code, 'utf8'), 0)
|
|
508
|
+
if (retainedBytes + bytes > DEVELOPMENT_COMPILATION_LIMITS.retainedBytes) fail('COMPILATION_LIMIT', 'Aggregate retained code exceeds 16 MiB')
|
|
509
|
+
const snapshot = freeze({ eligible: true, binding: observed.binding, entry: observed.observed.entry,
|
|
510
|
+
declaredEntry: observed.observed.declaredEntry, resolvedRuntimeEntry: observed.observed.resolvedRuntimeEntry,
|
|
511
|
+
profile: observed.observed.profile, declaredArtifacts: observed.observed.declaredArtifacts,
|
|
512
|
+
files: observed.observed.files, evidenceBoundary: 'parent-observation-only' })
|
|
513
|
+
// Code is constructed ONLY from the two pre-approval FD observations. Input
|
|
514
|
+
// reads below re-hash for validation, but NEVER replace these strings.
|
|
515
|
+
const input = freeze({ binding: snapshot.binding, declaredEntry: snapshot.declaredEntry,
|
|
516
|
+
resolvedRuntimeEntry: snapshot.resolvedRuntimeEntry, files: observed.code, evidenceBoundary: 'parent-observation-only' })
|
|
517
|
+
const handle = Object.freeze(Object.create(null))
|
|
518
|
+
const entry = { context: prior, lease: opaqueLease, registration: observed.registration, proof: observed.proof,
|
|
519
|
+
createdAt, expiresAt, bytes, snapshot, input }
|
|
520
|
+
created = entry
|
|
521
|
+
const remaining = Math.max(1, Math.ceil(expiresAt - clock()))
|
|
522
|
+
guard()
|
|
523
|
+
const lifetime = AbortSignal.any([prior.bodySignal, prior.ownerSignal, prior.policySignal, producerSignal])
|
|
524
|
+
const cancel = () => drop(entry, 'COMPILATION_CANCELLED')
|
|
525
|
+
entry.detach = () => lifetime.removeEventListener('abort', cancel)
|
|
526
|
+
pending.add(entry); retainedBytes += bytes
|
|
527
|
+
compilations.set(handle, entry); issued.set(snapshot, { compilationHandle: handle })
|
|
528
|
+
lifetime.addEventListener('abort', cancel, { once: true })
|
|
529
|
+
entry.timer = setTimeout(() => drop(entry, 'COMPILATION_EXPIRED'), remaining)
|
|
530
|
+
entry.timer.unref()
|
|
531
|
+
if (lifetime.aborted) { cancel(); fail('COMPILATION_CANCELLED', 'An execution lifetime ended during capture') }
|
|
532
|
+
return handle
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (created && pending.has(created)) drop(created, error instanceof DevelopmentTargetError ? error.code : 'TARGET_UNAVAILABLE')
|
|
535
|
+
if (error instanceof DevelopmentTargetError) throw error
|
|
536
|
+
throw new DevelopmentTargetError('TARGET_UNAVAILABLE', 'Registered compilation inputs could not be safely captured', error)
|
|
537
|
+
} finally { capturing = false }
|
|
538
|
+
}
|
|
539
|
+
function validateCompilation(handle, exec) {
|
|
540
|
+
const entry = handle && typeof handle === 'object' ? compilations.get(handle) : undefined
|
|
541
|
+
if (!entry) fail('UNISSUED_COMPILATION', 'Use this resolver\'s exact compilation handle')
|
|
542
|
+
if (entry.closed) fail(entry.closed, 'This compilation input is no longer live')
|
|
543
|
+
// A borrower cannot gain access or destroy the rightful invocation's input.
|
|
544
|
+
if (exec !== entry.context.exec) fail('COMPILATION_CONTEXT_CHANGED', 'Compilation inputs cannot move to a different exec, even on the same Agent')
|
|
545
|
+
if (entry.checking) { drop(entry, 'COMPILATION_CONTEXT_CHANGED'); fail('COMPILATION_CONTEXT_CHANGED', 'Compilation revalidation cannot reenter itself') }
|
|
546
|
+
entry.checking = true
|
|
547
|
+
try {
|
|
548
|
+
const guard = () => {
|
|
549
|
+
if (entry.closed) fail(entry.closed, 'The compilation lifetime ended during revalidation')
|
|
550
|
+
checkContext(entry.context, entry.createdAt, entry.expiresAt)
|
|
551
|
+
}
|
|
552
|
+
guard()
|
|
553
|
+
const current = observeCompilation(exec, entry.lease, entry.registration, false, guard)
|
|
554
|
+
if (current.proof !== entry.proof) fail('TARGET_CHANGED', 'Source, workspace, or registered compilation inputs changed')
|
|
555
|
+
guard()
|
|
556
|
+
return entry
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const failure = error instanceof DevelopmentTargetError ? error
|
|
559
|
+
: new DevelopmentTargetError('TARGET_CHANGED', 'Registered compilation input became unavailable', error)
|
|
560
|
+
drop(entry, failure.code)
|
|
561
|
+
throw failure
|
|
562
|
+
} finally { if (!entry.closed) entry.checking = false }
|
|
563
|
+
}
|
|
564
|
+
function releaseCompilation(handle) { return drop(handle && typeof handle === 'object' ? compilations.get(handle) : undefined, 'COMPILATION_RELEASED') }
|
|
565
|
+
return Object.freeze({ capture, revalidate, captureCompilation,
|
|
566
|
+
compilationSnapshot: (handle, exec) => validateCompilation(handle, exec).snapshot,
|
|
567
|
+
compilationInput: (handle, exec) => validateCompilation(handle, exec).input,
|
|
568
|
+
releaseCompilation, dispose })
|
|
569
|
+
}
|