@aiwg/cli 2026.8.0 → 2026.8.2
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 +33 -0
- package/agentic/code/providers/capability-matrix.yaml +511 -0
- package/agentic/code/providers/model-capabilities.v1.json +120 -0
- package/agentic/code/providers/model-catalog.v1.json +96 -0
- package/agentic/code/providers/model-policy-evaluations.v1.json +50 -0
- package/agentic/code/providers/premium-model-allowlist.v1.json +36 -0
- package/bin/aiwg.mjs +14 -10
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/cli.js +2 -0
- package/dist/src/artifacts/types.js +4 -0
- package/dist/src/auth/client.js +209 -0
- package/dist/src/auth/config.js +38 -0
- package/dist/src/auth/credential-store.js +141 -0
- package/dist/src/auth/resource-credentials.js +25 -0
- package/dist/src/auth/types.js +2 -0
- package/dist/src/channel/manager.mjs +5 -5
- package/dist/src/cli/handlers/auth.js +125 -0
- package/dist/src/cli/handlers/help.js +1 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/install.js +42 -4
- package/dist/src/cli/handlers/marketplace.js +375 -122
- package/dist/src/cli/handlers/resource-versions.js +2 -0
- package/dist/src/cli/handlers/sessions.js +23 -5
- package/dist/src/cli/handlers/subcommands.js +10 -1
- package/dist/src/cli/handlers/use.js +342 -43
- package/dist/src/config/gitignore.js +1 -0
- package/dist/src/extensions/commands/definitions.js +19 -0
- package/dist/src/marketplace/exchange.js +602 -0
- package/dist/src/marketplace/provenance-types.js +19 -0
- package/dist/src/marketplace/provenance.js +834 -0
- package/dist/src/memory/canonical-context.js +342 -0
- package/dist/src/memory/context-pack.js +282 -0
- package/dist/src/memory/index.js +4 -0
- package/dist/src/memory/intake.js +118 -0
- package/dist/src/packages/adapters/git.js +79 -29
- package/dist/src/packages/package-discovery.js +81 -0
- package/dist/src/packages/package-registry.js +2 -0
- package/dist/src/packages/registry.js +119 -20
- package/dist/src/resources/resolver.js +1 -0
- package/dist/src/resources/web-release.d.ts +3 -1
- package/dist/src/resources/web-release.js +14 -6
- package/dist/src/serve/agentic-sandbox-fleet-client.js +213 -0
- package/dist/src/serve/fleet-mission-conductor.js +293 -0
- package/dist/src/sessions/index.js +1 -0
- package/dist/src/sessions/output-registration.js +338 -0
- package/dist/src/sessions/promotion.js +73 -2
- package/dist/src/sessions/repository.js +2 -1
- package/dist/src/update/notifier.mjs +13 -2
- package/package.json +8 -1
- package/tools/_resolve-impl.mjs +74 -0
- package/tools/agents/deploy-agents.mjs +962 -0
- package/tools/agents/providers/base.mjs +2954 -0
- package/tools/agents/providers/claude.mjs +711 -0
- package/tools/agents/providers/codex.mjs +699 -0
- package/tools/agents/providers/copilot.mjs +659 -0
- package/tools/agents/providers/cursor.mjs +714 -0
- package/tools/agents/providers/factory.mjs +1130 -0
- package/tools/agents/providers/hermes.mjs +663 -0
- package/tools/agents/providers/hook-capabilities.mjs +85 -0
- package/tools/agents/providers/model-role.mjs +56 -0
- package/tools/agents/providers/openclaw-translator.mjs +348 -0
- package/tools/agents/providers/openclaw.mjs +680 -0
- package/tools/agents/providers/opencode.mjs +675 -0
- package/tools/agents/providers/openhuman.mjs +292 -0
- package/tools/agents/providers/warp.mjs +413 -0
- package/tools/agents/providers/windsurf.mjs +748 -0
- package/tools/commands/deploy-prompts-codex.mjs +336 -0
- package/tools/plugin/package-plugins.mjs +1013 -0
- package/tools/skills/deploy-skills-codex.mjs +571 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { basename, dirname, extname, relative, resolve, sep } from 'node:path';
|
|
4
|
+
function sha256(value) {
|
|
5
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
6
|
+
}
|
|
7
|
+
function safeProjectFile(projectRoot, requested) {
|
|
8
|
+
const root = realpathSync(projectRoot);
|
|
9
|
+
const candidate = realpathSync(resolve(root, requested));
|
|
10
|
+
if (candidate !== root && !candidate.startsWith(`${root}${sep}`)) {
|
|
11
|
+
throw new Error('intake source must resolve inside the project');
|
|
12
|
+
}
|
|
13
|
+
const segments = relative(root, candidate).split(sep);
|
|
14
|
+
if (segments.some(segment => segment === '.env' || segment === '.ssh'
|
|
15
|
+
|| /^(?:credentials?|secrets?|tokens?)(?:\.|$)/i.test(segment))) {
|
|
16
|
+
throw new Error('protected paths cannot be ingested into ordinary project memory');
|
|
17
|
+
}
|
|
18
|
+
return { absolute: candidate, locator: segments.join('/') };
|
|
19
|
+
}
|
|
20
|
+
function kindFor(filePath) {
|
|
21
|
+
const extension = extname(filePath).toLocaleLowerCase();
|
|
22
|
+
if (['.jsonl', '.transcript'].includes(extension))
|
|
23
|
+
return 'session-transcript';
|
|
24
|
+
if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'].includes(extension))
|
|
25
|
+
return 'image';
|
|
26
|
+
if (extension === '.pdf')
|
|
27
|
+
return 'pdf';
|
|
28
|
+
if (['.yaml', '.yml', '.json'].includes(extension))
|
|
29
|
+
return 'structured';
|
|
30
|
+
if (['.md', '.txt', '.html', '.htm'].includes(extension))
|
|
31
|
+
return 'document';
|
|
32
|
+
return 'artifact';
|
|
33
|
+
}
|
|
34
|
+
function assertFuturePathInsideProject(projectRoot, target) {
|
|
35
|
+
let ancestor = target;
|
|
36
|
+
while (!existsSync(ancestor))
|
|
37
|
+
ancestor = dirname(ancestor);
|
|
38
|
+
const actual = realpathSync(ancestor);
|
|
39
|
+
if (actual !== projectRoot && !actual.startsWith(`${projectRoot}${sep}`)) {
|
|
40
|
+
throw new Error('intake storage cannot traverse a link outside the project');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function atomicJson(filePath, value) {
|
|
44
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
45
|
+
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
46
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
47
|
+
renameSync(temporary, filePath);
|
|
48
|
+
}
|
|
49
|
+
export class MemoryIntakeCoordinator {
|
|
50
|
+
projectRoot;
|
|
51
|
+
rawRoot;
|
|
52
|
+
receiptRoot;
|
|
53
|
+
constructor(projectRoot) {
|
|
54
|
+
this.projectRoot = realpathSync(projectRoot);
|
|
55
|
+
this.rawRoot = resolve(this.projectRoot, '.aiwg/wiki/raw');
|
|
56
|
+
this.receiptRoot = resolve(this.projectRoot, '.aiwg/memory/compound-memory/intake-receipts');
|
|
57
|
+
assertFuturePathInsideProject(this.projectRoot, this.rawRoot);
|
|
58
|
+
assertFuturePathInsideProject(this.projectRoot, this.receiptRoot);
|
|
59
|
+
}
|
|
60
|
+
preview(requested) {
|
|
61
|
+
const source = safeProjectFile(this.projectRoot, requested);
|
|
62
|
+
const stat = statSync(source.absolute);
|
|
63
|
+
if (!stat.isFile())
|
|
64
|
+
throw new Error('intake source must be a regular file');
|
|
65
|
+
const bytes = readFileSync(source.absolute);
|
|
66
|
+
const digest = sha256(bytes);
|
|
67
|
+
const kind = kindFor(source.absolute);
|
|
68
|
+
const safeName = basename(source.absolute).replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
69
|
+
const rawLocator = `.aiwg/wiki/raw/${digest.slice(7, 23)}-${safeName}`;
|
|
70
|
+
const rawPath = resolve(this.projectRoot, rawLocator);
|
|
71
|
+
const duplicate = existsSync(rawPath) && sha256(readFileSync(rawPath)) === digest;
|
|
72
|
+
const identity = {
|
|
73
|
+
source: { locator: source.locator, digest, byteLength: bytes.length, kind },
|
|
74
|
+
rawLocator,
|
|
75
|
+
route: kind === 'session-transcript' ? 'sessions' : 'llm-wiki',
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
schemaVersion: 'aiwg.compound-memory.intake-preview.v1',
|
|
79
|
+
operationId: sha256(JSON.stringify(identity)),
|
|
80
|
+
...identity,
|
|
81
|
+
duplicate,
|
|
82
|
+
confirmationRequired: true,
|
|
83
|
+
mutation: { wouldCopyRaw: !duplicate, wouldPromoteKnowledge: false },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
confirm(requested, operationId) {
|
|
87
|
+
const preview = this.preview(requested);
|
|
88
|
+
const receiptPath = resolve(this.receiptRoot, `${operationId.replace(':', '_')}.json`);
|
|
89
|
+
if (existsSync(receiptPath)) {
|
|
90
|
+
return { ...JSON.parse(readFileSync(receiptPath, 'utf8')), duplicate: true };
|
|
91
|
+
}
|
|
92
|
+
if (preview.operationId !== operationId) {
|
|
93
|
+
throw new Error('intake confirmation requires the exact current preview');
|
|
94
|
+
}
|
|
95
|
+
const source = safeProjectFile(this.projectRoot, requested);
|
|
96
|
+
const rawPath = resolve(this.projectRoot, preview.rawLocator);
|
|
97
|
+
mkdirSync(this.rawRoot, { recursive: true, mode: 0o700 });
|
|
98
|
+
if (!preview.duplicate)
|
|
99
|
+
copyFileSync(source.absolute, rawPath, 1);
|
|
100
|
+
if (sha256(readFileSync(rawPath)) !== preview.source.digest) {
|
|
101
|
+
throw new Error('immutable raw copy digest does not match the source preview');
|
|
102
|
+
}
|
|
103
|
+
const receipt = {
|
|
104
|
+
schemaVersion: 'aiwg.compound-memory.intake-receipt.v1',
|
|
105
|
+
receiptId: sha256(`${operationId}\0${preview.rawLocator}`),
|
|
106
|
+
operationId,
|
|
107
|
+
sourceLocator: preview.source.locator,
|
|
108
|
+
sourceDigest: preview.source.digest,
|
|
109
|
+
rawLocator: preview.rawLocator,
|
|
110
|
+
route: preview.route,
|
|
111
|
+
duplicate: preview.duplicate,
|
|
112
|
+
registeredAt: new Date().toISOString(),
|
|
113
|
+
};
|
|
114
|
+
atomicJson(receiptPath, receipt);
|
|
115
|
+
return receipt;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=intake.js.map
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { execFile } from 'child_process';
|
|
13
13
|
import { promisify } from 'util';
|
|
14
|
-
import { mkdir, readFile } from 'fs/promises';
|
|
14
|
+
import { mkdir, mkdtemp, readFile, rename, rm } from 'fs/promises';
|
|
15
15
|
import { join } from 'path';
|
|
16
16
|
import { homedir } from 'os';
|
|
17
17
|
import { existsSync } from 'fs';
|
|
@@ -82,6 +82,41 @@ async function resolveLatestTag(gitUrl) {
|
|
|
82
82
|
}
|
|
83
83
|
return 'latest';
|
|
84
84
|
}
|
|
85
|
+
async function resolveRemoteCommit(gitUrl, requestedRef) {
|
|
86
|
+
if (/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(requestedRef))
|
|
87
|
+
return requestedRef;
|
|
88
|
+
const patterns = requestedRef === 'HEAD'
|
|
89
|
+
? ['HEAD']
|
|
90
|
+
: [`refs/tags/${requestedRef}^{}`, `refs/tags/${requestedRef}`, `refs/heads/${requestedRef}`, requestedRef];
|
|
91
|
+
try {
|
|
92
|
+
const output = await git(['ls-remote', gitUrl, ...patterns]);
|
|
93
|
+
const rows = output.split('\n').filter(Boolean).map((line) => {
|
|
94
|
+
const [oid = '', ref = ''] = line.split(/\s+/, 2);
|
|
95
|
+
return { oid, ref };
|
|
96
|
+
});
|
|
97
|
+
const peeled = rows.find((row) => row.ref.endsWith('^{}'));
|
|
98
|
+
const selected = peeled ?? rows[0];
|
|
99
|
+
return selected && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(selected.oid)
|
|
100
|
+
? selected.oid
|
|
101
|
+
: undefined;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function urlCacheIdentity(gitUrl) {
|
|
108
|
+
const urlKey = gitUrl
|
|
109
|
+
.replace(/^https?:\/\//, '')
|
|
110
|
+
.replace(/^ssh:\/\//, '')
|
|
111
|
+
.replace(/^git@/, '')
|
|
112
|
+
.replace(/\.git$/, '')
|
|
113
|
+
.replace(/[:/]/g, '_');
|
|
114
|
+
const parts = urlKey.split('_').filter(Boolean);
|
|
115
|
+
return {
|
|
116
|
+
name: parts[parts.length - 1] ?? 'package',
|
|
117
|
+
owner: parts[parts.length - 2] ?? 'unknown',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
85
120
|
/**
|
|
86
121
|
* GitAdapter
|
|
87
122
|
*
|
|
@@ -111,38 +146,53 @@ export class GitAdapter {
|
|
|
111
146
|
};
|
|
112
147
|
}
|
|
113
148
|
async fetch(source, options = {}) {
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
.replace(/^https?:\/\//, '')
|
|
122
|
-
.replace(/^git@/, '')
|
|
123
|
-
.replace(/\.git$/, '')
|
|
124
|
-
.replace(/[:/]/g, '_');
|
|
125
|
-
const parts = urlKey.split('_');
|
|
126
|
-
const name = parts[parts.length - 1] ?? 'package';
|
|
127
|
-
const owner = parts[parts.length - 2] ?? 'unknown';
|
|
128
|
-
const cachePath = buildCachePath(owner, name, version);
|
|
129
|
-
if (!options.refresh && existsSync(cachePath)) {
|
|
130
|
-
return cachePath;
|
|
149
|
+
// Resolve discovery inputs before any deployment and cache by immutable
|
|
150
|
+
// commit rather than by a movable tag/branch name (#2009).
|
|
151
|
+
let requestedRef = source.ref;
|
|
152
|
+
if (!requestedRef) {
|
|
153
|
+
const latest = await resolveLatestTag(source.gitUrl);
|
|
154
|
+
requestedRef = latest === 'latest' ? 'HEAD' : latest;
|
|
155
|
+
source.ref = requestedRef;
|
|
131
156
|
}
|
|
132
|
-
await
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
157
|
+
const advertisedCommit = await resolveRemoteCommit(source.gitUrl, requestedRef);
|
|
158
|
+
const { owner, name } = urlCacheIdentity(source.gitUrl);
|
|
159
|
+
if (advertisedCommit) {
|
|
160
|
+
const existing = buildCachePath(owner, name, advertisedCommit);
|
|
161
|
+
if (!options.refresh && existsSync(join(existing, '.git'))) {
|
|
162
|
+
const existingCommit = await git(['rev-parse', 'HEAD^{commit}'], existing);
|
|
163
|
+
if (existingCommit !== advertisedCommit)
|
|
164
|
+
throw new Error(`Immutable package cache mismatch at ${existing}`);
|
|
165
|
+
return existing;
|
|
166
|
+
}
|
|
136
167
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
168
|
+
const ownerDir = join(getCacheRoot(), owner);
|
|
169
|
+
await mkdir(ownerDir, { recursive: true });
|
|
170
|
+
const stage = await mkdtemp(join(ownerDir, `.${name}.resolve-`));
|
|
171
|
+
try {
|
|
172
|
+
await git(['clone', '--no-checkout', source.gitUrl, stage]);
|
|
173
|
+
await git(['checkout', '--detach', requestedRef], stage);
|
|
174
|
+
const resolvedCommit = await git(['rev-parse', 'HEAD^{commit}'], stage);
|
|
175
|
+
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(resolvedCommit)) {
|
|
176
|
+
throw new Error(`Git resolved '${requestedRef}' to invalid commit '${resolvedCommit}'`);
|
|
177
|
+
}
|
|
178
|
+
if (advertisedCommit && resolvedCommit !== advertisedCommit) {
|
|
179
|
+
throw new Error(`Git ref '${requestedRef}' moved during resolution (${advertisedCommit} -> ${resolvedCommit})`);
|
|
180
|
+
}
|
|
181
|
+
const cachePath = buildCachePath(owner, name, resolvedCommit);
|
|
182
|
+
if (existsSync(join(cachePath, '.git'))) {
|
|
183
|
+
const cachedCommit = await git(['rev-parse', 'HEAD^{commit}'], cachePath);
|
|
184
|
+
if (cachedCommit !== resolvedCommit)
|
|
185
|
+
throw new Error(`Immutable package cache mismatch at ${cachePath}`);
|
|
186
|
+
await rm(stage, { recursive: true, force: true });
|
|
187
|
+
return cachePath;
|
|
188
|
+
}
|
|
189
|
+
await rename(stage, cachePath);
|
|
190
|
+
return cachePath;
|
|
140
191
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
192
|
+
catch (error) {
|
|
193
|
+
await rm(stage, { recursive: true, force: true });
|
|
194
|
+
throw error;
|
|
144
195
|
}
|
|
145
|
-
return cachePath;
|
|
146
196
|
}
|
|
147
197
|
/** GitAdapter does not list packages */
|
|
148
198
|
async list() {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a fetched Git checkout to one deployable package without executing it.
|
|
3
|
+
* Supports root bundles and the documented standalone .aiwg/plugins layout.
|
|
4
|
+
*
|
|
5
|
+
* @implements #1997
|
|
6
|
+
* @implements #2009
|
|
7
|
+
*/
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { discoverProjectLocalBundles, loadAndValidateManifest } from '../extensions/project-local-discovery.js';
|
|
11
|
+
const TYPES = new Set(['framework', 'addon', 'extension', 'plugin', 'provider']);
|
|
12
|
+
function formatErrors(errors) {
|
|
13
|
+
return errors.map((error) => `${error.field}: expected ${error.expected}, got ${error.actual}${error.hint ? `; ${error.hint}` : ''}`).join('\n - ');
|
|
14
|
+
}
|
|
15
|
+
async function rootManifest(checkoutPath) {
|
|
16
|
+
try {
|
|
17
|
+
const value = JSON.parse(await readFile(path.join(checkoutPath, 'manifest.json'), 'utf8'));
|
|
18
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function discoverInstallablePackage(checkoutPath, selector) {
|
|
25
|
+
const checkout = path.resolve(checkoutPath);
|
|
26
|
+
const root = await rootManifest(checkout);
|
|
27
|
+
if (root) {
|
|
28
|
+
const declaredType = String(root.type ?? '');
|
|
29
|
+
if (root.manifestVersion === '1' && TYPES.has(declaredType)) {
|
|
30
|
+
const validation = await loadAndValidateManifest(path.join(checkout, 'manifest.json'), declaredType, checkout);
|
|
31
|
+
if (!validation.bundle) {
|
|
32
|
+
throw new Error(`Root package manifest validation failed:\n - ${formatErrors(validation.errors)}`);
|
|
33
|
+
}
|
|
34
|
+
if (selector && selector !== validation.bundle.id) {
|
|
35
|
+
throw new Error(`Package selector '${selector}' does not match root package '${validation.bundle.id}'`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
manifest: validation.bundle.manifest,
|
|
39
|
+
type: validation.bundle.type === 'provider' ? 'unknown' : validation.bundle.type,
|
|
40
|
+
wrapperPath: checkout,
|
|
41
|
+
artifactPath: validation.bundle.artifactPath,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Legacy root manifests remain installable for compatibility, but unknown
|
|
45
|
+
// roots no longer report a successful zero-artifact deployment.
|
|
46
|
+
if (['framework', 'addon', 'extension'].includes(String(root.type))) {
|
|
47
|
+
const name = String(root.id ?? root.name ?? '');
|
|
48
|
+
if (selector && selector !== name)
|
|
49
|
+
throw new Error(`Package selector '${selector}' does not match root package '${name}'`);
|
|
50
|
+
return {
|
|
51
|
+
manifest: root,
|
|
52
|
+
type: root.type,
|
|
53
|
+
wrapperPath: checkout,
|
|
54
|
+
artifactPath: checkout,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const discovery = await discoverProjectLocalBundles(checkout);
|
|
59
|
+
if (discovery.errors.length) {
|
|
60
|
+
throw new Error(`Standalone package discovery failed:\n - ${formatErrors(discovery.errors)}`);
|
|
61
|
+
}
|
|
62
|
+
let candidates = discovery.bundles.filter((bundle) => bundle.type === 'plugin');
|
|
63
|
+
if (selector)
|
|
64
|
+
candidates = candidates.filter((bundle) => bundle.id === selector);
|
|
65
|
+
if (candidates.length === 0) {
|
|
66
|
+
throw new Error(selector
|
|
67
|
+
? `No valid standalone plugin '${selector}' exists in this Git repository`
|
|
68
|
+
: 'Git repository contains no valid root package or standalone .aiwg/plugins wrapper');
|
|
69
|
+
}
|
|
70
|
+
if (candidates.length > 1) {
|
|
71
|
+
throw new Error(`Git repository contains multiple standalone plugins (${candidates.map((bundle) => bundle.id).sort().join(', ')}); select one with --package <id>`);
|
|
72
|
+
}
|
|
73
|
+
const selected = candidates[0];
|
|
74
|
+
return {
|
|
75
|
+
manifest: selected.manifest,
|
|
76
|
+
type: 'plugin',
|
|
77
|
+
wrapperPath: selected.bundlePath,
|
|
78
|
+
artifactPath: selected.artifactPath,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=package-discovery.js.map
|
|
@@ -137,6 +137,8 @@ export async function listPackages(configDir) {
|
|
|
137
137
|
source: entry.source,
|
|
138
138
|
installedAt: entry.installedAt,
|
|
139
139
|
deployCount: entry.deployedTo?.length ?? 0,
|
|
140
|
+
...(entry.provenance?.lockId ? { lockId: entry.provenance.lockId } : {}),
|
|
141
|
+
...(entry.provenance?.verificationStatus ? { verificationStatus: entry.provenance.verificationStatus } : {}),
|
|
140
142
|
};
|
|
141
143
|
});
|
|
142
144
|
}
|
|
@@ -7,13 +7,17 @@
|
|
|
7
7
|
* @implements #557
|
|
8
8
|
*/
|
|
9
9
|
import { readFile } from 'fs/promises';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import { join, relative, sep } from 'path';
|
|
12
|
+
import { GitAdapter } from './adapters/git.js';
|
|
12
13
|
import { GiteaAdapter } from './adapters/gitea.js';
|
|
13
14
|
import { GitHubAdapter } from './adapters/github.js';
|
|
14
15
|
import { ClawHubPackageAdapter } from './adapters/clawhub.js';
|
|
15
16
|
import { LocalCacheAdapter } from './adapters/local-cache.js';
|
|
16
|
-
import { setPackageEntry, listPackages as listFromRegistry, removePackageEntry, } from './package-registry.js';
|
|
17
|
+
import { getPackageEntry, setPackageEntry, listPackages as listFromRegistry, removePackageEntry, } from './package-registry.js';
|
|
18
|
+
import { discoverInstallablePackage } from './package-discovery.js';
|
|
19
|
+
import { buildFortemiEnvelopeShard, canonicalJson, createOperationReceipt, createProvenanceEnvelope, sha256, validateProvenanceEnvelope, verifyProvenanceEnvelope, } from '../marketplace/provenance.js';
|
|
20
|
+
import { findIndexedPackage, readMarketplaceIndex, readTrustStore, recordInstalledPackage, } from '../marketplace/exchange.js';
|
|
17
21
|
/**
|
|
18
22
|
* All adapters in resolution priority order
|
|
19
23
|
* (Scheme-prefixed adapters first so explicit prefixes are matched before
|
|
@@ -26,6 +30,9 @@ const ALL_ADAPTERS = [
|
|
|
26
30
|
new GitAdapter(),
|
|
27
31
|
];
|
|
28
32
|
const CACHE_ADAPTER = new LocalCacheAdapter();
|
|
33
|
+
function pathSeparatorSafe(parent, child) {
|
|
34
|
+
return relative(parent, child).replaceAll(sep, '/') || '.';
|
|
35
|
+
}
|
|
29
36
|
/**
|
|
30
37
|
* Parse a raw reference string into a PackageRef
|
|
31
38
|
*
|
|
@@ -176,26 +183,118 @@ export async function installPackage(rawRef, options = {}) {
|
|
|
176
183
|
` git@host:owner/name.git (SSH URL)`);
|
|
177
184
|
}
|
|
178
185
|
const { source, adapter } = resolved;
|
|
186
|
+
if (options.ref)
|
|
187
|
+
source.ref = options.ref;
|
|
179
188
|
const cachePath = await adapter.fetch(source, { refresh: options.refresh });
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
const discovered = await discoverInstallablePackage(cachePath, options.packageSelector);
|
|
190
|
+
const standardEnvelopePaths = [
|
|
191
|
+
join(cachePath, '.aiwg', 'marketplace', 'envelope.json'),
|
|
192
|
+
join(cachePath, 'aiwg-marketplace-envelope.json'),
|
|
193
|
+
];
|
|
194
|
+
let envelope = options.expectedEnvelope;
|
|
195
|
+
if (!envelope) {
|
|
196
|
+
for (const filename of standardEnvelopePaths) {
|
|
197
|
+
if (!existsSync(filename))
|
|
198
|
+
continue;
|
|
199
|
+
const value = JSON.parse(await readFile(filename, 'utf8'));
|
|
200
|
+
validateProvenanceEnvelope(value);
|
|
201
|
+
envelope = value;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!envelope) {
|
|
206
|
+
envelope = await createProvenanceEnvelope({
|
|
207
|
+
checkoutPath: cachePath,
|
|
208
|
+
artifactPath: discovered.artifactPath,
|
|
209
|
+
wrapperPath: pathSeparatorSafe(cachePath, discovered.wrapperPath),
|
|
210
|
+
manifest: discovered.manifest,
|
|
211
|
+
requestedRef: source.ref ?? ref.version ?? 'HEAD',
|
|
212
|
+
remote: source.gitUrl,
|
|
213
|
+
publisher: String(discovered.manifest.author ?? discovered.manifest.namespace ?? ref.owner ?? 'unverified'),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const key = `${envelope.package.namespace}/${envelope.package.name}`;
|
|
217
|
+
const namespace = envelope.package.namespace;
|
|
218
|
+
const type = discovered.type;
|
|
219
|
+
const existingEntry = await getPackageEntry(key, options.configDir);
|
|
220
|
+
let previousLock;
|
|
221
|
+
if (existingEntry?.provenance?.lockId) {
|
|
222
|
+
previousLock = (await findIndexedPackage(existingEntry.provenance.lockId, { configDir: options.configDir }))?.lock;
|
|
223
|
+
}
|
|
224
|
+
const localIndex = await readMarketplaceIndex({ configDir: options.configDir });
|
|
225
|
+
const installedLocks = Object.fromEntries(Object.values(localIndex.packages).map((entry) => [entry.lock.identity, entry.lock.lockId]));
|
|
226
|
+
const trustStore = options.trustStore ?? await readTrustStore({ configDir: options.configDir });
|
|
227
|
+
const verification = await verifyProvenanceEnvelope({
|
|
228
|
+
envelope,
|
|
229
|
+
contentRoot: discovered.artifactPath,
|
|
230
|
+
checkoutPath: cachePath,
|
|
231
|
+
trustStore,
|
|
232
|
+
policy: options.verify
|
|
233
|
+
? { requireSignature: true, allowIntegrityOnly: false, ...options.verificationPolicy }
|
|
234
|
+
: options.verificationPolicy,
|
|
235
|
+
installedLocks,
|
|
236
|
+
previousLock,
|
|
237
|
+
});
|
|
238
|
+
if (!verification.ok)
|
|
239
|
+
throw new Error(`Package verification failed: ${verification.errors.join('; ')}`);
|
|
240
|
+
if (options.expectedLockId && verification.lock.lockId !== options.expectedLockId) {
|
|
241
|
+
throw new Error(`Catalog/direct lock mismatch: expected ${options.expectedLockId}, got ${verification.lock.lockId}`);
|
|
242
|
+
}
|
|
243
|
+
if (options.expectedEnvelope && sha256(canonicalJson(options.expectedEnvelope)) !== verification.envelopeSha256) {
|
|
244
|
+
throw new Error('Catalog envelope changed before installation');
|
|
245
|
+
}
|
|
246
|
+
const fortemi = await buildFortemiEnvelopeShard(envelope);
|
|
247
|
+
const receipt = createOperationReceipt({
|
|
248
|
+
operation: 'install',
|
|
249
|
+
lock: verification.lock,
|
|
250
|
+
actor: options.actor ?? 'aiwg',
|
|
251
|
+
verificationStatus: verification.status,
|
|
252
|
+
evidence: {
|
|
253
|
+
source: source.gitUrl,
|
|
254
|
+
requestedRef: envelope.source.requestedRef,
|
|
255
|
+
resolvedCommit: envelope.source.resolvedCommit,
|
|
256
|
+
catalog: options.catalogId ?? null,
|
|
257
|
+
directGit: !options.catalogId,
|
|
258
|
+
},
|
|
259
|
+
conformance: fortemi.conformance,
|
|
260
|
+
});
|
|
261
|
+
await recordInstalledPackage({
|
|
262
|
+
configDir: options.configDir,
|
|
263
|
+
envelope,
|
|
264
|
+
lock: verification.lock,
|
|
265
|
+
receipt,
|
|
194
266
|
cachePath,
|
|
195
|
-
|
|
196
|
-
|
|
267
|
+
artifactPath: discovered.artifactPath,
|
|
268
|
+
verificationStatus: verification.status,
|
|
269
|
+
fortemiShard: fortemi.archive,
|
|
270
|
+
catalogId: options.catalogId,
|
|
271
|
+
});
|
|
272
|
+
await setPackageEntry(key, {
|
|
273
|
+
version: envelope.package.version,
|
|
274
|
+
source: envelope.source.canonicalRemote,
|
|
275
|
+
type: type === 'plugin' ? 'extension' : type,
|
|
276
|
+
cachePath: discovered.artifactPath,
|
|
277
|
+
installedAt: receipt.occurredAt,
|
|
278
|
+
deployedTo: existingEntry?.deployedTo ?? [],
|
|
279
|
+
provenance: {
|
|
280
|
+
lockId: verification.lock.lockId,
|
|
281
|
+
resolvedCommit: verification.lock.resolvedCommit,
|
|
282
|
+
treeSha256: verification.lock.treeSha256,
|
|
283
|
+
artifactSha256: verification.lock.artifactSha256,
|
|
284
|
+
envelopeSha256: verification.lock.envelopeSha256,
|
|
285
|
+
verificationStatus: verification.status,
|
|
286
|
+
},
|
|
197
287
|
}, options.configDir);
|
|
198
|
-
return {
|
|
288
|
+
return {
|
|
289
|
+
cachePath: discovered.artifactPath,
|
|
290
|
+
checkoutPath: cachePath,
|
|
291
|
+
key,
|
|
292
|
+
type,
|
|
293
|
+
namespace,
|
|
294
|
+
envelope,
|
|
295
|
+
lock: verification.lock,
|
|
296
|
+
verification,
|
|
297
|
+
};
|
|
199
298
|
}
|
|
200
299
|
/**
|
|
201
300
|
* Refresh all registered remote packages (used by `aiwg sync`)
|
|
@@ -117,6 +117,7 @@ export async function resolveAiwgResourceBytes(logicalId, options) {
|
|
|
117
117
|
const bytes = await fetchVerifiedRawResource(webRelease, parsed.rawPath, {
|
|
118
118
|
baseUrl: options.webReleaseOptions?.baseUrl,
|
|
119
119
|
fetcher: options.webReleaseOptions?.fetcher,
|
|
120
|
+
credentialProvider: options.webReleaseOptions?.credentialProvider,
|
|
120
121
|
allowInsecureLoopbackHttp: options.webReleaseOptions?.allowInsecureLoopbackHttp,
|
|
121
122
|
offline: options.offline,
|
|
122
123
|
});
|
|
@@ -45,6 +45,8 @@ export interface WebReleaseOptions {
|
|
|
45
45
|
cacheRoot?: string;
|
|
46
46
|
publicKeyPem?: string | Buffer;
|
|
47
47
|
fetcher?: ResourceFetcher;
|
|
48
|
+
/** Returns a bearer token at request time. Tokens never participate in URLs or cache keys. */
|
|
49
|
+
credentialProvider?: () => Promise<string | null>;
|
|
48
50
|
/** Test/development escape hatch. HTTP remains restricted to loopback. */
|
|
49
51
|
allowInsecureLoopbackHttp?: boolean;
|
|
50
52
|
}
|
|
@@ -72,7 +74,7 @@ export interface VerifiedWebRelease {
|
|
|
72
74
|
channelSequence?: number;
|
|
73
75
|
descriptors: ReadonlyMap<string, VerifiedReleaseDescriptor>;
|
|
74
76
|
}
|
|
75
|
-
export interface VerifiedRawResourceOptions extends Pick<WebReleaseOptions, "baseUrl" | "fetcher" | "allowInsecureLoopbackHttp"> {
|
|
77
|
+
export interface VerifiedRawResourceOptions extends Pick<WebReleaseOptions, "baseUrl" | "fetcher" | "credentialProvider" | "allowInsecureLoopbackHttp"> {
|
|
76
78
|
offline?: boolean;
|
|
77
79
|
}
|
|
78
80
|
export declare function verifySignedResourceBytes(bytes: Uint8Array, signatureBytes: Uint8Array, publicKeyPem?: string | Buffer, label?: string): string;
|
|
@@ -297,7 +297,7 @@ function resourceUrl(base, relativePath) {
|
|
|
297
297
|
url.pathname = `${prefix}/${relativePath}`;
|
|
298
298
|
return url.toString();
|
|
299
299
|
}
|
|
300
|
-
async function fetchBytes(fetcher, url, label, maxBytes) {
|
|
300
|
+
async function fetchBytes(fetcher, url, label, maxBytes, bearerToken) {
|
|
301
301
|
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0)
|
|
302
302
|
throw new Error(`${label} has an invalid network size limit`);
|
|
303
303
|
const controller = new AbortController();
|
|
@@ -315,7 +315,10 @@ async function fetchBytes(fetcher, url, label, maxBytes) {
|
|
|
315
315
|
const response = await Promise.race([
|
|
316
316
|
fetcher(url, {
|
|
317
317
|
redirect: "error",
|
|
318
|
-
headers: {
|
|
318
|
+
headers: {
|
|
319
|
+
"accept-encoding": "identity",
|
|
320
|
+
...(bearerToken ? { authorization: `Bearer ${bearerToken}` } : {}),
|
|
321
|
+
},
|
|
319
322
|
signal: controller.signal,
|
|
320
323
|
}),
|
|
321
324
|
timeoutFailure,
|
|
@@ -904,16 +907,21 @@ export async function resolveWebRelease(options = {}) {
|
|
|
904
907
|
const base = normalizeBaseUrl(options.baseUrl ?? DEFAULT_RESOURCE_BASE_URL, options.allowInsecureLoopbackHttp === true);
|
|
905
908
|
// Validate injected trust material before reading cache or making requests.
|
|
906
909
|
loadTrustRoot(publicKeyPem);
|
|
910
|
+
const bearerToken = options.offline ? null : await options.credentialProvider?.() ?? null;
|
|
911
|
+
const authorize = async (input, init = {}) => {
|
|
912
|
+
const headers = { ...(init.headers || {}), ...(bearerToken ? { authorization: `Bearer ${bearerToken}` } : {}) };
|
|
913
|
+
return (options.fetcher ?? globalThis.fetch)(input, { ...init, headers });
|
|
914
|
+
};
|
|
907
915
|
if (selector.kind === "exact") {
|
|
908
916
|
if (options.offline)
|
|
909
917
|
return resolveOfflineExact(cacheRoot, selector, selector.value, publicKeyPem, base);
|
|
910
|
-
const fetcher =
|
|
918
|
+
const fetcher = authorize;
|
|
911
919
|
if (!fetcher)
|
|
912
920
|
throw new Error("No fetch implementation is available for AIWG web resources");
|
|
913
921
|
return fetchAndCacheRelease(base, fetcher, cacheRoot, selector, selector.value, publicKeyPem);
|
|
914
922
|
}
|
|
915
923
|
if (selector.kind === "range" || selector.kind === "digest") {
|
|
916
|
-
const fetcher =
|
|
924
|
+
const fetcher = authorize;
|
|
917
925
|
const index = await resolveVersionIndex(base, fetcher, cacheRoot, publicKeyPem, options.offline);
|
|
918
926
|
const selected = selectVersionFromIndex(index, selector);
|
|
919
927
|
if (options.offline) {
|
|
@@ -929,7 +937,7 @@ export async function resolveWebRelease(options = {}) {
|
|
|
929
937
|
throw new Error(`AIWG resource channel ${selector.value} is not cached; offline mode cannot fetch it`);
|
|
930
938
|
return resolveOfflineExact(cacheRoot, selector, cached.manifest.version, publicKeyPem, base, cached.manifest.releaseManifestSha256, cached.manifest.sequence);
|
|
931
939
|
}
|
|
932
|
-
const fetcher =
|
|
940
|
+
const fetcher = authorize;
|
|
933
941
|
if (!fetcher)
|
|
934
942
|
throw new Error("No fetch implementation is available for AIWG web resources");
|
|
935
943
|
const channelPrefix = `resources/channels/${selector.value}`;
|
|
@@ -989,7 +997,7 @@ export async function fetchVerifiedRawResource(release, resourcePath, options =
|
|
|
989
997
|
const fetcher = options.fetcher ?? globalThis.fetch;
|
|
990
998
|
if (!fetcher)
|
|
991
999
|
throw new Error("No fetch implementation is available for AIWG web resources");
|
|
992
|
-
const bytes = await fetchBytes(fetcher, resourceUrl(base, `resources/${release.version}/${resourcePath}`), `raw resource ${resourcePath}`, Math.min(descriptor.size, MAX_RAW_RESOURCE_BYTES));
|
|
1000
|
+
const bytes = await fetchBytes(fetcher, resourceUrl(base, `resources/${release.version}/${resourcePath}`), `raw resource ${resourcePath}`, Math.min(descriptor.size, MAX_RAW_RESOURCE_BYTES), await options.credentialProvider?.() ?? null);
|
|
993
1001
|
verifyDescriptor(bytes, descriptor);
|
|
994
1002
|
const stagingRoot = path.join(release.cacheDir, ".raw-staging");
|
|
995
1003
|
fs.mkdirSync(stagingRoot, { recursive: true });
|