@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
package/src/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
3
|
import { promises as fs } from 'node:fs'
|
|
4
|
-
import { basename,
|
|
4
|
+
import { basename, join, relative, resolve } from 'node:path'
|
|
5
5
|
import { Readable } from 'node:stream'
|
|
6
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
6
7
|
import { createGunzip } from 'node:zlib'
|
|
7
8
|
import { extract } from 'tar-stream'
|
|
9
|
+
import { publishCatalog, type PublishOptions } from './catalog-store.js'
|
|
8
10
|
import {
|
|
9
11
|
CatalogSchema,
|
|
10
12
|
type Catalog,
|
|
@@ -59,6 +61,18 @@ import {
|
|
|
59
61
|
} from './site-icons.js'
|
|
60
62
|
import { normalizeDisplayName } from './display-name.js'
|
|
61
63
|
import { immutableAssetUrl } from './publication.js'
|
|
64
|
+
import { sourceDigest as bytesDigest } from './source-digest.js'
|
|
65
|
+
|
|
66
|
+
export { bytesDigest }
|
|
67
|
+
export {
|
|
68
|
+
catalogFingerprint,
|
|
69
|
+
publishCatalog,
|
|
70
|
+
withCatalogRead,
|
|
71
|
+
writeFileAtomically,
|
|
72
|
+
type CatalogCoordinationOptions,
|
|
73
|
+
type PublicationStep,
|
|
74
|
+
type PublishOptions,
|
|
75
|
+
} from './catalog-store.js'
|
|
62
76
|
|
|
63
77
|
/**
|
|
64
78
|
* Marks the catalog format this compiler emits.
|
|
@@ -297,17 +311,6 @@ export function digest(value: unknown): string {
|
|
|
297
311
|
return `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`
|
|
298
312
|
}
|
|
299
313
|
|
|
300
|
-
export function bytesDigest(files: ReadonlyMap<string, Uint8Array>): string {
|
|
301
|
-
const hash = createHash('sha256')
|
|
302
|
-
for (const [path, bytes] of [...files.entries()].toSorted(([left], [right]) =>
|
|
303
|
-
left.localeCompare(right)
|
|
304
|
-
)) {
|
|
305
|
-
hash.update(`${path.length}:${path}:${bytes.byteLength}:`)
|
|
306
|
-
hash.update(bytes)
|
|
307
|
-
}
|
|
308
|
-
return `sha256:${hash.digest('hex')}`
|
|
309
|
-
}
|
|
310
|
-
|
|
311
314
|
export function stablePluginId(sourceId: string, upstreamName: string): string {
|
|
312
315
|
return `plugin:${slug(sourceId)}:${slug(upstreamName)}`
|
|
313
316
|
}
|
|
@@ -345,20 +348,26 @@ export async function synchronize(input: SyncInput): Promise<SyncResult> {
|
|
|
345
348
|
sourceId: config.sourceId,
|
|
346
349
|
commitSha,
|
|
347
350
|
}))
|
|
351
|
+
const resolvedLock = createSourcesLock(resolved)
|
|
348
352
|
const unchanged =
|
|
349
353
|
input.fromLock === undefined &&
|
|
350
354
|
input.forceRebuild !== true &&
|
|
351
355
|
input.existingLock !== undefined &&
|
|
352
356
|
resolved.length === input.existingLock.sources.length &&
|
|
353
|
-
|
|
357
|
+
resolvedLock.sources.every((source) => {
|
|
354
358
|
const previous = input.existingLock?.sources.find(
|
|
355
|
-
(entry) => entry.sourceId === source.
|
|
359
|
+
(entry) => entry.sourceId === source.sourceId
|
|
356
360
|
)
|
|
357
|
-
if (!previous) return false
|
|
361
|
+
if (!previous || sourceLockChanged(previous, source)) return false
|
|
362
|
+
if (!input.existingCatalog) return true
|
|
363
|
+
const priorCatalog = input.existingCatalog.sources.find(
|
|
364
|
+
(entry) => entry.sourceId === source.sourceId
|
|
365
|
+
)
|
|
366
|
+
const config = resolved.find((entry) => entry.config.sourceId === source.sourceId)!.config
|
|
358
367
|
return (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
368
|
+
!!priorCatalog &&
|
|
369
|
+
priorCatalog.displayName === config.displayName &&
|
|
370
|
+
priorCatalog.trustClassification === config.trustClassification
|
|
362
371
|
)
|
|
363
372
|
})
|
|
364
373
|
if (unchanged) {
|
|
@@ -398,7 +407,7 @@ export async function synchronize(input: SyncInput): Promise<SyncResult> {
|
|
|
398
407
|
...(input.transformRelease ? { transformRelease: input.transformRelease } : {}),
|
|
399
408
|
})
|
|
400
409
|
const catalog = buildResult.catalog
|
|
401
|
-
const lock =
|
|
410
|
+
const lock = resolvedLock
|
|
402
411
|
const artifacts = createArtifacts(orderLeadingPlugins(catalog, input.leading), lock, {
|
|
403
412
|
...(input.leading ? { leading: input.leading } : {}),
|
|
404
413
|
...(input.topCount !== undefined ? { topCount: input.topCount } : {}),
|
|
@@ -463,6 +472,7 @@ export async function buildCatalog(input: {
|
|
|
463
472
|
...(input.productCategories ? { productCategories: input.productCategories } : {}),
|
|
464
473
|
...(input.leading ? { leading: input.leading } : {}),
|
|
465
474
|
...(input.productIconOverrides ? { productIconOverrides: input.productIconOverrides } : {}),
|
|
475
|
+
...(input.iconAssetsDirectory ? { iconAssetsDirectory: input.iconAssetsDirectory } : {}),
|
|
466
476
|
})
|
|
467
477
|
).catalog
|
|
468
478
|
}
|
|
@@ -988,10 +998,7 @@ async function resolvePluginSource(
|
|
|
988
998
|
pin?.resolvedCommitSha ??
|
|
989
999
|
spec.sha ??
|
|
990
1000
|
(resolveExternalRefs
|
|
991
|
-
? await resolveGitRef(
|
|
992
|
-
spec.repositoryUrl,
|
|
993
|
-
spec.ref?.startsWith('refs/') ? spec.ref : `refs/heads/${spec.ref ?? 'main'}`
|
|
994
|
-
)
|
|
1001
|
+
? await resolveGitRef(spec.repositoryUrl, externalGitRef(spec.ref))
|
|
995
1002
|
: source.commitSha)
|
|
996
1003
|
return {
|
|
997
1004
|
repositoryUrl: canonicalUrl,
|
|
@@ -1247,6 +1254,18 @@ function findLicense(
|
|
|
1247
1254
|
}
|
|
1248
1255
|
}
|
|
1249
1256
|
|
|
1257
|
+
function sourceLockChanged(previous: SourceLockEntry, current: SourceLockEntry): boolean {
|
|
1258
|
+
return (
|
|
1259
|
+
previous.repositoryUrl !== current.repositoryUrl ||
|
|
1260
|
+
previous.defaultBranch !== current.defaultBranch ||
|
|
1261
|
+
previous.manifestPath !== current.manifestPath ||
|
|
1262
|
+
previous.marketplaceDialect !== current.marketplaceDialect ||
|
|
1263
|
+
previous.resolvedCommitSha !== current.resolvedCommitSha ||
|
|
1264
|
+
previous.sourceManifestDigest !== current.sourceManifestDigest ||
|
|
1265
|
+
digest(previous.pluginPins) !== digest(current.pluginPins)
|
|
1266
|
+
)
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1250
1269
|
function createSourcesLock(sources: readonly ResolvedSource[]): SourcesLock {
|
|
1251
1270
|
const entries: SourceLockEntry[] = sources
|
|
1252
1271
|
.map((source) => ({
|
|
@@ -1485,35 +1504,16 @@ export function iconAssets(index: ConsumerIndex): readonly PublishedAsset[] {
|
|
|
1485
1504
|
return [...assets.values()].toSorted((left, right) => left.name.localeCompare(right.name))
|
|
1486
1505
|
}
|
|
1487
1506
|
|
|
1507
|
+
/**
|
|
1508
|
+
* Publishes a verified artifact set to `generatedDirectory` under the output's
|
|
1509
|
+
* coordination lock. See `catalog-store.ts` for the publication contract.
|
|
1510
|
+
*/
|
|
1488
1511
|
export async function writeArtifacts(
|
|
1489
1512
|
generatedDirectory: string,
|
|
1490
|
-
artifacts: GeneratedArtifacts
|
|
1513
|
+
artifacts: GeneratedArtifacts,
|
|
1514
|
+
options: PublishOptions = {}
|
|
1491
1515
|
): Promise<void> {
|
|
1492
|
-
|
|
1493
|
-
await fs.mkdir(parent, { recursive: true })
|
|
1494
|
-
const temporary = await fs.mkdtemp(join(parent, '.catalog-'))
|
|
1495
|
-
try {
|
|
1496
|
-
for (const [name, content] of Object.entries(artifacts))
|
|
1497
|
-
await fs.writeFile(join(temporary, name), content, 'utf8')
|
|
1498
|
-
const backup = `${generatedDirectory}.previous`
|
|
1499
|
-
await fs.rm(backup, { recursive: true, force: true })
|
|
1500
|
-
let movedExisting = false
|
|
1501
|
-
try {
|
|
1502
|
-
await fs.rename(generatedDirectory, backup)
|
|
1503
|
-
movedExisting = true
|
|
1504
|
-
} catch (error) {
|
|
1505
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
1506
|
-
}
|
|
1507
|
-
try {
|
|
1508
|
-
await fs.rename(temporary, generatedDirectory)
|
|
1509
|
-
} catch (error) {
|
|
1510
|
-
if (movedExisting) await fs.rename(backup, generatedDirectory)
|
|
1511
|
-
throw error
|
|
1512
|
-
}
|
|
1513
|
-
if (movedExisting) await fs.rm(backup, { recursive: true, force: true })
|
|
1514
|
-
} finally {
|
|
1515
|
-
await fs.rm(temporary, { recursive: true, force: true })
|
|
1516
|
-
}
|
|
1516
|
+
await publishCatalog(generatedDirectory, artifacts as Record<string, string>, options)
|
|
1517
1517
|
}
|
|
1518
1518
|
|
|
1519
1519
|
export function verifyArtifacts(artifacts: GeneratedArtifacts): void {
|
|
@@ -1878,11 +1878,20 @@ function createChangeReport(
|
|
|
1878
1878
|
const previous = existingLock.sources.find(
|
|
1879
1879
|
(candidate) => candidate.sourceId === source.sourceId
|
|
1880
1880
|
)
|
|
1881
|
+
const priorCatalog = existingCatalog?.sources.find(
|
|
1882
|
+
(entry) => entry.sourceId === source.sourceId
|
|
1883
|
+
)
|
|
1884
|
+
const currentCatalog = catalog.sources.find(
|
|
1885
|
+
(entry) => entry.sourceId === source.sourceId
|
|
1886
|
+
)
|
|
1881
1887
|
return (
|
|
1882
1888
|
!previous ||
|
|
1883
|
-
previous
|
|
1884
|
-
|
|
1885
|
-
|
|
1889
|
+
sourceLockChanged(previous, source) ||
|
|
1890
|
+
(existingCatalog !== undefined &&
|
|
1891
|
+
(!priorCatalog ||
|
|
1892
|
+
!currentCatalog ||
|
|
1893
|
+
priorCatalog.displayName !== currentCatalog.displayName ||
|
|
1894
|
+
priorCatalog.trustClassification !== currentCatalog.trustClassification))
|
|
1886
1895
|
)
|
|
1887
1896
|
})()
|
|
1888
1897
|
)
|
|
@@ -2002,7 +2011,10 @@ async function resolvePluginPins(input: {
|
|
|
2002
2011
|
|
|
2003
2012
|
const repositoryUrl = canonicalRepositoryUrl(entry.source.repositoryUrl)
|
|
2004
2013
|
const pluginSubdirectory = entry.source.subdirectory
|
|
2005
|
-
|
|
2014
|
+
// Existing locks describe the prior sync; only explicit replay freezes revisions.
|
|
2015
|
+
const locked = input.fromLock
|
|
2016
|
+
? input.lockedPins?.find((pin) => pin.pluginName === entry.name)
|
|
2017
|
+
: undefined
|
|
2006
2018
|
if (input.fromLock && !locked)
|
|
2007
2019
|
throw new Error(`SOURCE_LOCK_PLUGIN_PIN_MISSING: ${input.config.sourceId}:${entry.name}`)
|
|
2008
2020
|
if (locked) {
|
|
@@ -2018,17 +2030,19 @@ async function resolvePluginPins(input: {
|
|
|
2018
2030
|
const resolvedCommitSha =
|
|
2019
2031
|
entry.source.sha ??
|
|
2020
2032
|
(input.resolveExternalRefs
|
|
2021
|
-
? await resolveGitRef(
|
|
2022
|
-
repositoryUrl,
|
|
2023
|
-
entry.source.ref?.startsWith('refs/')
|
|
2024
|
-
? entry.source.ref
|
|
2025
|
-
: `refs/heads/${entry.source.ref ?? 'main'}`
|
|
2026
|
-
)
|
|
2033
|
+
? await resolveGitRef(repositoryUrl, externalGitRef(entry.source.ref))
|
|
2027
2034
|
: input.sourceCommitSha)
|
|
2028
2035
|
return { pluginName: entry.name, repositoryUrl, pluginSubdirectory, resolvedCommitSha }
|
|
2029
2036
|
}
|
|
2030
2037
|
)
|
|
2031
|
-
return pins.toSorted((left, right) =>
|
|
2038
|
+
return pins.toSorted((left, right) =>
|
|
2039
|
+
left.pluginName < right.pluginName ? -1 : left.pluginName > right.pluginName ? 1 : 0
|
|
2040
|
+
)
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
function externalGitRef(ref: string | undefined): string {
|
|
2044
|
+
if (ref === undefined) return 'HEAD'
|
|
2045
|
+
return ref.startsWith('refs/') ? ref : `refs/heads/${ref}`
|
|
2032
2046
|
}
|
|
2033
2047
|
|
|
2034
2048
|
async function mapWithConcurrency<T, U>(
|
|
@@ -2061,9 +2075,14 @@ async function readFixtureManifest(
|
|
|
2061
2075
|
}
|
|
2062
2076
|
|
|
2063
2077
|
async function resolveGitRef(repositoryUrl: string, ref: string): Promise<string> {
|
|
2064
|
-
const
|
|
2065
|
-
|
|
2066
|
-
|
|
2078
|
+
const peeled = ref.startsWith('refs/tags/') && !ref.endsWith('^{}') ? `${ref}^{}` : undefined
|
|
2079
|
+
const result = spawn(
|
|
2080
|
+
'git',
|
|
2081
|
+
['ls-remote', canonicalRepositoryUrl(repositoryUrl), ref, ...(peeled ? [peeled] : [])],
|
|
2082
|
+
{
|
|
2083
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2084
|
+
}
|
|
2085
|
+
)
|
|
2067
2086
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2068
2087
|
new Promise<string>((resolveOutput, reject) => {
|
|
2069
2088
|
const chunks: Buffer[] = []
|
|
@@ -2084,7 +2103,13 @@ async function resolveGitRef(repositoryUrl: string, ref: string): Promise<string
|
|
|
2084
2103
|
])
|
|
2085
2104
|
if (exitCode !== 0)
|
|
2086
2105
|
throw new Error(`GIT_REF_LOOKUP_FAILED: ${repositoryUrl}:${ref}:${stderr.trim()}`)
|
|
2087
|
-
const
|
|
2106
|
+
const rows = stdout
|
|
2107
|
+
.trim()
|
|
2108
|
+
.split('\n')
|
|
2109
|
+
.map((line) => line.split(/\s+/))
|
|
2110
|
+
const sha =
|
|
2111
|
+
(peeled && rows.find((row) => row[1] === peeled)?.[0]) ??
|
|
2112
|
+
rows.find((row) => row[1] === ref)?.[0]
|
|
2088
2113
|
if (!sha || !/^[a-f0-9]{40}$/.test(sha))
|
|
2089
2114
|
throw new Error(`GIT_REF_NOT_FOUND: ${repositoryUrl}:${ref}`)
|
|
2090
2115
|
return sha
|
|
@@ -2111,7 +2136,8 @@ function rawUrl(repositoryUrl: string, commitSha: string, path: string): string
|
|
|
2111
2136
|
}
|
|
2112
2137
|
|
|
2113
2138
|
async function fetchText(url: string): Promise<string> {
|
|
2114
|
-
|
|
2139
|
+
// Manifest requests use the same bounded retry policy as the tree and archive.
|
|
2140
|
+
const response = await fetchUpstream(url, { headers: upstreamRequestHeaders(url) })
|
|
2115
2141
|
if (!response.ok) throw new Error(`HTTP_FETCH_FAILED: ${response.status}:${url}`)
|
|
2116
2142
|
return response.text()
|
|
2117
2143
|
}
|
|
@@ -2192,10 +2218,6 @@ const UPSTREAM_WEATHER_SKIP_CODES: ReadonlySet<PluginSkipReasonCode> = new Set([
|
|
|
2192
2218
|
'PLUGIN_PATH_NOT_FOUND',
|
|
2193
2219
|
])
|
|
2194
2220
|
|
|
2195
|
-
function sleep(ms: number): Promise<void> {
|
|
2196
|
-
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms))
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
2221
|
function upstreamRetryDelayMs(attempt: number, response?: Response): number {
|
|
2200
2222
|
const retryAfter = response?.headers.get('retry-after')
|
|
2201
2223
|
if (retryAfter !== null && retryAfter !== undefined) {
|
|
@@ -2230,8 +2252,8 @@ async function fetchUpstream(url: string, init: RequestInit): Promise<Response>
|
|
|
2230
2252
|
try {
|
|
2231
2253
|
response = await fetch(url, init)
|
|
2232
2254
|
} catch (error) {
|
|
2233
|
-
if (attempt >= UPSTREAM_FETCH_MAX_ATTEMPTS - 1) throw error
|
|
2234
|
-
await sleep(upstreamRetryDelayMs(attempt))
|
|
2255
|
+
if (init.signal?.aborted || attempt >= UPSTREAM_FETCH_MAX_ATTEMPTS - 1) throw error
|
|
2256
|
+
await sleep(upstreamRetryDelayMs(attempt), undefined, { signal: init.signal ?? undefined })
|
|
2235
2257
|
attempt += 1
|
|
2236
2258
|
continue
|
|
2237
2259
|
}
|
|
@@ -2241,7 +2263,10 @@ async function fetchUpstream(url: string, init: RequestInit): Promise<Response>
|
|
|
2241
2263
|
attempt >= UPSTREAM_FETCH_MAX_ATTEMPTS - 1
|
|
2242
2264
|
)
|
|
2243
2265
|
return response
|
|
2244
|
-
await
|
|
2266
|
+
await response.body?.cancel().catch(() => {})
|
|
2267
|
+
await sleep(upstreamRetryDelayMs(attempt, response), undefined, {
|
|
2268
|
+
signal: init.signal ?? undefined,
|
|
2269
|
+
})
|
|
2245
2270
|
attempt += 1
|
|
2246
2271
|
}
|
|
2247
2272
|
}
|
|
@@ -2343,8 +2368,8 @@ export async function readArchiveFiles(
|
|
|
2343
2368
|
|
|
2344
2369
|
export class NetworkSnapshotLoader implements SnapshotLoader {
|
|
2345
2370
|
readonly #policy: CatalogPolicy
|
|
2346
|
-
readonly #trees = new Map<string, readonly TreeEntry[]
|
|
2347
|
-
readonly #archives = new Map<string, ReadonlyMap<string, Uint8Array
|
|
2371
|
+
readonly #trees = new Map<string, Promise<readonly TreeEntry[]>>()
|
|
2372
|
+
readonly #archives = new Map<string, Promise<ReadonlyMap<string, Uint8Array>>>()
|
|
2348
2373
|
|
|
2349
2374
|
constructor(policy: CatalogPolicy) {
|
|
2350
2375
|
this.#policy = policy
|
|
@@ -2361,22 +2386,28 @@ export class NetworkSnapshotLoader implements SnapshotLoader {
|
|
|
2361
2386
|
const [owner, repo] = parsed.pathname.split('/').filter(Boolean)
|
|
2362
2387
|
if (!owner || !repo) throw new Error('GIT_REPOSITORY_INVALID')
|
|
2363
2388
|
const key = `${owner}/${repo}@${commitSha}`
|
|
2364
|
-
let
|
|
2365
|
-
if (!
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2389
|
+
let pendingTree = this.#trees.get(key)
|
|
2390
|
+
if (!pendingTree) {
|
|
2391
|
+
pendingTree = (async () => {
|
|
2392
|
+
const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${commitSha}?recursive=1`
|
|
2393
|
+
const response = await fetchUpstream(treeUrl, {
|
|
2394
|
+
headers: upstreamRequestHeaders(treeUrl),
|
|
2395
|
+
})
|
|
2396
|
+
if (!response.ok)
|
|
2397
|
+
throw new PluginSafetyError('PLUGIN_SOURCE_UNAVAILABLE', [`${owner}/${repo}`])
|
|
2398
|
+
const payload = (await response.json()) as { truncated?: boolean; tree?: TreeEntry[] }
|
|
2399
|
+
if (payload.truncated) throw new Error(`GIT_TREE_TRUNCATED: ${owner}/${repo}@${commitSha}`)
|
|
2400
|
+
return (payload.tree ?? []).map((entry) => ({
|
|
2401
|
+
...entry,
|
|
2402
|
+
path: safeRelativePath(entry.path, 'TREE_PATH_INVALID'),
|
|
2403
|
+
}))
|
|
2404
|
+
})().catch((error: unknown) => {
|
|
2405
|
+
this.#trees.delete(key)
|
|
2406
|
+
throw error
|
|
2369
2407
|
})
|
|
2370
|
-
|
|
2371
|
-
throw new PluginSafetyError('PLUGIN_SOURCE_UNAVAILABLE', [`${owner}/${repo}`])
|
|
2372
|
-
const payload = (await response.json()) as { truncated?: boolean; tree?: TreeEntry[] }
|
|
2373
|
-
if (payload.truncated) throw new Error(`GIT_TREE_TRUNCATED: ${owner}/${repo}@${commitSha}`)
|
|
2374
|
-
tree = (payload.tree ?? []).map((entry) => ({
|
|
2375
|
-
...entry,
|
|
2376
|
-
path: safeRelativePath(entry.path, 'TREE_PATH_INVALID'),
|
|
2377
|
-
}))
|
|
2378
|
-
this.#trees.set(key, tree)
|
|
2408
|
+
this.#trees.set(key, pendingTree)
|
|
2379
2409
|
}
|
|
2410
|
+
const tree = await pendingTree
|
|
2380
2411
|
const prefix = pluginSubdirectory ? `${safeRelativePath(pluginSubdirectory)}/` : ''
|
|
2381
2412
|
const submodules = tree.filter(
|
|
2382
2413
|
(entry) =>
|
|
@@ -2428,12 +2459,18 @@ export class NetworkSnapshotLoader implements SnapshotLoader {
|
|
|
2428
2459
|
): Promise<ReadonlyMap<string, Uint8Array>> {
|
|
2429
2460
|
const cached = this.#archives.get(key)
|
|
2430
2461
|
if (cached) return cached
|
|
2431
|
-
const
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2462
|
+
const pending = (async () => {
|
|
2463
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/tarball/${commitSha}`
|
|
2464
|
+
const response = await fetchUpstream(url, { headers: upstreamRequestHeaders(url) })
|
|
2465
|
+
if (!response.ok)
|
|
2466
|
+
throw new PluginSafetyError('PLUGIN_SOURCE_UNAVAILABLE', [`${owner}/${repo}`])
|
|
2467
|
+
return readArchiveFiles(new Uint8Array(await response.arrayBuffer()), this.#policy)
|
|
2468
|
+
})().catch((error: unknown) => {
|
|
2469
|
+
this.#archives.delete(key)
|
|
2470
|
+
throw error
|
|
2471
|
+
})
|
|
2472
|
+
this.#archives.set(key, pending)
|
|
2473
|
+
return pending
|
|
2437
2474
|
}
|
|
2438
2475
|
}
|
|
2439
2476
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { expect, test } from 'bun:test'
|
|
4
|
+
import { FixtureSnapshotLoader, synchronize } from './index.js'
|
|
5
|
+
import { withPortableFixture } from '../test-support/portable-fixture.js'
|
|
6
|
+
|
|
7
|
+
test('retries transient marketplace manifest responses while preserving permanent failures', () =>
|
|
8
|
+
withPortableFixture(async (input, root) => {
|
|
9
|
+
const initial = await synchronize(input)
|
|
10
|
+
const manifest = await readFile(join(root, 'fixtures/source/marketplace.json'), 'utf8')
|
|
11
|
+
const realFetch = globalThis.fetch
|
|
12
|
+
let calls = 0
|
|
13
|
+
try {
|
|
14
|
+
globalThis.fetch = (async () => {
|
|
15
|
+
calls++
|
|
16
|
+
if (calls < 3)
|
|
17
|
+
return new Response('temporarily unavailable', {
|
|
18
|
+
status: 503,
|
|
19
|
+
headers: { 'retry-after': '0' },
|
|
20
|
+
})
|
|
21
|
+
return new Response(manifest, { status: 200 })
|
|
22
|
+
}) as unknown as typeof fetch
|
|
23
|
+
const options = {
|
|
24
|
+
...input,
|
|
25
|
+
mode: 'live' as const,
|
|
26
|
+
fromLock: initial.lock!,
|
|
27
|
+
snapshotLoader: new FixtureSnapshotLoader(input.fixtureRoot!, input.sources),
|
|
28
|
+
}
|
|
29
|
+
const recovered = await synchronize(options)
|
|
30
|
+
expect(recovered.catalog?.catalogId).toBe(initial.catalog?.catalogId)
|
|
31
|
+
expect(calls).toBe(3)
|
|
32
|
+
calls = 0
|
|
33
|
+
globalThis.fetch = (async () => {
|
|
34
|
+
calls++
|
|
35
|
+
return new Response('not found', { status: 404 })
|
|
36
|
+
}) as unknown as typeof fetch
|
|
37
|
+
await expect(synchronize(options)).rejects.toThrow('HTTP_FETCH_FAILED: 404')
|
|
38
|
+
expect(calls).toBe(1)
|
|
39
|
+
calls = 0
|
|
40
|
+
globalThis.fetch = (async () => {
|
|
41
|
+
calls++
|
|
42
|
+
return new Response('busy', { status: 429, headers: { 'retry-after': '0' } })
|
|
43
|
+
}) as unknown as typeof fetch
|
|
44
|
+
await expect(synchronize(options)).rejects.toThrow('HTTP_FETCH_FAILED: 429')
|
|
45
|
+
expect(calls).toBe(3)
|
|
46
|
+
} finally {
|
|
47
|
+
globalThis.fetch = realFetch
|
|
48
|
+
}
|
|
49
|
+
}))
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
PACKAGE_METADATA_KEY,
|
|
13
13
|
} from './portable-catalog.js'
|
|
14
14
|
import { verifyAgentPackage, materializeAgentPackage } from './agent-plugins-catalog.js'
|
|
15
|
-
import { createCatalogInstallationPlan, createInstallationPlan } from './
|
|
15
|
+
import { createCatalogInstallationPlan, createInstallationPlan } from './harness.js'
|
|
16
16
|
import type { HarnessProfile } from './agent-plugins-schema.js'
|
|
17
17
|
import { snapshotFromDirectory } from './index.js'
|
|
18
18
|
import {
|
|
@@ -351,7 +351,7 @@ it('refuses a malformed provenance descriptor with one typed line naming the plu
|
|
|
351
351
|
}))
|
|
352
352
|
|
|
353
353
|
const CONSUMER_SCRIPT = `import { readFileSync } from 'node:fs'
|
|
354
|
-
import { createCatalogInstallationPlan } from '@adea-ai/plugins/
|
|
354
|
+
import { createCatalogInstallationPlan } from '@adea-ai/plugins/harness'
|
|
355
355
|
import { synchronizePortable } from '@adea-ai/plugins/portable-catalog'
|
|
356
356
|
import { digest } from '@adea-ai/plugins'
|
|
357
357
|
const request = JSON.parse(readFileSync(process.argv[2], 'utf8'))
|
|
@@ -377,7 +377,7 @@ try {
|
|
|
377
377
|
ok: true,
|
|
378
378
|
plan,
|
|
379
379
|
catalogId: result.catalog.catalogId,
|
|
380
|
-
resolvedHarness: import.meta.resolve('@adea-ai/plugins/
|
|
380
|
+
resolvedHarness: import.meta.resolve('@adea-ai/plugins/harness'),
|
|
381
381
|
resolvedCatalog: import.meta.resolve('@adea-ai/plugins/portable-catalog'),
|
|
382
382
|
})
|
|
383
383
|
)
|
|
@@ -475,7 +475,7 @@ it('keeps the packed consumer inside the packed artifact and its own dependencie
|
|
|
475
475
|
// The packed artifact was built from the CURRENT source: a stale build
|
|
476
476
|
// would still carry the sentinel the rebuild removes.
|
|
477
477
|
assert.equal(consumer.builtFromCurrentSource, true)
|
|
478
|
-
assert.equal(existsSync(join(consumer.packedRoot, 'dist', '
|
|
478
|
+
assert.equal(existsSync(join(consumer.packedRoot, 'dist', 'harness.js')), true)
|
|
479
479
|
// Every copied dependency really resolves INSIDE the consumer: the
|
|
480
480
|
// workspace installs them as links into the package manager's store, so
|
|
481
481
|
// a copy that kept the link would resolve outside the consumer.
|
|
@@ -487,7 +487,10 @@ it('keeps the packed consumer inside the packed artifact and its own dependencie
|
|
|
487
487
|
// dependencies, and never the whole workspace.
|
|
488
488
|
const packedManifest = JSON.parse(
|
|
489
489
|
await readFile(join(consumer.packedRoot, 'package.json'), 'utf8')
|
|
490
|
-
) as { dependencies?: Record<string, string> }
|
|
490
|
+
) as { dependencies?: Record<string, string>; exports: Record<string, unknown> }
|
|
491
|
+
assert.ok(packedManifest.exports['./harness'])
|
|
492
|
+
assert.equal(packedManifest.exports['./agent-plugins/harness'], undefined)
|
|
493
|
+
assert.equal(existsSync(join(consumer.packedRoot, 'dist', 'agent-plugins-harness.js')), false)
|
|
491
494
|
const declared = Object.keys(packedManifest.dependencies ?? {})
|
|
492
495
|
for (const name of declared) assert.ok(consumer.linkedDependencies.includes(name))
|
|
493
496
|
const installed = await readdir(join(consumer.consumerRoot, 'node_modules'))
|
package/src/publication.ts
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* that slot, so no version is ever the one a visitor sees first.
|
|
10
10
|
*
|
|
11
11
|
* The branch gives each catalog an immutable path — `catalogs/<catalogId>/…`,
|
|
12
|
-
* where `catalogId` is the canonical digest of the catalog itself
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* where `catalogId` is the canonical digest of the catalog itself. The
|
|
13
|
+
* publisher must reject replacement of existing snapshot bytes, and
|
|
14
|
+
* `raw.githubusercontent.com` serves the branch with permissive
|
|
15
15
|
* CORS so a browser can fetch it directly.
|
|
16
16
|
*/
|
|
17
17
|
|
|
@@ -63,11 +63,9 @@ export interface StagedAssetAudit {
|
|
|
63
63
|
/**
|
|
64
64
|
* Audits the bytes this build staged against what it declared.
|
|
65
65
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* therefore a different path. What survives is the check that cannot be
|
|
70
|
-
* delegated to the path — that the bytes exist here at all.
|
|
66
|
+
* This checks that the declared bytes exist here at all. Publication must
|
|
67
|
+
* separately compare the complete staged snapshot with any existing snapshot:
|
|
68
|
+
* companion artifacts can differ without changing the catalog identity.
|
|
71
69
|
*/
|
|
72
70
|
export function auditStagedAssets(input: {
|
|
73
71
|
readonly declared: readonly DeclaredAsset[]
|
|
@@ -95,9 +93,8 @@ export interface PublicationPlan {
|
|
|
95
93
|
/**
|
|
96
94
|
* The 64-hex digest a `catalog:<hex>` identity is addressed by.
|
|
97
95
|
*
|
|
98
|
-
* A catalog identity names
|
|
99
|
-
* snapshot path
|
|
100
|
-
* different identity and cannot write over this one.
|
|
96
|
+
* A catalog identity names the catalog content. The publisher must also guard
|
|
97
|
+
* the snapshot path against changes to companion artifacts under the same ID.
|
|
101
98
|
*/
|
|
102
99
|
export function catalogIdSuffix(catalogId: string): string {
|
|
103
100
|
const match = /^catalog:([a-f0-9]{64})$/.exec(catalogId)
|
package/src/schema.ts
CHANGED
|
@@ -248,45 +248,6 @@ export const SourcesLockSchema = z
|
|
|
248
248
|
})
|
|
249
249
|
.strict()
|
|
250
250
|
|
|
251
|
-
export const MaterializationPlanSchema = z
|
|
252
|
-
.object({
|
|
253
|
-
planVersion: z.literal(1),
|
|
254
|
-
pluginId: PluginSchema.shape.pluginId,
|
|
255
|
-
releaseId: PluginReleaseSchema.shape.releaseId,
|
|
256
|
-
harness: HarnessSchema,
|
|
257
|
-
source: z
|
|
258
|
-
.object({
|
|
259
|
-
repositoryUrl: z.string().url(),
|
|
260
|
-
commitSha: ShaSchema,
|
|
261
|
-
pluginSubdirectory: z.string().min(1),
|
|
262
|
-
contentDigest: DigestSchema,
|
|
263
|
-
})
|
|
264
|
-
.strict(),
|
|
265
|
-
targetLayout: z
|
|
266
|
-
.object({
|
|
267
|
-
root: z.string().min(1),
|
|
268
|
-
notes: z.array(z.string().min(1)).max(32),
|
|
269
|
-
})
|
|
270
|
-
.strict(),
|
|
271
|
-
files: z
|
|
272
|
-
.array(
|
|
273
|
-
z
|
|
274
|
-
.object({
|
|
275
|
-
sourcePath: z.string().min(1),
|
|
276
|
-
targetPath: z.string().min(1),
|
|
277
|
-
action: z.enum(['copy', 'translate', 'ignore', 'unsupported']),
|
|
278
|
-
reason: z.string().min(1).max(500),
|
|
279
|
-
})
|
|
280
|
-
.strict()
|
|
281
|
-
)
|
|
282
|
-
.max(4096),
|
|
283
|
-
configuration: JsonObjectSchema,
|
|
284
|
-
requiredConnectors: z.array(z.string().min(1)).max(128),
|
|
285
|
-
requiredCredentials: z.array(z.string().min(1)).max(128),
|
|
286
|
-
policyConstraints: z.array(z.string().min(1)).max(128),
|
|
287
|
-
})
|
|
288
|
-
.strict()
|
|
289
|
-
|
|
290
251
|
export type CatalogSource = z.output<typeof CatalogSourceSchema>
|
|
291
252
|
export type Capability = z.output<typeof CapabilitySchema>
|
|
292
253
|
export type HarnessCompatibility = z.output<typeof HarnessCompatibilitySchema>
|
|
@@ -298,7 +259,6 @@ export type Plugin = z.output<typeof PluginSchema>
|
|
|
298
259
|
export type Catalog = z.output<typeof CatalogSchema>
|
|
299
260
|
export type SourceLockEntry = z.output<typeof SourceLockEntrySchema>
|
|
300
261
|
export type SourcesLock = z.output<typeof SourcesLockSchema>
|
|
301
|
-
export type MaterializationPlan = z.output<typeof MaterializationPlanSchema>
|
|
302
262
|
|
|
303
263
|
export function parseCatalog(input: unknown): Catalog {
|
|
304
264
|
return CatalogSchema.parse(input)
|