@adea-ai/plugins 3.0.10 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -11
- package/dist/agent-plugins-catalog.d.ts +2 -3
- package/dist/agent-plugins-catalog.js +2 -10
- package/dist/agent-plugins-catalog.js.map +1 -1
- package/dist/catalog-store.d.ts +81 -0
- package/dist/catalog-store.js +487 -0
- package/dist/catalog-store.js.map +1 -0
- package/dist/consumer-index.d.ts +1 -1
- package/dist/consumer-index.js +17 -7
- package/dist/consumer-index.js.map +1 -1
- package/dist/display-name.js +3 -2
- package/dist/display-name.js.map +1 -1
- package/dist/harness.d.ts +50 -9
- package/dist/harness.js +190 -147
- package/dist/harness.js.map +1 -1
- package/dist/icon-mirror.d.ts +2 -2
- package/dist/icon-mirror.js +38 -25
- package/dist/icon-mirror.js.map +1 -1
- package/dist/icons.js +5 -27
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +103 -88
- package/dist/index.js.map +1 -1
- package/dist/publication.d.ts +8 -11
- package/dist/publication.js +8 -11
- package/dist/publication.js.map +1 -1
- package/dist/schema.d.ts +0 -40
- package/dist/schema.js +0 -36
- package/dist/schema.js.map +1 -1
- package/dist/site-icons.d.ts +2 -0
- package/dist/site-icons.js +46 -9
- package/dist/site-icons.js.map +1 -1
- package/dist/source-digest.d.ts +2 -0
- package/dist/source-digest.js +12 -0
- package/dist/source-digest.js.map +1 -0
- package/dist/sources.js +3 -3
- package/dist/sources.js.map +1 -1
- package/native/build-provenance.txt +5 -5
- package/native/staged-reader-win32-arm64.node +0 -0
- package/native/staged-reader-win32-x64.node +0 -0
- package/package.json +4 -9
- package/src/agent-plugins-catalog.ts +3 -10
- package/src/artifacts.test.ts +80 -0
- package/src/catalog-installation.test.ts +1 -1
- package/src/catalog-store.ts +546 -0
- package/src/consumer-index.test.ts +125 -0
- package/src/consumer-index.ts +23 -8
- package/src/default-head.test.ts +143 -0
- package/src/display-name.test.ts +8 -0
- package/src/display-name.ts +3 -2
- package/src/{agent-plugins-harness.test.ts → harness.test.ts} +1 -1
- package/src/harness.ts +249 -170
- package/src/icon-mirror.ts +36 -23
- package/src/icons.test.ts +5 -0
- package/src/icons.ts +6 -29
- package/src/index.test.ts +272 -40
- package/src/index.ts +131 -94
- package/src/manifest-retry.test.ts +49 -0
- package/src/portable-catalog.test.ts +8 -5
- package/src/publication.ts +8 -11
- package/src/schema.ts +0 -40
- package/src/site-icon-fallback.test.ts +36 -19
- package/src/site-icons.test.ts +104 -0
- package/src/site-icons.ts +47 -9
- package/src/source-config-sync.test.ts +84 -0
- package/src/source-digest.test.ts +101 -0
- package/src/source-digest.ts +14 -0
- package/src/sources-json.test.ts +53 -0
- package/src/sources.ts +3 -3
- package/src/upstream-headers.test.ts +31 -7
- package/src/write-artifacts.test.ts +375 -0
- package/dist/agent-plugins-harness.d.ts +0 -54
- package/dist/agent-plugins-harness.js +0 -212
- package/dist/agent-plugins-harness.js.map +0 -1
- package/src/agent-plugins-harness.ts +0 -281
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import {
|
|
3
|
+
mkdtemp,
|
|
4
|
+
mkdir,
|
|
5
|
+
readFile,
|
|
6
|
+
readdir,
|
|
7
|
+
rename,
|
|
8
|
+
rm,
|
|
9
|
+
symlink,
|
|
10
|
+
utimes,
|
|
11
|
+
writeFile,
|
|
12
|
+
} from 'node:fs/promises'
|
|
13
|
+
import { tmpdir } from 'node:os'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import { describe, expect, test } from 'bun:test'
|
|
17
|
+
import {
|
|
18
|
+
catalogFingerprint,
|
|
19
|
+
withCatalogRead,
|
|
20
|
+
writeArtifacts,
|
|
21
|
+
type GeneratedArtifacts,
|
|
22
|
+
type PublicationStep,
|
|
23
|
+
} from './index.js'
|
|
24
|
+
import { catalogPathsIn } from './catalog-store.js'
|
|
25
|
+
|
|
26
|
+
const artifacts = (name: string) =>
|
|
27
|
+
({ 'catalog.v1.json': name, 'integrity.json': name }) as GeneratedArtifacts
|
|
28
|
+
|
|
29
|
+
const child = fileURLToPath(new URL('../test-support/catalog-writer-child.ts', import.meta.url))
|
|
30
|
+
|
|
31
|
+
/** Runs a writer in its own process, optionally with its own TMPDIR. */
|
|
32
|
+
function runWriter(
|
|
33
|
+
output: string,
|
|
34
|
+
content: string,
|
|
35
|
+
crashAt: PublicationStep | 'none',
|
|
36
|
+
options: { waitMs?: number; tmp?: string } = {}
|
|
37
|
+
) {
|
|
38
|
+
const result = spawnSync(
|
|
39
|
+
process.execPath,
|
|
40
|
+
[
|
|
41
|
+
child,
|
|
42
|
+
output,
|
|
43
|
+
content,
|
|
44
|
+
crashAt,
|
|
45
|
+
...(options.waitMs === undefined ? [] : [String(options.waitMs)]),
|
|
46
|
+
],
|
|
47
|
+
{
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
env: {
|
|
50
|
+
...process.env,
|
|
51
|
+
...(options.tmp ? { TMPDIR: options.tmp, TMP: options.tmp, TEMP: options.tmp } : {}),
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
// Windows reports a forced termination as an exit status, not a signal.
|
|
56
|
+
const killed =
|
|
57
|
+
result.signal === 'SIGKILL' || (process.platform === 'win32' && result.status !== 0)
|
|
58
|
+
return { stdout: result.stdout.trim(), stderr: result.stderr, killed }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A promise the test resolves to let a paused writer continue. */
|
|
62
|
+
function gate() {
|
|
63
|
+
let open!: () => void
|
|
64
|
+
const opened = new Promise<void>((resolve) => {
|
|
65
|
+
open = resolve
|
|
66
|
+
})
|
|
67
|
+
return { open, opened }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function withParent(run: (parent: string) => Promise<void>) {
|
|
71
|
+
const parent = await mkdtemp(join(tmpdir(), 'catalog-writers-'))
|
|
72
|
+
try {
|
|
73
|
+
await run(parent)
|
|
74
|
+
} finally {
|
|
75
|
+
await rm(parent, { recursive: true, force: true })
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const read = (output: string) => readFile(join(output, 'catalog.v1.json'), 'utf8')
|
|
80
|
+
/** Coordination leftovers beside an output; a settled output has none. */
|
|
81
|
+
const leftovers = async (parent: string) =>
|
|
82
|
+
(await readdir(parent)).filter(
|
|
83
|
+
(name) => name.startsWith('.catalog-') || name.endsWith('.previous')
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
describe('writer coordination', () => {
|
|
87
|
+
test('writers with different temporary directories exclude each other', () =>
|
|
88
|
+
withParent(async (parent) => {
|
|
89
|
+
const output = join(parent, 'generated')
|
|
90
|
+
await writeArtifacts(output, artifacts('first'))
|
|
91
|
+
const otherTmp = join(parent, 'other-tmp')
|
|
92
|
+
await mkdir(otherTmp)
|
|
93
|
+
const paused = gate()
|
|
94
|
+
const reached = gate()
|
|
95
|
+
const holder = writeArtifacts(output, artifacts('holder'), {
|
|
96
|
+
onStep: async (step) => {
|
|
97
|
+
if (step !== 'staged') return
|
|
98
|
+
reached.open()
|
|
99
|
+
await paused.opened
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
await reached.opened
|
|
103
|
+
// The diagnosed race: the lock lived in TMPDIR, so a writer with another
|
|
104
|
+
// TMPDIR took its own lock and swapped concurrently. Both in-process and
|
|
105
|
+
// as a separate process with its own TMPDIR, the second writer now meets
|
|
106
|
+
// the first writer's lock.
|
|
107
|
+
const previousTmp = process.env.TMPDIR
|
|
108
|
+
process.env.TMPDIR = otherTmp
|
|
109
|
+
try {
|
|
110
|
+
await expect(writeArtifacts(output, artifacts('intruder'), { waitMs: 0 })).rejects.toThrow(
|
|
111
|
+
'CATALOG_WRITE_IN_PROGRESS'
|
|
112
|
+
)
|
|
113
|
+
} finally {
|
|
114
|
+
if (previousTmp === undefined) delete process.env.TMPDIR
|
|
115
|
+
else process.env.TMPDIR = previousTmp
|
|
116
|
+
}
|
|
117
|
+
const separate = runWriter(output, 'separate', 'none', { waitMs: 0, tmp: otherTmp })
|
|
118
|
+
expect(separate.stdout).toContain('CATALOG_WRITE_IN_PROGRESS')
|
|
119
|
+
expect(await read(output)).toBe('first')
|
|
120
|
+
paused.open()
|
|
121
|
+
await holder
|
|
122
|
+
expect(await read(output)).toBe('holder')
|
|
123
|
+
expect(await readdir(otherTmp)).toEqual([])
|
|
124
|
+
expect(await leftovers(parent)).toEqual([])
|
|
125
|
+
}))
|
|
126
|
+
|
|
127
|
+
test('a waiting writer publishes after the holder, never in between', () =>
|
|
128
|
+
withParent(async (parent) => {
|
|
129
|
+
const output = join(parent, 'generated')
|
|
130
|
+
const steps: string[] = []
|
|
131
|
+
const paused = gate()
|
|
132
|
+
const reached = gate()
|
|
133
|
+
const first = writeArtifacts(output, artifacts('first'), {
|
|
134
|
+
onStep: async (step) => {
|
|
135
|
+
steps.push(`first:${step}`)
|
|
136
|
+
if (step !== 'backed-up') return
|
|
137
|
+
reached.open()
|
|
138
|
+
await paused.opened
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
await reached.opened
|
|
142
|
+
const contending = gate()
|
|
143
|
+
const second = writeArtifacts(output, artifacts('second'), {
|
|
144
|
+
pollMs: 1,
|
|
145
|
+
onStep: (step) => {
|
|
146
|
+
steps.push(`second:${step}`)
|
|
147
|
+
if (step === 'acquiring') contending.open()
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
await contending.opened
|
|
151
|
+
// Let the second writer poll the held lock a few times.
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
153
|
+
paused.open()
|
|
154
|
+
await Promise.all([first, second])
|
|
155
|
+
expect(steps).toEqual([
|
|
156
|
+
'first:acquiring',
|
|
157
|
+
'first:staged',
|
|
158
|
+
'first:backed-up',
|
|
159
|
+
'second:acquiring',
|
|
160
|
+
'first:swapped',
|
|
161
|
+
'first:cleaned',
|
|
162
|
+
'second:staged',
|
|
163
|
+
'second:backed-up',
|
|
164
|
+
'second:swapped',
|
|
165
|
+
'second:cleaned',
|
|
166
|
+
])
|
|
167
|
+
expect(await read(output)).toBe('second')
|
|
168
|
+
expect(await leftovers(parent)).toEqual([])
|
|
169
|
+
}))
|
|
170
|
+
|
|
171
|
+
test('outputs that differ only in case share one lock', () => {
|
|
172
|
+
expect(catalogPathsIn('/x', 'Generated').lock).toBe(catalogPathsIn('/x', 'generated').lock)
|
|
173
|
+
expect(catalogPathsIn('/x', 'generated').lock).not.toBe(catalogPathsIn('/x', 'other').lock)
|
|
174
|
+
expect(
|
|
175
|
+
catalogPathsIn('/x', 'generated').lock.startsWith(join('/x', '.catalog-generated-'))
|
|
176
|
+
).toBe(true)
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe('publication against a read snapshot', () => {
|
|
181
|
+
test('refuses to overwrite a catalog replaced after the base was read', () =>
|
|
182
|
+
withParent(async (parent) => {
|
|
183
|
+
const output = join(parent, 'generated')
|
|
184
|
+
await writeArtifacts(output, artifacts('base'))
|
|
185
|
+
// The diagnosed race: a scoped sync validated the output and read its
|
|
186
|
+
// lock, another writer replaced the catalog, and the sync then published
|
|
187
|
+
// over it. The base read and the publication now compare fingerprints.
|
|
188
|
+
const base = await withCatalogRead(output, () => catalogFingerprint(output))
|
|
189
|
+
await writeArtifacts(output, artifacts('concurrent'))
|
|
190
|
+
await expect(
|
|
191
|
+
writeArtifacts(output, artifacts('stale'), { expectedBase: base })
|
|
192
|
+
).rejects.toThrow('CATALOG_BASE_CHANGED')
|
|
193
|
+
expect(await read(output)).toBe('concurrent')
|
|
194
|
+
const fresh = await withCatalogRead(output, () => catalogFingerprint(output))
|
|
195
|
+
await writeArtifacts(output, artifacts('rebased'), { expectedBase: fresh })
|
|
196
|
+
expect(await read(output)).toBe('rebased')
|
|
197
|
+
expect(await leftovers(parent)).toEqual([])
|
|
198
|
+
}))
|
|
199
|
+
|
|
200
|
+
test('an absent output is a base, and a first publication must still find it absent', () =>
|
|
201
|
+
withParent(async (parent) => {
|
|
202
|
+
const output = join(parent, 'generated')
|
|
203
|
+
const base = await withCatalogRead(output, () => catalogFingerprint(output))
|
|
204
|
+
expect(base).toBe('absent')
|
|
205
|
+
await writeArtifacts(output, artifacts('other'))
|
|
206
|
+
await expect(
|
|
207
|
+
writeArtifacts(output, artifacts('mine'), { expectedBase: base })
|
|
208
|
+
).rejects.toThrow('CATALOG_BASE_CHANGED')
|
|
209
|
+
expect(await read(output)).toBe('other')
|
|
210
|
+
}))
|
|
211
|
+
|
|
212
|
+
test('a coordinated reader never observes the swap window', () =>
|
|
213
|
+
withParent(async (parent) => {
|
|
214
|
+
const output = join(parent, 'generated')
|
|
215
|
+
await writeArtifacts(output, artifacts('old'))
|
|
216
|
+
const paused = gate()
|
|
217
|
+
const reached = gate()
|
|
218
|
+
const writer = writeArtifacts(output, artifacts('new'), {
|
|
219
|
+
onStep: async (step) => {
|
|
220
|
+
if (step !== 'backed-up') return
|
|
221
|
+
reached.open()
|
|
222
|
+
await paused.opened
|
|
223
|
+
},
|
|
224
|
+
})
|
|
225
|
+
await reached.opened
|
|
226
|
+
// Between the renames the output does not exist on disk.
|
|
227
|
+
await expect(read(output)).rejects.toThrow()
|
|
228
|
+
const reader = withCatalogRead(output, () => read(output), { pollMs: 1 })
|
|
229
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
230
|
+
paused.open()
|
|
231
|
+
expect(await reader).toBe('new')
|
|
232
|
+
await writer
|
|
233
|
+
}))
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe('crash recovery', () => {
|
|
237
|
+
const cases: readonly { step: PublicationStep; expected: string }[] = [
|
|
238
|
+
{ step: 'staged', expected: 'old' },
|
|
239
|
+
{ step: 'backed-up', expected: 'old' },
|
|
240
|
+
{ step: 'swapped', expected: 'new' },
|
|
241
|
+
{ step: 'cleaned', expected: 'new' },
|
|
242
|
+
]
|
|
243
|
+
for (const { step, expected } of cases) {
|
|
244
|
+
test(`a writer killed after "${step}" is ${expected === 'old' ? 'rolled back' : 'completed'} by the next reader`, () =>
|
|
245
|
+
withParent(async (parent) => {
|
|
246
|
+
const output = join(parent, 'generated')
|
|
247
|
+
await writeArtifacts(output, artifacts('old'))
|
|
248
|
+
const crashed = runWriter(output, 'new', step)
|
|
249
|
+
expect(crashed.killed).toBe(true)
|
|
250
|
+
expect(crashed.stdout).toBe('')
|
|
251
|
+
// The killed process left its lock behind; its holder is gone.
|
|
252
|
+
expect((await readdir(parent)).some((name) => name.endsWith('.lock'))).toBe(true)
|
|
253
|
+
expect(await withCatalogRead(output, () => read(output), { waitMs: 0 })).toBe(expected)
|
|
254
|
+
expect(await leftovers(parent)).toEqual([])
|
|
255
|
+
await writeArtifacts(output, artifacts('after'), { waitMs: 0 })
|
|
256
|
+
expect(await read(output)).toBe('after')
|
|
257
|
+
}))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
test('a writer killed during a first publication leaves no output and no residue', () =>
|
|
261
|
+
withParent(async (parent) => {
|
|
262
|
+
const output = join(parent, 'generated')
|
|
263
|
+
expect(runWriter(output, 'new', 'staged').killed).toBe(true)
|
|
264
|
+
await writeArtifacts(output, artifacts('next'), { waitMs: 0 })
|
|
265
|
+
expect(await read(output)).toBe('next')
|
|
266
|
+
expect(await leftovers(parent)).toEqual([])
|
|
267
|
+
}))
|
|
268
|
+
|
|
269
|
+
test('a holder on another host is broken only after its heartbeat lease expires', () =>
|
|
270
|
+
withParent(async (parent) => {
|
|
271
|
+
const output = join(parent, 'generated')
|
|
272
|
+
await writeArtifacts(output, artifacts('old'))
|
|
273
|
+
const { lock, owner } = catalogPathsIn(parent, 'generated')
|
|
274
|
+
await mkdir(lock)
|
|
275
|
+
await writeFile(owner, JSON.stringify({ token: 'remote', pid: 1, host: 'elsewhere.invalid' }))
|
|
276
|
+
await expect(
|
|
277
|
+
writeArtifacts(output, artifacts('blocked'), { waitMs: 0, staleAfterMs: 60_000 })
|
|
278
|
+
).rejects.toThrow(/CATALOG_WRITE_IN_PROGRESS: .*pid 1 on elsewhere\.invalid/)
|
|
279
|
+
const old = new Date(Date.now() - 120_000)
|
|
280
|
+
await utimes(owner, old, old)
|
|
281
|
+
await writeArtifacts(output, artifacts('new'), { waitMs: 0, staleAfterMs: 60_000 })
|
|
282
|
+
expect(await read(output)).toBe('new')
|
|
283
|
+
expect(await leftovers(parent)).toEqual([])
|
|
284
|
+
}))
|
|
285
|
+
|
|
286
|
+
test('a holder that lost its lock refuses to swap', () =>
|
|
287
|
+
withParent(async (parent) => {
|
|
288
|
+
const output = join(parent, 'generated')
|
|
289
|
+
await writeArtifacts(output, artifacts('old'))
|
|
290
|
+
const { lock, owner } = catalogPathsIn(parent, 'generated')
|
|
291
|
+
const writer = writeArtifacts(output, artifacts('late'), {
|
|
292
|
+
onStep: async (step) => {
|
|
293
|
+
if (step !== 'staged') return
|
|
294
|
+
// Another contender judged this holder stale and took the lock.
|
|
295
|
+
await rename(lock, `${lock}.taken`)
|
|
296
|
+
await mkdir(lock)
|
|
297
|
+
await writeFile(owner, JSON.stringify({ token: 'other', pid: process.pid, host: 'x' }))
|
|
298
|
+
},
|
|
299
|
+
})
|
|
300
|
+
await expect(writer).rejects.toThrow('CATALOG_LOCK_LOST')
|
|
301
|
+
expect(await read(output)).toBe('old')
|
|
302
|
+
expect(JSON.parse(await readFile(owner, 'utf8')).token).toBe('other')
|
|
303
|
+
}))
|
|
304
|
+
|
|
305
|
+
test('preserves a backup whose provenance is unknown', () =>
|
|
306
|
+
withParent(async (parent) => {
|
|
307
|
+
const output = join(parent, 'generated')
|
|
308
|
+
await mkdir(output)
|
|
309
|
+
await writeFile(join(output, 'catalog.v1.json'), 'current')
|
|
310
|
+
await mkdir(`${output}.previous`)
|
|
311
|
+
await writeFile(join(`${output}.previous`, 'recovery'), 'saved')
|
|
312
|
+
await expect(writeArtifacts(output, artifacts('blocked'))).rejects.toThrow(
|
|
313
|
+
'CATALOG_RECOVERY_REQUIRED'
|
|
314
|
+
)
|
|
315
|
+
expect(await readFile(join(`${output}.previous`, 'recovery'), 'utf8')).toBe('saved')
|
|
316
|
+
expect(await read(output)).toBe('current')
|
|
317
|
+
expect(await withCatalogRead(output, () => read(output))).toBe('current')
|
|
318
|
+
expect(await readFile(join(`${output}.previous`, 'recovery'), 'utf8')).toBe('saved')
|
|
319
|
+
}))
|
|
320
|
+
|
|
321
|
+
test('a failed write keeps the previous catalog and releases the lock', () =>
|
|
322
|
+
withParent(async (parent) => {
|
|
323
|
+
const output = join(parent, 'generated')
|
|
324
|
+
await writeArtifacts(output, artifacts('previous'))
|
|
325
|
+
await expect(
|
|
326
|
+
writeArtifacts(output, { ...artifacts('broken'), 'missing/file': 'bad' })
|
|
327
|
+
).rejects.toThrow('CATALOG_ARTIFACT_NAME_INVALID')
|
|
328
|
+
await expect(
|
|
329
|
+
writeArtifacts(output, artifacts('broken'), {
|
|
330
|
+
onStep: (step) => {
|
|
331
|
+
if (step === 'backed-up') throw new Error('injected failure')
|
|
332
|
+
},
|
|
333
|
+
})
|
|
334
|
+
).rejects.toThrow('injected failure')
|
|
335
|
+
expect(await read(output)).toBe('previous')
|
|
336
|
+
expect(await leftovers(parent)).toEqual([])
|
|
337
|
+
await writeArtifacts(output, artifacts('later'), { waitMs: 0 })
|
|
338
|
+
expect(await read(output)).toBe('later')
|
|
339
|
+
}))
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
describe('reserved paths', () => {
|
|
343
|
+
test('refuses outputs inside coordination files or backups', () =>
|
|
344
|
+
withParent(async (parent) => {
|
|
345
|
+
const output = join(parent, 'generated')
|
|
346
|
+
const { lock } = catalogPathsIn(parent, 'generated')
|
|
347
|
+
for (const destination of [lock, `${output}.previous`, join(`${output}.previous`, 'x')])
|
|
348
|
+
await expect(writeArtifacts(destination, artifacts('blocked'))).rejects.toThrow(
|
|
349
|
+
'CATALOG_WRITE_LOCK_RESERVED'
|
|
350
|
+
)
|
|
351
|
+
await mkdir(lock)
|
|
352
|
+
const alias = join(parent, 'lock-alias')
|
|
353
|
+
await symlink(lock, alias, 'dir')
|
|
354
|
+
await expect(writeArtifacts(join(alias, 'nested'), artifacts('blocked'))).rejects.toThrow(
|
|
355
|
+
'CATALOG_WRITE_LOCK_RESERVED'
|
|
356
|
+
)
|
|
357
|
+
await rm(lock, { recursive: true })
|
|
358
|
+
// A backup can still be read by hand, without coordination.
|
|
359
|
+
await mkdir(`${output}.previous`)
|
|
360
|
+
await writeFile(join(`${output}.previous`, 'catalog.v1.json'), 'saved')
|
|
361
|
+
expect(await withCatalogRead(`${output}.previous`, () => read(`${output}.previous`))).toBe(
|
|
362
|
+
'saved'
|
|
363
|
+
)
|
|
364
|
+
}))
|
|
365
|
+
|
|
366
|
+
test('preserves filesystem semantics for a symlink followed by dot dot', () =>
|
|
367
|
+
withParent(async (parent) => {
|
|
368
|
+
const real = join(parent, 'real')
|
|
369
|
+
await mkdir(join(real, 'child'), { recursive: true })
|
|
370
|
+
await symlink(join(real, 'child'), join(parent, 'link'), 'dir')
|
|
371
|
+
await writeArtifacts(`${parent}/link/../generated`, artifacts('real target'))
|
|
372
|
+
expect(await read(join(real, 'generated'))).toBe('real target')
|
|
373
|
+
await expect(read(join(parent, 'generated'))).rejects.toThrow()
|
|
374
|
+
}))
|
|
375
|
+
})
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { type HarnessProfile, type McpServer, type InstallationPlan } from './agent-plugins-schema.js';
|
|
2
|
-
import type { Catalog } from './schema.js';
|
|
3
|
-
export interface InstallationInput {
|
|
4
|
-
readonly pluginId: string;
|
|
5
|
-
readonly releaseId: string;
|
|
6
|
-
readonly instanceId: string;
|
|
7
|
-
readonly source: InstallationPlan['source'];
|
|
8
|
-
readonly package: unknown;
|
|
9
|
-
readonly profile: HarnessProfile;
|
|
10
|
-
readonly requiredCredentials?: readonly string[];
|
|
11
|
-
readonly requiredConnectors?: readonly string[];
|
|
12
|
-
readonly allowPartial?: boolean;
|
|
13
|
-
}
|
|
14
|
-
export interface CatalogInstallationInput {
|
|
15
|
-
readonly catalog: Catalog;
|
|
16
|
-
readonly pluginId: string;
|
|
17
|
-
readonly releaseId?: string;
|
|
18
|
-
readonly instanceId: string;
|
|
19
|
-
readonly profile: HarnessProfile;
|
|
20
|
-
readonly allowPartial?: boolean;
|
|
21
|
-
}
|
|
22
|
-
/** Resolve the exact catalog release before planning. The caller must also authenticate
|
|
23
|
-
* the catalog publisher and verify its artifact integrity before invoking this API.
|
|
24
|
-
*/
|
|
25
|
-
export declare function createCatalogInstallationPlan(input: CatalogInstallationInput): InstallationPlan;
|
|
26
|
-
/** A declarative proposal, never an execution grant. No brand-based support assumptions. */
|
|
27
|
-
export declare function createInstallationPlan(input: InstallationInput): InstallationPlan;
|
|
28
|
-
export interface RuntimeRoots {
|
|
29
|
-
readonly pluginRoot: string;
|
|
30
|
-
readonly pluginData: string;
|
|
31
|
-
}
|
|
32
|
-
/** Exact, single-pass expansion. Replacement text is never scanned recursively. */
|
|
33
|
-
export declare function expandPluginPaths(value: string, roots: RuntimeRoots): string;
|
|
34
|
-
export type McpBinding = {
|
|
35
|
-
type: 'stdio';
|
|
36
|
-
command: string;
|
|
37
|
-
args: string[];
|
|
38
|
-
cwd: string;
|
|
39
|
-
env: Record<string, string>;
|
|
40
|
-
shell: false;
|
|
41
|
-
} | {
|
|
42
|
-
type: 'streamable-http' | 'sse';
|
|
43
|
-
url: string;
|
|
44
|
-
headers: Record<string, string>;
|
|
45
|
-
redirectPolicy: 'same-origin';
|
|
46
|
-
authorization: 'control-plane';
|
|
47
|
-
};
|
|
48
|
-
/** Produce concrete Control Plane launch/connect descriptors, without launching or connecting. */
|
|
49
|
-
export declare function resolveMcpBinding(input: McpServer, roots: RuntimeRoots, baseEnv?: Readonly<Record<string, string>>, platform?: 'posix' | 'win32'): McpBinding;
|
|
50
|
-
/** Mandatory filesystem preflight for Control Plane callers after safe materialization.
|
|
51
|
-
* Call immediately before activation inside the same protected installation boundary;
|
|
52
|
-
* a separate preflight is not a substitute for preventing concurrent filesystem mutation.
|
|
53
|
-
*/
|
|
54
|
-
export declare function verifyBindingPaths(input: McpServer, roots: RuntimeRoots): Promise<void>;
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { isAbsolute, resolve, relative, sep } from 'node:path';
|
|
2
|
-
import { realpath } from 'node:fs/promises';
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
4
|
-
import { HarnessProfileSchema, InstallationPlanSchema, } from './agent-plugins-schema.js';
|
|
5
|
-
import { verifyPortableCatalog, PACKAGE_METADATA_KEY } from './portable-catalog.js';
|
|
6
|
-
import { verifyAgentPackage, validateServer } from './agent-plugins-catalog.js';
|
|
7
|
-
/** Resolve the exact catalog release before planning. The caller must also authenticate
|
|
8
|
-
* the catalog publisher and verify its artifact integrity before invoking this API.
|
|
9
|
-
*/
|
|
10
|
-
export function createCatalogInstallationPlan(input) {
|
|
11
|
-
verifyPortableCatalog(input.catalog);
|
|
12
|
-
const plugin = input.catalog.plugins.find((entry) => entry.pluginId === input.pluginId);
|
|
13
|
-
if (!plugin)
|
|
14
|
-
throw new Error(`PLUGIN_NOT_FOUND: ${input.pluginId}`);
|
|
15
|
-
const release = plugin.availableReleases.find((entry) => entry.releaseId === (input.releaseId ?? plugin.currentReleaseId));
|
|
16
|
-
if (!release)
|
|
17
|
-
throw new Error('RELEASE_NOT_FOUND');
|
|
18
|
-
if (release.contentResolution !== 'complete')
|
|
19
|
-
throw new Error('COMPLETE_SOURCE_REQUIRED');
|
|
20
|
-
const pkg = release.releaseMetadata[PACKAGE_METADATA_KEY];
|
|
21
|
-
if (pkg === undefined)
|
|
22
|
-
throw new Error('PORTABLE_RESYNC_REQUIRED: run a full catalog sync before planning portable installation');
|
|
23
|
-
return createInstallationPlan({
|
|
24
|
-
pluginId: plugin.pluginId,
|
|
25
|
-
releaseId: release.releaseId,
|
|
26
|
-
instanceId: input.instanceId,
|
|
27
|
-
profile: input.profile,
|
|
28
|
-
package: pkg,
|
|
29
|
-
allowPartial: input.allowPartial === true,
|
|
30
|
-
source: {
|
|
31
|
-
repositoryUrl: release.resolvedRepositoryUrl,
|
|
32
|
-
commitSha: release.resolvedCommitSha,
|
|
33
|
-
pluginSubdirectory: release.pluginSubdirectory,
|
|
34
|
-
contentDigest: release.canonicalContentDigest,
|
|
35
|
-
},
|
|
36
|
-
requiredCredentials: release.requiredCredentials,
|
|
37
|
-
requiredConnectors: release.requiredConnectors,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
function identity(...parts) {
|
|
41
|
-
return createHash('sha256').update(JSON.stringify(parts)).digest('hex');
|
|
42
|
-
}
|
|
43
|
-
/** A declarative proposal, never an execution grant. No brand-based support assumptions. */
|
|
44
|
-
export function createInstallationPlan(input) {
|
|
45
|
-
const profile = HarnessProfileSchema.parse(input.profile);
|
|
46
|
-
const pkg = verifyAgentPackage(input.package);
|
|
47
|
-
if (pkg.sourceDigest !== input.source.contentDigest)
|
|
48
|
-
throw new Error('Package recipe does not match the selected immutable release.');
|
|
49
|
-
if (!input.instanceId || input.instanceId.length > 256)
|
|
50
|
-
throw new Error('A stable installation instanceId is required.');
|
|
51
|
-
const disabled = [];
|
|
52
|
-
for (const item of pkg.nonPortable)
|
|
53
|
-
disabled.push({ component: item.kind, reason: item.reason });
|
|
54
|
-
for (const item of pkg.diagnostics.filter((entry) => entry.severity === 'error'))
|
|
55
|
-
disabled.push({ component: item.path, reason: item.message });
|
|
56
|
-
const nativeEligible = profile.agentPlugins.versions.includes(pkg.specVersion) &&
|
|
57
|
-
pkg.nonPortable.length === 0 &&
|
|
58
|
-
!Object.keys(pkg.manifest?.extensions ?? {}).length;
|
|
59
|
-
const nativeCoverage = (profile.agentPlugins.skills ? pkg.skills.length : 0) +
|
|
60
|
-
Object.values(pkg.mcpServers).filter((server) => profile.agentPlugins.mcpTransports.includes(server.type)).length;
|
|
61
|
-
const adapterCoverage = (profile.components.skillDirectories ? pkg.skills.length : 0) +
|
|
62
|
-
Object.values(pkg.mcpServers).filter((server) => profile.components.mcpTransports.includes(server.type)).length;
|
|
63
|
-
const native = nativeEligible && nativeCoverage > 0 && nativeCoverage >= adapterCoverage;
|
|
64
|
-
const strategy = pkg.status === 'unavailable'
|
|
65
|
-
? 'unavailable'
|
|
66
|
-
: native
|
|
67
|
-
? 'native-agent-plugin'
|
|
68
|
-
: 'component-adapter';
|
|
69
|
-
let skillDirectories = [];
|
|
70
|
-
const servers = new Map();
|
|
71
|
-
if (strategy !== 'unavailable') {
|
|
72
|
-
if (native ? profile.agentPlugins.skills : profile.components.skillDirectories)
|
|
73
|
-
skillDirectories = pkg.skills.map((skill) => skill.path);
|
|
74
|
-
else
|
|
75
|
-
for (const skill of pkg.skills)
|
|
76
|
-
disabled.push({
|
|
77
|
-
component: `skill:${skill.name}`,
|
|
78
|
-
reason: 'The selected adapter cannot expose skill directories.',
|
|
79
|
-
});
|
|
80
|
-
for (const [name, server] of Object.entries(pkg.mcpServers)) {
|
|
81
|
-
if ((native ? profile.agentPlugins.mcpTransports : profile.components.mcpTransports).includes(server.type))
|
|
82
|
-
servers.set(name, server);
|
|
83
|
-
else
|
|
84
|
-
disabled.push({
|
|
85
|
-
component: `mcp:${name}`,
|
|
86
|
-
reason: `No ${server.type} MCP binding on this harness adapter.`,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
const hasContent = skillDirectories.length > 0 || servers.size > 0;
|
|
91
|
-
const compatibility = !hasContent ? 'unsupported' : disabled.length > 0 ? 'partial' : 'full';
|
|
92
|
-
const selectedStrategy = !hasContent || (compatibility === 'partial' && !input.allowPartial) ? 'unavailable' : strategy;
|
|
93
|
-
return InstallationPlanSchema.parse({
|
|
94
|
-
planVersion: 2,
|
|
95
|
-
pluginId: input.pluginId,
|
|
96
|
-
releaseId: input.releaseId,
|
|
97
|
-
instanceId: input.instanceId,
|
|
98
|
-
profile,
|
|
99
|
-
source: input.source,
|
|
100
|
-
package: pkg,
|
|
101
|
-
// Git source identity also pins executable modes, which the v1 byte snapshot omits.
|
|
102
|
-
packageKey: pkg.packageDigest
|
|
103
|
-
? `packages/${identity(input.source.repositoryUrl, input.source.commitSha, input.source.pluginSubdirectory, pkg.packageDigest)}`
|
|
104
|
-
: '',
|
|
105
|
-
// Deliberately excludes release, package digest and harness. Switching harnesses or updating
|
|
106
|
-
// the same approved instance preserves data; distinct scopes use distinct instance IDs.
|
|
107
|
-
dataKey: `instances/${identity(input.pluginId, input.instanceId)}`,
|
|
108
|
-
preserveDataAcrossUpdates: true,
|
|
109
|
-
strategy: selectedStrategy,
|
|
110
|
-
compatibility,
|
|
111
|
-
allowedToActivate: false,
|
|
112
|
-
approvalRequired: true,
|
|
113
|
-
selection: selectedStrategy === 'unavailable'
|
|
114
|
-
? { skillDirectories: [], mcpServers: {} }
|
|
115
|
-
: { skillDirectories, mcpServers: Object.fromEntries(servers) },
|
|
116
|
-
disabled,
|
|
117
|
-
requiredCredentials: [...(input.requiredCredentials ?? [])],
|
|
118
|
-
requiredConnectors: [...(input.requiredConnectors ?? [])],
|
|
119
|
-
preconditions: [
|
|
120
|
-
'Verify catalog integrity/provenance and exact source digest before applying the package recipe.',
|
|
121
|
-
'Use an immutable package root; preserve source file modes and complete skill directories/resources.',
|
|
122
|
-
'Create a writable data directory for this instance; never key it by release or copy credentials between instances.',
|
|
123
|
-
'Control Plane must authorize activation, filesystem/network access, executables and credential bindings.',
|
|
124
|
-
'Recheck the actual harness/runtime/adapter profile and realpath containment immediately before activation.',
|
|
125
|
-
'Native mode registers exactly one package root. Component mode registers only the selected skill directories and MCP bindings, never legacy hooks/config files.',
|
|
126
|
-
'Do not install dependencies or run lifecycle scripts implicitly; review required executables and skill compatibility requirements.',
|
|
127
|
-
'Skill allowed-tools metadata is advisory and never grants permission.',
|
|
128
|
-
'Namespace registrations by installation identity; detect skill/server name collisions instead of overwriting existing harness configuration.',
|
|
129
|
-
'Remote authorization is client-managed. Do not expand remote URLs/headers or forward configured headers to another origin without authorization.',
|
|
130
|
-
],
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
/** Exact, single-pass expansion. Replacement text is never scanned recursively. */
|
|
134
|
-
export function expandPluginPaths(value, roots) {
|
|
135
|
-
return value.replace(/\$\{(PLUGIN_ROOT|PLUGIN_DATA)\}/g, (_match, name) => name === 'PLUGIN_ROOT' ? roots.pluginRoot : roots.pluginData);
|
|
136
|
-
}
|
|
137
|
-
function within(path, root) {
|
|
138
|
-
const suffix = relative(root, path);
|
|
139
|
-
return suffix === '' || (suffix !== '..' && !suffix.startsWith(`..${sep}`) && !isAbsolute(suffix));
|
|
140
|
-
}
|
|
141
|
-
/** Produce concrete Control Plane launch/connect descriptors, without launching or connecting. */
|
|
142
|
-
export function resolveMcpBinding(input, roots, baseEnv = {}, platform = process.platform === 'win32' ? 'win32' : 'posix') {
|
|
143
|
-
const server = validateServer(input);
|
|
144
|
-
if (!isAbsolute(roots.pluginRoot) || !isAbsolute(roots.pluginData))
|
|
145
|
-
throw new Error('Runtime plugin roots must be absolute paths.');
|
|
146
|
-
if (server.type !== 'stdio')
|
|
147
|
-
return {
|
|
148
|
-
type: server.type,
|
|
149
|
-
url: server.url,
|
|
150
|
-
headers: { ...server.headers },
|
|
151
|
-
redirectPolicy: 'same-origin',
|
|
152
|
-
authorization: 'control-plane',
|
|
153
|
-
};
|
|
154
|
-
const command = server.command.startsWith('./')
|
|
155
|
-
? resolve(roots.pluginRoot, server.command)
|
|
156
|
-
: server.command;
|
|
157
|
-
if (server.command.startsWith('./') && !within(command, roots.pluginRoot))
|
|
158
|
-
throw new Error('Executable escapes the plugin root.');
|
|
159
|
-
const cwdInput = server.cwd ?? '${PLUGIN_ROOT}';
|
|
160
|
-
const cwdRoot = cwdInput.startsWith('${PLUGIN_DATA}') ? roots.pluginData : roots.pluginRoot;
|
|
161
|
-
const expanded = expandPluginPaths(cwdInput, roots);
|
|
162
|
-
const cwd = resolve(roots.pluginRoot, expanded);
|
|
163
|
-
if (!within(cwd, cwdRoot))
|
|
164
|
-
throw new Error('Working directory escapes its declared root.');
|
|
165
|
-
const env = {};
|
|
166
|
-
const overlay = (name, value) => {
|
|
167
|
-
if (platform === 'win32')
|
|
168
|
-
for (const existing of Object.keys(env))
|
|
169
|
-
if (existing.toUpperCase() === name.toUpperCase())
|
|
170
|
-
delete env[existing];
|
|
171
|
-
Object.defineProperty(env, name, {
|
|
172
|
-
value,
|
|
173
|
-
enumerable: true,
|
|
174
|
-
configurable: true,
|
|
175
|
-
writable: true,
|
|
176
|
-
});
|
|
177
|
-
};
|
|
178
|
-
for (const [key, value] of Object.entries(baseEnv))
|
|
179
|
-
overlay(key, value);
|
|
180
|
-
for (const [key, value] of Object.entries(server.env ?? {}))
|
|
181
|
-
overlay(key, expandPluginPaths(value, roots));
|
|
182
|
-
overlay('PLUGIN_ROOT', roots.pluginRoot);
|
|
183
|
-
overlay('PLUGIN_DATA', roots.pluginData);
|
|
184
|
-
return {
|
|
185
|
-
type: 'stdio',
|
|
186
|
-
command,
|
|
187
|
-
args: (server.args ?? []).map((arg) => expandPluginPaths(arg, roots)),
|
|
188
|
-
cwd,
|
|
189
|
-
env,
|
|
190
|
-
shell: false,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
/** Mandatory filesystem preflight for Control Plane callers after safe materialization.
|
|
194
|
-
* Call immediately before activation inside the same protected installation boundary;
|
|
195
|
-
* a separate preflight is not a substitute for preventing concurrent filesystem mutation.
|
|
196
|
-
*/
|
|
197
|
-
export async function verifyBindingPaths(input, roots) {
|
|
198
|
-
const binding = resolveMcpBinding(input, roots);
|
|
199
|
-
if (binding.type !== 'stdio' || input.type !== 'stdio')
|
|
200
|
-
return;
|
|
201
|
-
const root = await realpath(roots.pluginRoot);
|
|
202
|
-
const data = await realpath(roots.pluginData);
|
|
203
|
-
if (root !== roots.pluginRoot || data !== roots.pluginData)
|
|
204
|
-
throw new Error('Supply canonical filesystem-resolved runtime roots.');
|
|
205
|
-
const cwd = await realpath(binding.cwd);
|
|
206
|
-
const expected = input.cwd?.startsWith('${PLUGIN_DATA}') ? data : root;
|
|
207
|
-
if (!within(cwd, expected))
|
|
208
|
-
throw new Error('Working directory resolves outside its declared root.');
|
|
209
|
-
if (input.command.startsWith('./') && !within(await realpath(binding.command), root))
|
|
210
|
-
throw new Error('Executable resolves outside the package root.');
|
|
211
|
-
}
|
|
212
|
-
//# sourceMappingURL=agent-plugins-harness.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"agent-plugins-harness.js","sourceRoot":"","sources":["../src/agent-plugins-harness.ts"],"names":[],"mappings":"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"}
|