@tiangong-ai/cli 0.0.21 → 0.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -2
- package/README.md +110 -10
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +25 -3
- package/dist/cli.js.map +1 -1
- package/dist/research/orchestration.js +117 -4
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/workspace/broker.js +38 -46
- package/dist/research/workspace/broker.js.map +1 -1
- package/dist/research/workspace/capabilities.d.ts +2 -0
- package/dist/research/workspace/capabilities.js +246 -7
- package/dist/research/workspace/capabilities.js.map +1 -1
- package/dist/research/workspace/constants.d.ts +2 -2
- package/dist/research/workspace/constants.js +6 -0
- package/dist/research/workspace/constants.js.map +1 -1
- package/dist/research/workspace/credentials.d.ts +19 -0
- package/dist/research/workspace/credentials.js +111 -0
- package/dist/research/workspace/credentials.js.map +1 -0
- package/dist/research/workspace/executor.d.ts +1 -0
- package/dist/research/workspace/executor.js +7 -3
- package/dist/research/workspace/executor.js.map +1 -1
- package/dist/research/workspace/external-skills.d.ts +458 -0
- package/dist/research/workspace/external-skills.js +1000 -0
- package/dist/research/workspace/external-skills.js.map +1 -0
- package/dist/research/workspace/preflight.d.ts +2 -0
- package/dist/research/workspace/preflight.js +11 -4
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/runtime.js +63 -20
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/storage.js +37 -2
- package/dist/research/workspace/storage.js.map +1 -1
- package/dist/research/workspace/types.d.ts +29 -0
- package/dist/research/workspace/workspace.d.ts +2 -0
- package/dist/research/workspace/workspace.js +98 -45
- package/dist/research/workspace/workspace.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
3
|
+
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
|
+
import { CliError } from "../../errors.js";
|
|
6
|
+
import { buildCapabilityLock, loadCapabilityDeclarations, parseCapabilityDeclarations, verifyCapabilities, } from "./capabilities.js";
|
|
7
|
+
import { inspectCapabilityCredentialEnvironment, loadCapabilityCredentialMap, } from "./credentials.js";
|
|
8
|
+
import { sanitizeResearchText } from "./sanitization.js";
|
|
9
|
+
import { hashRegularTree, isObject, pathExists, readJsonFile, sha256Text, workspacePaths, writeJsonAtomic, } from "./storage.js";
|
|
10
|
+
export const EXTERNAL_SKILLS_CLI_VERSION = "1.5.22";
|
|
11
|
+
export const EXTERNAL_SKILL_PROFILE = "internet-research";
|
|
12
|
+
export const EXTERNAL_SKILL_CONTEXT_PROFILE = "internet-research-with-context";
|
|
13
|
+
export const EXTERNAL_SKILL_MEDIA_PROFILE = "internet-research-with-media";
|
|
14
|
+
const BRAVE_SOURCE = {
|
|
15
|
+
repository: "brave/brave-search-skills",
|
|
16
|
+
locator: "https://github.com/brave/brave-search-skills.git",
|
|
17
|
+
immutableRef: "3e088af66eb61f1c207c22b2be0278ca8744d1d1",
|
|
18
|
+
license: "MIT",
|
|
19
|
+
};
|
|
20
|
+
const EXTERNAL_SKILL_CATALOG = [
|
|
21
|
+
{
|
|
22
|
+
id: "external.brave.web-search",
|
|
23
|
+
capabilityId: "method.brave.web-search",
|
|
24
|
+
skillName: "web-search",
|
|
25
|
+
tier: "required",
|
|
26
|
+
purpose: "Broad public-internet discovery with ranked URLs, snippets, dates, and pagination.",
|
|
27
|
+
expectedTreeSha256: "0432f4eb084766046a2feeb146f0b3917850d138eb4182c23fc000a058fbe123",
|
|
28
|
+
requiredForDiscovery: true,
|
|
29
|
+
maxResponseBytes: 1024 * 1024,
|
|
30
|
+
maxItems: 100,
|
|
31
|
+
fullText: false,
|
|
32
|
+
publicationDates: true,
|
|
33
|
+
healthUrl: "https://api.search.brave.com/res/v1/web/search?q=tiangong+research+connectivity+check&count=1&result_filter=web",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "external.brave.llm-context",
|
|
37
|
+
capabilityId: "method.brave.llm-context",
|
|
38
|
+
skillName: "llm-context",
|
|
39
|
+
tier: "enhanced",
|
|
40
|
+
purpose: "Bounded extracted web content for grounding beyond search-result snippets.",
|
|
41
|
+
expectedTreeSha256: "5abba551d0498a80eba4e64207f483974f5a312cda5b263c1cc2b7f99d81c4a3",
|
|
42
|
+
requiredForDiscovery: true,
|
|
43
|
+
maxResponseBytes: 2 * 1024 * 1024,
|
|
44
|
+
maxItems: 50,
|
|
45
|
+
fullText: false,
|
|
46
|
+
publicationDates: true,
|
|
47
|
+
healthUrl: "https://api.search.brave.com/res/v1/llm/context?q=tiangong+research+connectivity+check&count=1&maximum_number_of_tokens=1024&maximum_number_of_tokens_per_url=512",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "external.brave.news-search",
|
|
51
|
+
capabilityId: "method.brave.news-search",
|
|
52
|
+
skillName: "news-search",
|
|
53
|
+
tier: "required",
|
|
54
|
+
purpose: "Date-sensitive news discovery that complements the general web index.",
|
|
55
|
+
expectedTreeSha256: "80f8dcb7c78209cce5315e507f0aba21e3f654f42f6a414c177de644bcd07773",
|
|
56
|
+
requiredForDiscovery: true,
|
|
57
|
+
maxResponseBytes: 1024 * 1024,
|
|
58
|
+
maxItems: 100,
|
|
59
|
+
fullText: false,
|
|
60
|
+
publicationDates: true,
|
|
61
|
+
healthUrl: "https://api.search.brave.com/res/v1/news/search?q=tiangong+research+connectivity+check&count=1",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "external.brave.images-search",
|
|
65
|
+
capabilityId: "method.brave.images-search",
|
|
66
|
+
skillName: "images-search",
|
|
67
|
+
tier: "conditional",
|
|
68
|
+
purpose: "Image-source discovery for research questions with a material visual-evidence dimension.",
|
|
69
|
+
expectedTreeSha256: "467a9afae0e959b482cb6b2236a57a327959b28985871f88c202dc70b10a7c84",
|
|
70
|
+
requiredForDiscovery: false,
|
|
71
|
+
maxResponseBytes: 1024 * 1024,
|
|
72
|
+
maxItems: 200,
|
|
73
|
+
fullText: false,
|
|
74
|
+
publicationDates: true,
|
|
75
|
+
healthUrl: "https://api.search.brave.com/res/v1/images/search?q=tiangong+research+connectivity+check&count=1",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "external.brave.videos-search",
|
|
79
|
+
capabilityId: "method.brave.videos-search",
|
|
80
|
+
skillName: "videos-search",
|
|
81
|
+
tier: "conditional",
|
|
82
|
+
purpose: "Video-source discovery for research questions with a material audiovisual dimension.",
|
|
83
|
+
expectedTreeSha256: "ff3a2e2291efc27f3f09847b7e0b20e77d229f6a44a1a64e562964c820eb9719",
|
|
84
|
+
requiredForDiscovery: false,
|
|
85
|
+
maxResponseBytes: 1024 * 1024,
|
|
86
|
+
maxItems: 100,
|
|
87
|
+
fullText: false,
|
|
88
|
+
publicationDates: true,
|
|
89
|
+
healthUrl: "https://api.search.brave.com/res/v1/videos/search?q=tiangong+research+connectivity+check&count=1",
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
const EVALUATED_EXTERNAL_SKILLS = [
|
|
93
|
+
{
|
|
94
|
+
skillName: "answers",
|
|
95
|
+
expectedTreeSha256: "7c11afce362b8beb6a3c27ef0f77d82d10869db2ee583f74ecf0d76422d4410a",
|
|
96
|
+
disposition: "unsupported-execution",
|
|
97
|
+
reason: "Its synthesized-answer endpoint requires POST and is not a raw-evidence source; the current evidence broker authorizes bounded GET only.",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
skillName: "bx",
|
|
101
|
+
expectedTreeSha256: "c5847b34c8b3c751891014ffb09c1a7842fef20efb7993c255ff5419ed53d017",
|
|
102
|
+
disposition: "unsupported-execution",
|
|
103
|
+
reason: "It delegates to a separately installed executable and credential store; arbitrary command execution is outside brokered evidence admission.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
skillName: "local-descriptions",
|
|
107
|
+
expectedTreeSha256: "4a5750f8564bb9664366b2250048c5bb59a44420b8e758e23147458a582ee2b9",
|
|
108
|
+
disposition: "custom-admission",
|
|
109
|
+
reason: "It consumes location IDs from a prior search, so admit it explicitly when the research question has a material local-place dimension.",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
skillName: "local-pois",
|
|
113
|
+
expectedTreeSha256: "17187a13051014c83a85debfcf42e948b0630ee23e6fbb62817603090bc8a2ef",
|
|
114
|
+
disposition: "custom-admission",
|
|
115
|
+
reason: "It consumes location IDs from a prior search, so admit it explicitly when the research question has a material POI dimension.",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
skillName: "spellcheck",
|
|
119
|
+
expectedTreeSha256: "98ec9f346ab442288f3a4cd17ef6dd0a56e0fed61ee273a6df38733087254cd5",
|
|
120
|
+
disposition: "not-evidence-source",
|
|
121
|
+
reason: "It normalizes queries but does not produce independently reviewable evidence.",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
skillName: "suggest",
|
|
125
|
+
expectedTreeSha256: "88213b478e2010bd511d4fc98770093121a47de64ee890d76c91b8b6d8aa61ad",
|
|
126
|
+
disposition: "not-evidence-source",
|
|
127
|
+
reason: "It proposes queries but does not produce independently reviewable evidence.",
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
export async function inspectExternalSkillCatalog(input) {
|
|
131
|
+
const selectedPath = resolve(input.selectedPath);
|
|
132
|
+
const workspace = input.workspace ? resolve(input.workspace) : null;
|
|
133
|
+
const roots = skillRoots(selectedPath, input.skillRoot ?? null);
|
|
134
|
+
const declarations = workspace ? await loadCapabilityDeclarations(workspace) : null;
|
|
135
|
+
const verification = workspace ? await verifyCapabilities(workspace) : null;
|
|
136
|
+
let credentialEnvironment = null;
|
|
137
|
+
let credentialEnvironmentError = null;
|
|
138
|
+
if (workspace) {
|
|
139
|
+
try {
|
|
140
|
+
credentialEnvironment = await inspectCapabilityCredentialEnvironment(workspace, declarations?.capabilities ?? []);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
credentialEnvironmentError = sanitizeResearchText(error instanceof Error ? error.message : String(error));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const entries = [];
|
|
147
|
+
for (const entry of EXTERNAL_SKILL_CATALOG) {
|
|
148
|
+
const installation = await inspectCatalogInstallation(entry, roots);
|
|
149
|
+
const declaration = declarations?.capabilities.find((capability) => capability.source?.catalogId === entry.id ||
|
|
150
|
+
(basename(capability.skillPath) === entry.skillName &&
|
|
151
|
+
capability.id === entry.capabilityId));
|
|
152
|
+
const locked = declaration !== undefined &&
|
|
153
|
+
capabilityVerificationErrors(verification, declaration.id).length === 0;
|
|
154
|
+
const credentialStatus = credentialEnvironmentError
|
|
155
|
+
? "blocked"
|
|
156
|
+
: declaration
|
|
157
|
+
? declaration.credentials.length === 0
|
|
158
|
+
? "not-required"
|
|
159
|
+
: declaration.credentials.every((credential) => credentialEnvironment?.configuredIds.includes(credential.id))
|
|
160
|
+
? "configured"
|
|
161
|
+
: "missing"
|
|
162
|
+
: "not-configured";
|
|
163
|
+
entries.push({
|
|
164
|
+
id: entry.id,
|
|
165
|
+
capabilityId: entry.capabilityId,
|
|
166
|
+
skillName: entry.skillName,
|
|
167
|
+
tier: entry.tier,
|
|
168
|
+
purpose: entry.purpose,
|
|
169
|
+
source: {
|
|
170
|
+
repository: BRAVE_SOURCE.repository,
|
|
171
|
+
locator: BRAVE_SOURCE.locator,
|
|
172
|
+
immutableRef: BRAVE_SOURCE.immutableRef,
|
|
173
|
+
license: BRAVE_SOURCE.license,
|
|
174
|
+
expectedTreeSha256: entry.expectedTreeSha256,
|
|
175
|
+
},
|
|
176
|
+
install: {
|
|
177
|
+
projectPlan: pinnedInstallPlan(false, selectedPath, [entry.skillName]),
|
|
178
|
+
globalPlan: pinnedInstallPlan(true, selectedPath, [entry.skillName]),
|
|
179
|
+
automaticAtRuntime: false,
|
|
180
|
+
},
|
|
181
|
+
configuration: {
|
|
182
|
+
allowedHosts: ["api.search.brave.com"],
|
|
183
|
+
credentialIds: ["brave.search.api-key"],
|
|
184
|
+
discoveryScopes: ["public-internet"],
|
|
185
|
+
requiredForDiscovery: entry.requiredForDiscovery,
|
|
186
|
+
providerPlan: entry.tier === "enhanced"
|
|
187
|
+
? "Requires provider subscription access to LLM Context."
|
|
188
|
+
: "Brave Search API plan with access to the selected endpoint.",
|
|
189
|
+
},
|
|
190
|
+
installation,
|
|
191
|
+
status: {
|
|
192
|
+
configured: declaration !== undefined,
|
|
193
|
+
locked,
|
|
194
|
+
credential: credentialStatus,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const evaluatedAlternatives = [];
|
|
199
|
+
for (const entry of EVALUATED_EXTERNAL_SKILLS) {
|
|
200
|
+
evaluatedAlternatives.push({
|
|
201
|
+
skillName: entry.skillName,
|
|
202
|
+
recommended: false,
|
|
203
|
+
disposition: entry.disposition,
|
|
204
|
+
reason: entry.reason,
|
|
205
|
+
source: {
|
|
206
|
+
repository: BRAVE_SOURCE.repository,
|
|
207
|
+
locator: BRAVE_SOURCE.locator,
|
|
208
|
+
immutableRef: BRAVE_SOURCE.immutableRef,
|
|
209
|
+
license: BRAVE_SOURCE.license,
|
|
210
|
+
expectedTreeSha256: entry.expectedTreeSha256,
|
|
211
|
+
},
|
|
212
|
+
install: {
|
|
213
|
+
projectPlan: pinnedInstallPlan(false, selectedPath, [entry.skillName]),
|
|
214
|
+
globalPlan: pinnedInstallPlan(true, selectedPath, [entry.skillName]),
|
|
215
|
+
automaticAtRuntime: false,
|
|
216
|
+
},
|
|
217
|
+
installation: await inspectCatalogInstallation(entry, roots),
|
|
218
|
+
configuration: entry.disposition === "custom-admission"
|
|
219
|
+
? {
|
|
220
|
+
status: "requires-explicit-import",
|
|
221
|
+
command: "tiangong-ai research capability import --definition /absolute/capability.json --workspace /absolute/workspace --json",
|
|
222
|
+
}
|
|
223
|
+
: {
|
|
224
|
+
status: "not-supported",
|
|
225
|
+
command: null,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const workspaceCapabilities = declarations?.capabilities.map((capability) => {
|
|
230
|
+
const lockErrors = capabilityVerificationErrors(verification, capability.id);
|
|
231
|
+
const credentialIds = capability.credentials.map((credential) => credential.id);
|
|
232
|
+
return {
|
|
233
|
+
id: capability.id,
|
|
234
|
+
skillName: basename(capability.skillPath),
|
|
235
|
+
catalogId: capability.source?.catalogId ?? null,
|
|
236
|
+
externalSource: isExternalCapabilitySource(capability.source),
|
|
237
|
+
source: capability.source === null
|
|
238
|
+
? null
|
|
239
|
+
: {
|
|
240
|
+
type: capability.source.type,
|
|
241
|
+
locatorSha256: sha256Text(capability.source.locator),
|
|
242
|
+
immutableRef: capability.source.immutableRef,
|
|
243
|
+
expectedTreeSha256: capability.source.expectedTreeSha256,
|
|
244
|
+
license: capability.source.license,
|
|
245
|
+
},
|
|
246
|
+
requiredForDiscovery: capability.requiredForDiscovery,
|
|
247
|
+
discoveryScopes: capability.coverage?.discoveryScopes ?? [],
|
|
248
|
+
allowedHosts: capability.allowedHosts,
|
|
249
|
+
credentialIds,
|
|
250
|
+
status: {
|
|
251
|
+
locked: verification?.status === "verified" || lockErrors.length === 0,
|
|
252
|
+
credential: credentialEnvironmentError
|
|
253
|
+
? "blocked"
|
|
254
|
+
: credentialIds.length === 0
|
|
255
|
+
? "not-required"
|
|
256
|
+
: credentialIds.every((credentialId) => credentialEnvironment?.configuredIds.includes(credentialId))
|
|
257
|
+
? "configured"
|
|
258
|
+
: "missing",
|
|
259
|
+
errors: lockErrors,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}) ?? [];
|
|
263
|
+
return {
|
|
264
|
+
schemaVersion: 1,
|
|
265
|
+
catalog: "tiangong-research-external-skills",
|
|
266
|
+
policy: {
|
|
267
|
+
implementations: "external-only",
|
|
268
|
+
runtimeInstall: false,
|
|
269
|
+
projectLocalCopyRecommended: true,
|
|
270
|
+
},
|
|
271
|
+
installer: {
|
|
272
|
+
package: "skills",
|
|
273
|
+
version: EXTERNAL_SKILLS_CLI_VERSION,
|
|
274
|
+
source: {
|
|
275
|
+
locator: BRAVE_SOURCE.locator,
|
|
276
|
+
immutableRef: BRAVE_SOURCE.immutableRef,
|
|
277
|
+
},
|
|
278
|
+
projectPlan: pinnedInstallPlan(false, selectedPath),
|
|
279
|
+
globalPlan: pinnedInstallPlan(true, selectedPath),
|
|
280
|
+
allRecommendedProjectPlan: pinnedInstallPlan(false, selectedPath, EXTERNAL_SKILL_CATALOG.map((entry) => entry.skillName)),
|
|
281
|
+
allRecommendedGlobalPlan: pinnedInstallPlan(true, selectedPath, EXTERNAL_SKILL_CATALOG.map((entry) => entry.skillName)),
|
|
282
|
+
},
|
|
283
|
+
profiles: [
|
|
284
|
+
{
|
|
285
|
+
id: EXTERNAL_SKILL_PROFILE,
|
|
286
|
+
description: "Required public-internet evidence discovery and bounded page context.",
|
|
287
|
+
skillIds: EXTERNAL_SKILL_CATALOG.filter((entry) => entry.tier === "required").map((entry) => entry.id),
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: EXTERNAL_SKILL_CONTEXT_PROFILE,
|
|
291
|
+
description: "Required public-internet discovery plus plan-dependent extracted page context.",
|
|
292
|
+
skillIds: EXTERNAL_SKILL_CATALOG.filter((entry) => entry.tier !== "conditional").map((entry) => entry.id),
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: EXTERNAL_SKILL_MEDIA_PROFILE,
|
|
296
|
+
description: "Public-internet evidence discovery plus image and video source discovery.",
|
|
297
|
+
skillIds: EXTERNAL_SKILL_CATALOG.map((entry) => entry.id),
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
conditionalSkillIds: EXTERNAL_SKILL_CATALOG.filter((entry) => entry.tier === "conditional").map((entry) => entry.id),
|
|
301
|
+
enhancedSkillIds: EXTERNAL_SKILL_CATALOG.filter((entry) => entry.tier === "enhanced").map((entry) => entry.id),
|
|
302
|
+
entries,
|
|
303
|
+
evaluatedAlternatives,
|
|
304
|
+
credentials: [
|
|
305
|
+
{
|
|
306
|
+
id: "brave.search.api-key",
|
|
307
|
+
provider: "Brave Search API",
|
|
308
|
+
requiredBy: EXTERNAL_SKILL_CATALOG.map((entry) => entry.id),
|
|
309
|
+
obtainAt: "https://api.search.brave.com",
|
|
310
|
+
workspaceEnvironmentKey: "TIANGONG_RESEARCH_CAPABILITY_CREDENTIALS_JSON",
|
|
311
|
+
exampleAssignment: 'TIANGONG_RESEARCH_CAPABILITY_CREDENTIALS_JSON={"brave.search.api-key":"REPLACE_WITH_OWNER_VALUE"}',
|
|
312
|
+
configureCommand: "tiangong-ai research capability credential set --id brave.search.api-key --from-env BRAVE_SEARCH_API_KEY --workspace /absolute/workspace --json",
|
|
313
|
+
minimumUtf8Bytes: 8,
|
|
314
|
+
outputPolicy: "value-is-never-emitted",
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
customExternalCapabilities: {
|
|
318
|
+
supported: true,
|
|
319
|
+
purpose: "Bind an owner-selected external Skill to an explicitly allowlisted database or evidence API.",
|
|
320
|
+
supportedKinds: [
|
|
321
|
+
{
|
|
322
|
+
kind: "brokered-evidence",
|
|
323
|
+
execution: "The isolated producer translates the staged external Skill's documented GET endpoint into the credential broker.",
|
|
324
|
+
permissions: ["project-read", "candidate-write", "brokered-network"],
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
kind: "method-guidance",
|
|
328
|
+
execution: "The staged external Skill provides reviewed method instructions only; it does not grant network or tool execution.",
|
|
329
|
+
permissions: ["project-read", "candidate-write"],
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
runtimeInstall: false,
|
|
333
|
+
sourceRequirements: [
|
|
334
|
+
"external source identity",
|
|
335
|
+
"immutable source reference",
|
|
336
|
+
"whole-tree lock",
|
|
337
|
+
"license identifier",
|
|
338
|
+
],
|
|
339
|
+
networkRequirements: [
|
|
340
|
+
"exact HTTPS allowedHosts",
|
|
341
|
+
"bounded GET response policy (the current broker does not authorize POST)",
|
|
342
|
+
"safe healthCheck",
|
|
343
|
+
"logical credential declarations only",
|
|
344
|
+
],
|
|
345
|
+
commands: [
|
|
346
|
+
"tiangong-ai research capability import --definition /absolute/capability.json --workspace /absolute/workspace --json",
|
|
347
|
+
"tiangong-ai research capability doctor --live --workspace /absolute/workspace --json",
|
|
348
|
+
],
|
|
349
|
+
brokeredEvidenceDefinitionTemplate: {
|
|
350
|
+
id: "database.owner-source.search",
|
|
351
|
+
skillPath: "/absolute/path/to/external-database-skill",
|
|
352
|
+
source: {
|
|
353
|
+
type: "git",
|
|
354
|
+
locator: "https://github.com/EXTERNAL_OWNER/EXTERNAL_SKILL.git",
|
|
355
|
+
immutableRef: "FULL_40_CHARACTER_COMMIT_SHA",
|
|
356
|
+
expectedTreeSha256: "FULL_64_CHARACTER_TREE_SHA256",
|
|
357
|
+
license: "SPDX_OR_LICENSE_REF",
|
|
358
|
+
catalogId: null,
|
|
359
|
+
},
|
|
360
|
+
requiredForDiscovery: true,
|
|
361
|
+
permissions: ["project-read", "candidate-write", "brokered-network"],
|
|
362
|
+
allowedHosts: ["database.example.org"],
|
|
363
|
+
http: {
|
|
364
|
+
accept: "application/json",
|
|
365
|
+
allowedContentTypes: ["application/json"],
|
|
366
|
+
maxResponseBytes: 524288,
|
|
367
|
+
maxItems: 100,
|
|
368
|
+
},
|
|
369
|
+
coverage: {
|
|
370
|
+
dimensions: ["*"],
|
|
371
|
+
sourceTypes: ["*"],
|
|
372
|
+
discoveryScopes: ["database:owner-source"],
|
|
373
|
+
fullText: true,
|
|
374
|
+
publicationDates: true,
|
|
375
|
+
},
|
|
376
|
+
credentials: [
|
|
377
|
+
{
|
|
378
|
+
id: "database.owner-source.api-key",
|
|
379
|
+
allowedHosts: ["database.example.org"],
|
|
380
|
+
headerName: "Authorization",
|
|
381
|
+
prefix: "Bearer ",
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
healthCheck: {
|
|
385
|
+
url: "https://database.example.org/health?query=connectivity",
|
|
386
|
+
credentialId: "database.owner-source.api-key",
|
|
387
|
+
expectedContentTypes: ["application/json"],
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
workspaceCapabilities,
|
|
392
|
+
summary: {
|
|
393
|
+
recommended: entries.length,
|
|
394
|
+
evaluated: entries.length + evaluatedAlternatives.length,
|
|
395
|
+
notSelected: evaluatedAlternatives.length,
|
|
396
|
+
required: entries.filter((entry) => entry.tier === "required").length,
|
|
397
|
+
enhanced: entries.filter((entry) => entry.tier === "enhanced").length,
|
|
398
|
+
conditional: entries.filter((entry) => entry.tier === "conditional").length,
|
|
399
|
+
installed: entries.filter((entry) => entry.installation.status === "installed").length,
|
|
400
|
+
configured: entries.filter((entry) => entry.status.configured).length,
|
|
401
|
+
credentialsReady: entries.filter((entry) => entry.status.credential === "configured" || entry.status.credential === "not-required").length,
|
|
402
|
+
workspaceCapabilities: workspaceCapabilities.length,
|
|
403
|
+
},
|
|
404
|
+
workspace,
|
|
405
|
+
skillRoots: roots,
|
|
406
|
+
capabilityVerification: verification,
|
|
407
|
+
credentialEnvironment: credentialEnvironmentError
|
|
408
|
+
? { status: "blocked", detail: credentialEnvironmentError, missingIds: [] }
|
|
409
|
+
: workspace
|
|
410
|
+
? {
|
|
411
|
+
status: credentialEnvironment?.missingIds.length
|
|
412
|
+
? "blocked"
|
|
413
|
+
: "ready",
|
|
414
|
+
detail: credentialEnvironment?.detail ?? "unavailable",
|
|
415
|
+
missingIds: credentialEnvironment?.missingIds ?? [],
|
|
416
|
+
}
|
|
417
|
+
: null,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
export async function configureExternalSkillProfile(input) {
|
|
421
|
+
if (input.profile !== EXTERNAL_SKILL_PROFILE &&
|
|
422
|
+
input.profile !== EXTERNAL_SKILL_CONTEXT_PROFILE &&
|
|
423
|
+
input.profile !== EXTERNAL_SKILL_MEDIA_PROFILE) {
|
|
424
|
+
throw new CliError(`Unsupported external Skill profile: ${input.profile}`, {
|
|
425
|
+
code: "RESEARCH_EXTERNAL_SKILL_PROFILE_INVALID",
|
|
426
|
+
exitCode: 2,
|
|
427
|
+
details: {
|
|
428
|
+
supportedProfiles: [
|
|
429
|
+
EXTERNAL_SKILL_PROFILE,
|
|
430
|
+
EXTERNAL_SKILL_CONTEXT_PROFILE,
|
|
431
|
+
EXTERNAL_SKILL_MEDIA_PROFILE,
|
|
432
|
+
],
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
const workspace = resolve(input.workspace);
|
|
437
|
+
await requireExistingCapabilitiesVerified(workspace);
|
|
438
|
+
const roots = skillRoots(workspace, input.skillRoot ?? null);
|
|
439
|
+
const selectedEntries = EXTERNAL_SKILL_CATALOG.filter((entry) => input.profile === EXTERNAL_SKILL_MEDIA_PROFILE ||
|
|
440
|
+
entry.tier === "required" ||
|
|
441
|
+
(input.profile === EXTERNAL_SKILL_CONTEXT_PROFILE && entry.tier === "enhanced"));
|
|
442
|
+
const configured = await Promise.all(selectedEntries.map(async (entry) => catalogDeclaration(entry, await requireCatalogSkill(entry, roots, workspace))));
|
|
443
|
+
const existing = await loadCapabilityDeclarations(workspace);
|
|
444
|
+
const managedIds = new Set(configured.map((capability) => capability.id));
|
|
445
|
+
for (const capability of existing.capabilities) {
|
|
446
|
+
if (managedIds.has(capability.id) &&
|
|
447
|
+
capability.source?.catalogId !==
|
|
448
|
+
configured.find((candidate) => candidate.id === capability.id)?.source?.catalogId) {
|
|
449
|
+
throw new CliError(`Capability ID conflicts with the external Skill profile: ${capability.id}`, {
|
|
450
|
+
code: "RESEARCH_CAPABILITY_CONFLICT",
|
|
451
|
+
exitCode: 3,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const merged = parseCapabilityDeclarations({
|
|
456
|
+
schemaVersion: 1,
|
|
457
|
+
capabilities: [
|
|
458
|
+
...existing.capabilities.filter((capability) => !managedIds.has(capability.id)),
|
|
459
|
+
...configured,
|
|
460
|
+
],
|
|
461
|
+
});
|
|
462
|
+
const lock = await buildCapabilityLock(merged);
|
|
463
|
+
await persistDeclarationsAndLock(workspace, merged, lock);
|
|
464
|
+
return {
|
|
465
|
+
schemaVersion: 1,
|
|
466
|
+
workspace,
|
|
467
|
+
profile: input.profile,
|
|
468
|
+
configured: configured.map((capability) => ({
|
|
469
|
+
id: capability.id,
|
|
470
|
+
skillName: basename(capability.skillPath),
|
|
471
|
+
treeSha256: lock.capabilities.find((record) => record.id === capability.id)?.treeSha256,
|
|
472
|
+
credentialIds: capability.credentials.map((credential) => credential.id),
|
|
473
|
+
requiredForDiscovery: capability.requiredForDiscovery,
|
|
474
|
+
})),
|
|
475
|
+
lockStatus: "written",
|
|
476
|
+
next: [
|
|
477
|
+
"Configure declared logical credential values in .tiangong-research/.env with mode 0600.",
|
|
478
|
+
"Run research capability doctor --live before production preflight.",
|
|
479
|
+
],
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
export async function importExternalCapability(input) {
|
|
483
|
+
if (!isAbsolute(input.definitionPath)) {
|
|
484
|
+
throw new CliError("External capability definition path must be absolute.", {
|
|
485
|
+
code: "RESEARCH_CAPABILITY_IMPORT_INVALID",
|
|
486
|
+
exitCode: 2,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
const info = await lstat(input.definitionPath).catch(() => undefined);
|
|
490
|
+
if (!info?.isFile() || info.isSymbolicLink()) {
|
|
491
|
+
throw new CliError("External capability definition must be a regular non-symlink JSON file.", {
|
|
492
|
+
code: "RESEARCH_CAPABILITY_IMPORT_INVALID",
|
|
493
|
+
exitCode: 2,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
let source;
|
|
497
|
+
try {
|
|
498
|
+
source = JSON.parse(await readFile(input.definitionPath, "utf8"));
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
throw new CliError("External capability definition is unreadable or invalid JSON.", {
|
|
502
|
+
code: "RESEARCH_CAPABILITY_IMPORT_INVALID",
|
|
503
|
+
exitCode: 2,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
const imported = parseCapabilityDeclarations({ schemaVersion: 1, capabilities: [source] })
|
|
507
|
+
.capabilities[0];
|
|
508
|
+
if (!isExternalCapabilitySource(imported.source) || imported.source?.catalogId !== null) {
|
|
509
|
+
throw new CliError("Imported research capabilities must identify an external Skill source.", {
|
|
510
|
+
code: "RESEARCH_CAPABILITY_IMPORT_INVALID",
|
|
511
|
+
exitCode: 2,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (imported.permissions.includes("brokered-network") && !imported.healthCheck) {
|
|
515
|
+
throw new CliError("Imported brokered-network capabilities require a safe healthCheck.", {
|
|
516
|
+
code: "RESEARCH_CAPABILITY_IMPORT_INVALID",
|
|
517
|
+
exitCode: 2,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
const workspace = resolve(input.workspace);
|
|
521
|
+
await requireExistingCapabilitiesVerified(workspace);
|
|
522
|
+
const existing = await loadCapabilityDeclarations(workspace);
|
|
523
|
+
if (existing.capabilities.some((capability) => capability.id === imported.id)) {
|
|
524
|
+
throw new CliError(`Capability ID already exists: ${imported.id}`, {
|
|
525
|
+
code: "RESEARCH_CAPABILITY_CONFLICT",
|
|
526
|
+
exitCode: 3,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
const merged = parseCapabilityDeclarations({
|
|
530
|
+
schemaVersion: 1,
|
|
531
|
+
capabilities: [...existing.capabilities, imported],
|
|
532
|
+
});
|
|
533
|
+
const lock = await buildCapabilityLock(merged);
|
|
534
|
+
await persistDeclarationsAndLock(workspace, merged, lock);
|
|
535
|
+
return {
|
|
536
|
+
schemaVersion: 1,
|
|
537
|
+
workspace,
|
|
538
|
+
imported: {
|
|
539
|
+
id: imported.id,
|
|
540
|
+
skillName: basename(imported.skillPath),
|
|
541
|
+
sourceType: imported.source.type,
|
|
542
|
+
sourceLocatorSha256: sha256Text(imported.source.locator),
|
|
543
|
+
immutableRef: imported.source.immutableRef,
|
|
544
|
+
expectedTreeSha256: imported.source.expectedTreeSha256,
|
|
545
|
+
license: imported.source.license,
|
|
546
|
+
requiredForDiscovery: imported.requiredForDiscovery,
|
|
547
|
+
discoveryScopes: imported.coverage?.discoveryScopes ?? [],
|
|
548
|
+
},
|
|
549
|
+
lockStatus: "written",
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
export async function doctorExternalCapabilities(workspace, options = {}) {
|
|
553
|
+
const root = resolve(workspace);
|
|
554
|
+
const declarations = await loadCapabilityDeclarations(root);
|
|
555
|
+
const verification = await verifyCapabilities(root);
|
|
556
|
+
let credentialEnvironment = null;
|
|
557
|
+
let credentialError = null;
|
|
558
|
+
let credentialMap = new Map();
|
|
559
|
+
try {
|
|
560
|
+
credentialEnvironment = await inspectCapabilityCredentialEnvironment(root, declarations.capabilities);
|
|
561
|
+
credentialMap = await loadCapabilityCredentialMap(root, declarations.capabilities);
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
credentialError = sanitizeResearchText(error instanceof Error ? error.message : String(error));
|
|
565
|
+
}
|
|
566
|
+
const lock = await readJsonFile(workspacePaths(root).capabilityLock, "Capability lock").catch(() => null);
|
|
567
|
+
const lockRecords = isObject(lock) && Array.isArray(lock.capabilities) ? lock.capabilities.filter(isObject) : [];
|
|
568
|
+
const capabilities = [];
|
|
569
|
+
for (const capability of declarations.capabilities) {
|
|
570
|
+
const lockRecord = lockRecords.find((record) => record.id === capability.id);
|
|
571
|
+
const missingCredentialIds = capability.credentials
|
|
572
|
+
.map((credential) => credential.id)
|
|
573
|
+
.filter((credentialId) => !credentialMap.has(credentialId));
|
|
574
|
+
const staticErrors = [];
|
|
575
|
+
if (!lockRecord || capabilityVerificationErrors(verification, capability.id).length > 0) {
|
|
576
|
+
staticErrors.push("lock-or-tree-drift");
|
|
577
|
+
}
|
|
578
|
+
if (missingCredentialIds.length)
|
|
579
|
+
staticErrors.push("credential-missing");
|
|
580
|
+
if (credentialError)
|
|
581
|
+
staticErrors.push("credential-environment-invalid");
|
|
582
|
+
if (capability.permissions.includes("brokered-network") && !capability.healthCheck) {
|
|
583
|
+
staticErrors.push("health-check-missing");
|
|
584
|
+
}
|
|
585
|
+
if (capability.coverage?.discoveryScopes.includes("public-internet") &&
|
|
586
|
+
!isExternalCapabilitySource(capability.source)) {
|
|
587
|
+
staticErrors.push("external-source-required");
|
|
588
|
+
}
|
|
589
|
+
const health = options.live
|
|
590
|
+
? staticErrors.length
|
|
591
|
+
? {
|
|
592
|
+
status: "blocked",
|
|
593
|
+
code: staticErrors[0],
|
|
594
|
+
host: capability.healthCheck ? new URL(capability.healthCheck.url).host : null,
|
|
595
|
+
targetSha256: capability.healthCheck ? sha256Text(capability.healthCheck.url) : null,
|
|
596
|
+
httpStatus: null,
|
|
597
|
+
retryAfterSeconds: null,
|
|
598
|
+
detail: "Live probe was not started because static readiness failed.",
|
|
599
|
+
}
|
|
600
|
+
: capability.healthCheck
|
|
601
|
+
? await probeCapability(capability, capability.healthCheck, credentialMap, options.fetcher, options.sleeper)
|
|
602
|
+
: {
|
|
603
|
+
status: "not-applicable",
|
|
604
|
+
code: "not-applicable",
|
|
605
|
+
host: null,
|
|
606
|
+
targetSha256: null,
|
|
607
|
+
httpStatus: null,
|
|
608
|
+
retryAfterSeconds: null,
|
|
609
|
+
detail: "Capability has no brokered network health check.",
|
|
610
|
+
}
|
|
611
|
+
: {
|
|
612
|
+
status: "not-run",
|
|
613
|
+
code: "not-run",
|
|
614
|
+
host: capability.healthCheck ? new URL(capability.healthCheck.url).host : null,
|
|
615
|
+
targetSha256: capability.healthCheck ? sha256Text(capability.healthCheck.url) : null,
|
|
616
|
+
httpStatus: null,
|
|
617
|
+
retryAfterSeconds: null,
|
|
618
|
+
detail: "Use --live for an explicit provider connectivity probe.",
|
|
619
|
+
};
|
|
620
|
+
capabilities.push({
|
|
621
|
+
id: capability.id,
|
|
622
|
+
skillName: basename(capability.skillPath),
|
|
623
|
+
catalogId: capability.source?.catalogId ?? null,
|
|
624
|
+
sourceType: capability.source?.type ?? null,
|
|
625
|
+
externalSource: isExternalCapabilitySource(capability.source),
|
|
626
|
+
installed: Boolean(lockRecord),
|
|
627
|
+
locked: Boolean(lockRecord) && verification.status === "verified",
|
|
628
|
+
requiredForDiscovery: capability.requiredForDiscovery,
|
|
629
|
+
discoveryScopes: capability.coverage?.discoveryScopes ?? [],
|
|
630
|
+
credentialIds: capability.credentials.map((credential) => credential.id),
|
|
631
|
+
missingCredentialIds,
|
|
632
|
+
staticStatus: staticErrors.length ? "blocked" : "ready",
|
|
633
|
+
staticErrors,
|
|
634
|
+
health,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
const failures = capabilities.flatMap((capability) => {
|
|
638
|
+
const rows = capability.staticStatus === "blocked" ? [`${capability.id}:static`] : [];
|
|
639
|
+
if (options.live &&
|
|
640
|
+
capability.health.status !== "pass" &&
|
|
641
|
+
capability.health.status !== "not-applicable") {
|
|
642
|
+
rows.push(`${capability.id}:live`);
|
|
643
|
+
}
|
|
644
|
+
return rows;
|
|
645
|
+
});
|
|
646
|
+
if (credentialError)
|
|
647
|
+
failures.push("credential-environment");
|
|
648
|
+
return {
|
|
649
|
+
schemaVersion: 1,
|
|
650
|
+
workspace: root,
|
|
651
|
+
mode: options.live ? "live" : "static",
|
|
652
|
+
status: failures.length ? "blocked" : "ready",
|
|
653
|
+
capabilityVerification: verification,
|
|
654
|
+
credentialEnvironment: credentialError
|
|
655
|
+
? { status: "blocked", detail: credentialError, missingIds: [] }
|
|
656
|
+
: {
|
|
657
|
+
status: credentialEnvironment?.missingIds.length
|
|
658
|
+
? "blocked"
|
|
659
|
+
: "ready",
|
|
660
|
+
detail: credentialEnvironment?.detail ?? "unavailable",
|
|
661
|
+
missingIds: credentialEnvironment?.missingIds ?? [],
|
|
662
|
+
},
|
|
663
|
+
capabilities,
|
|
664
|
+
failures,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
export function hasPublicInternetCapability(declarations) {
|
|
668
|
+
return declarations.capabilities.some((capability) => capability.permissions.includes("brokered-network") &&
|
|
669
|
+
capability.coverage?.discoveryScopes.includes("public-internet") &&
|
|
670
|
+
isExternalCapabilitySource(capability.source));
|
|
671
|
+
}
|
|
672
|
+
export function requiredDiscoveryCapabilityIds(declarations) {
|
|
673
|
+
return declarations.capabilities
|
|
674
|
+
.filter((capability) => capability.requiredForDiscovery && capability.permissions.includes("brokered-network"))
|
|
675
|
+
.map((capability) => capability.id)
|
|
676
|
+
.sort();
|
|
677
|
+
}
|
|
678
|
+
function catalogDeclaration(entry, skillPath) {
|
|
679
|
+
return parseCapabilityDeclarations({
|
|
680
|
+
schemaVersion: 1,
|
|
681
|
+
capabilities: [
|
|
682
|
+
{
|
|
683
|
+
id: entry.capabilityId,
|
|
684
|
+
skillPath,
|
|
685
|
+
source: {
|
|
686
|
+
type: "git",
|
|
687
|
+
locator: BRAVE_SOURCE.locator,
|
|
688
|
+
immutableRef: BRAVE_SOURCE.immutableRef,
|
|
689
|
+
expectedTreeSha256: entry.expectedTreeSha256,
|
|
690
|
+
license: BRAVE_SOURCE.license,
|
|
691
|
+
catalogId: entry.id,
|
|
692
|
+
},
|
|
693
|
+
requiredForDiscovery: entry.requiredForDiscovery,
|
|
694
|
+
permissions: ["project-read", "candidate-write", "brokered-network"],
|
|
695
|
+
allowedHosts: ["api.search.brave.com"],
|
|
696
|
+
http: {
|
|
697
|
+
accept: "application/json",
|
|
698
|
+
allowedContentTypes: ["application/json"],
|
|
699
|
+
maxResponseBytes: entry.maxResponseBytes,
|
|
700
|
+
maxItems: entry.maxItems,
|
|
701
|
+
},
|
|
702
|
+
coverage: {
|
|
703
|
+
dimensions: ["*"],
|
|
704
|
+
sourceTypes: ["*"],
|
|
705
|
+
discoveryScopes: ["public-internet"],
|
|
706
|
+
fullText: entry.fullText,
|
|
707
|
+
publicationDates: entry.publicationDates,
|
|
708
|
+
},
|
|
709
|
+
credentials: [
|
|
710
|
+
{
|
|
711
|
+
id: "brave.search.api-key",
|
|
712
|
+
allowedHosts: ["api.search.brave.com"],
|
|
713
|
+
headerName: "X-Subscription-Token",
|
|
714
|
+
prefix: "",
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
healthCheck: {
|
|
718
|
+
url: entry.healthUrl,
|
|
719
|
+
credentialId: "brave.search.api-key",
|
|
720
|
+
expectedContentTypes: ["application/json"],
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
],
|
|
724
|
+
}).capabilities[0];
|
|
725
|
+
}
|
|
726
|
+
async function inspectCatalogInstallation(entry, roots) {
|
|
727
|
+
const candidates = [];
|
|
728
|
+
const errors = [];
|
|
729
|
+
for (const root of roots) {
|
|
730
|
+
const path = join(root, entry.skillName);
|
|
731
|
+
if (!(await pathExists(path)))
|
|
732
|
+
continue;
|
|
733
|
+
try {
|
|
734
|
+
candidates.push({ path, treeSha256: await hashRegularTree(path) });
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
errors.push(sanitizeResearchText(error instanceof Error ? error.message : String(error)));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const exact = candidates.filter((candidate) => candidate.treeSha256 === entry.expectedTreeSha256);
|
|
741
|
+
const status = candidates.length > 1
|
|
742
|
+
? "ambiguous"
|
|
743
|
+
: exact.length === 1
|
|
744
|
+
? "installed"
|
|
745
|
+
: candidates.length || errors.length
|
|
746
|
+
? "drifted"
|
|
747
|
+
: "missing";
|
|
748
|
+
return {
|
|
749
|
+
status,
|
|
750
|
+
paths: candidates.map((candidate) => candidate.path),
|
|
751
|
+
observedTreeSha256: candidates.map((candidate) => candidate.treeSha256),
|
|
752
|
+
errors,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
async function requireCatalogSkill(entry, roots, installBasePath) {
|
|
756
|
+
const installation = await inspectCatalogInstallation(entry, roots);
|
|
757
|
+
const exactPaths = installation.paths.filter((_path, index) => installation.observedTreeSha256[index] === entry.expectedTreeSha256);
|
|
758
|
+
if (installation.status !== "installed" || exactPaths.length !== 1) {
|
|
759
|
+
throw new CliError(`Required external Skill is missing, ambiguous, or differs from its reviewed bytes: ${entry.skillName}`, {
|
|
760
|
+
code: "RESEARCH_EXTERNAL_SKILL_NOT_READY",
|
|
761
|
+
exitCode: 3,
|
|
762
|
+
details: {
|
|
763
|
+
catalogId: entry.id,
|
|
764
|
+
status: installation.status,
|
|
765
|
+
expectedTreeSha256: entry.expectedTreeSha256,
|
|
766
|
+
installPlan: pinnedInstallPlan(false, installBasePath),
|
|
767
|
+
remediation: installation.status === "ambiguous"
|
|
768
|
+
? "Pass --skill-root with one explicit installation root."
|
|
769
|
+
: "Run the pinned install command outside the research runtime, then retry configuration.",
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
return exactPaths[0];
|
|
774
|
+
}
|
|
775
|
+
function skillRoots(selectedPath, explicitRoot) {
|
|
776
|
+
if (explicitRoot) {
|
|
777
|
+
if (!isAbsolute(explicitRoot)) {
|
|
778
|
+
throw new CliError("External Skill root must be absolute.", {
|
|
779
|
+
code: "RESEARCH_EXTERNAL_SKILL_ROOT_INVALID",
|
|
780
|
+
exitCode: 2,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
return [resolve(explicitRoot)];
|
|
784
|
+
}
|
|
785
|
+
return [
|
|
786
|
+
...new Set([
|
|
787
|
+
join(resolve(selectedPath), ".agents", "skills"),
|
|
788
|
+
join(homedir(), ".agents", "skills"),
|
|
789
|
+
]),
|
|
790
|
+
];
|
|
791
|
+
}
|
|
792
|
+
function localInstallCommand(global, sourceDirectory, skillNames) {
|
|
793
|
+
const skills = skillNames.join(" ");
|
|
794
|
+
return [
|
|
795
|
+
`npx --yes skills@${EXTERNAL_SKILLS_CLI_VERSION} add`,
|
|
796
|
+
shellQuote(sourceDirectory),
|
|
797
|
+
`--skill ${skills}`,
|
|
798
|
+
"--agent codex --yes --copy",
|
|
799
|
+
global ? "--global" : "",
|
|
800
|
+
]
|
|
801
|
+
.filter(Boolean)
|
|
802
|
+
.join(" ");
|
|
803
|
+
}
|
|
804
|
+
function pinnedInstallPlan(global, selectedPath, skillNames = EXTERNAL_SKILL_CATALOG.filter((entry) => entry.tier === "required").map((entry) => entry.skillName)) {
|
|
805
|
+
const workingDirectory = resolve(selectedPath);
|
|
806
|
+
const checkoutDirectory = global
|
|
807
|
+
? join(homedir(), ".cache", "tiangong-ai", "external-skills", `brave-search-skills-${BRAVE_SOURCE.immutableRef.slice(0, 12)}`)
|
|
808
|
+
: join(workingDirectory, ".tiangong-external-skills", "sources", `brave-search-skills-${BRAVE_SOURCE.immutableRef.slice(0, 12)}`);
|
|
809
|
+
return {
|
|
810
|
+
shell: "POSIX",
|
|
811
|
+
workingDirectory,
|
|
812
|
+
checkoutDirectory,
|
|
813
|
+
precondition: "checkoutDirectory must not already exist",
|
|
814
|
+
automaticAtRuntime: false,
|
|
815
|
+
commands: [
|
|
816
|
+
`cd ${shellQuote(workingDirectory)}`,
|
|
817
|
+
`git init --quiet ${shellQuote(checkoutDirectory)}`,
|
|
818
|
+
`git -C ${shellQuote(checkoutDirectory)} remote add origin ${shellQuote(BRAVE_SOURCE.locator)}`,
|
|
819
|
+
`git -C ${shellQuote(checkoutDirectory)} fetch --depth 1 origin ${BRAVE_SOURCE.immutableRef}`,
|
|
820
|
+
`git -C ${shellQuote(checkoutDirectory)} checkout --detach FETCH_HEAD`,
|
|
821
|
+
`test "$(git -C ${shellQuote(checkoutDirectory)} rev-parse HEAD)" = ${shellQuote(BRAVE_SOURCE.immutableRef)}`,
|
|
822
|
+
localInstallCommand(global, checkoutDirectory, skillNames),
|
|
823
|
+
],
|
|
824
|
+
verification: {
|
|
825
|
+
repository: BRAVE_SOURCE.repository,
|
|
826
|
+
immutableRef: BRAVE_SOURCE.immutableRef,
|
|
827
|
+
expectedTrees: [...EXTERNAL_SKILL_CATALOG, ...EVALUATED_EXTERNAL_SKILLS]
|
|
828
|
+
.filter((entry) => skillNames.includes(entry.skillName))
|
|
829
|
+
.map((entry) => ({
|
|
830
|
+
skillName: entry.skillName,
|
|
831
|
+
sha256: entry.expectedTreeSha256,
|
|
832
|
+
})),
|
|
833
|
+
},
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
function shellQuote(value) {
|
|
837
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
838
|
+
}
|
|
839
|
+
async function persistDeclarationsAndLock(root, declarations, lock) {
|
|
840
|
+
const paths = workspacePaths(root);
|
|
841
|
+
await writeJsonAtomic(paths.capabilityDeclarations, declarations);
|
|
842
|
+
await writeJsonAtomic(paths.capabilityLock, lock, 0o444);
|
|
843
|
+
}
|
|
844
|
+
async function requireExistingCapabilitiesVerified(root) {
|
|
845
|
+
const verification = await verifyCapabilities(root);
|
|
846
|
+
if (verification.status !== "verified") {
|
|
847
|
+
throw new CliError("Existing research capabilities are drifted; configuration changes cannot replace their lock.", {
|
|
848
|
+
code: "RESEARCH_CAPABILITY_DRIFT",
|
|
849
|
+
exitCode: 3,
|
|
850
|
+
details: {
|
|
851
|
+
verification,
|
|
852
|
+
remediation: "Restore the declared Skill bytes or update its immutable source and expected tree hash explicitly before retrying.",
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
async function probeCapability(capability, healthCheck, credentialMap, fetcher = fetch, sleeper = sleep) {
|
|
858
|
+
const initial = new URL(healthCheck.url);
|
|
859
|
+
const headers = new Headers({ Accept: capability.http?.accept ?? "application/json" });
|
|
860
|
+
let credentialHosts = null;
|
|
861
|
+
if (healthCheck.credentialId) {
|
|
862
|
+
const credential = capability.credentials.find((candidate) => candidate.id === healthCheck.credentialId);
|
|
863
|
+
const value = credentialMap.get(healthCheck.credentialId);
|
|
864
|
+
if (!credential || !value)
|
|
865
|
+
throw new Error("health check credential is unavailable");
|
|
866
|
+
credentialHosts = credential.allowedHosts;
|
|
867
|
+
headers.set(credential.headerName, `${credential.prefix}${value}`);
|
|
868
|
+
}
|
|
869
|
+
let current = initial;
|
|
870
|
+
try {
|
|
871
|
+
let redirectCount = 0;
|
|
872
|
+
let rateLimitRetries = 0;
|
|
873
|
+
while (redirectCount <= 5) {
|
|
874
|
+
if (current.protocol !== "https:" ||
|
|
875
|
+
current.username ||
|
|
876
|
+
current.password ||
|
|
877
|
+
!capability.allowedHosts.includes(current.host.toLowerCase())) {
|
|
878
|
+
throw new Error("health check redirect escaped the capability host policy");
|
|
879
|
+
}
|
|
880
|
+
if (credentialHosts && !credentialHosts.includes(current.host.toLowerCase())) {
|
|
881
|
+
throw new Error("health check redirect escaped the credential host policy");
|
|
882
|
+
}
|
|
883
|
+
const response = await fetcher(current, {
|
|
884
|
+
method: "GET",
|
|
885
|
+
headers,
|
|
886
|
+
redirect: "manual",
|
|
887
|
+
signal: AbortSignal.timeout(30_000),
|
|
888
|
+
});
|
|
889
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
890
|
+
const location = response.headers.get("location");
|
|
891
|
+
await response.body?.cancel();
|
|
892
|
+
if (!location || redirectCount === 5) {
|
|
893
|
+
throw new Error("health check redirect policy failed");
|
|
894
|
+
}
|
|
895
|
+
current = new URL(location, current);
|
|
896
|
+
redirectCount += 1;
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
900
|
+
const retryAfterSeconds = parseRetryAfter(response.headers.get("retry-after"));
|
|
901
|
+
await response.body?.cancel();
|
|
902
|
+
if (response.status === 429 && rateLimitRetries === 0) {
|
|
903
|
+
rateLimitRetries += 1;
|
|
904
|
+
await sleeper(Math.min(5, retryAfterSeconds ?? 1) * 1000);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (!response.ok) {
|
|
908
|
+
return {
|
|
909
|
+
status: "fail",
|
|
910
|
+
code: response.status === 401 || response.status === 403
|
|
911
|
+
? "authentication-failed"
|
|
912
|
+
: response.status === 429
|
|
913
|
+
? "rate-limited"
|
|
914
|
+
: response.status >= 500
|
|
915
|
+
? "provider-unavailable"
|
|
916
|
+
: "request-rejected",
|
|
917
|
+
host: current.host,
|
|
918
|
+
targetSha256: sha256Text(current.toString()),
|
|
919
|
+
httpStatus: response.status,
|
|
920
|
+
retryAfterSeconds,
|
|
921
|
+
detail: `Provider returned HTTP ${response.status}.`,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
if (!healthCheck.expectedContentTypes.includes(contentType)) {
|
|
925
|
+
return {
|
|
926
|
+
status: "fail",
|
|
927
|
+
code: "content-type-mismatch",
|
|
928
|
+
host: current.host,
|
|
929
|
+
targetSha256: sha256Text(current.toString()),
|
|
930
|
+
httpStatus: response.status,
|
|
931
|
+
retryAfterSeconds,
|
|
932
|
+
detail: `Provider returned unsupported content type ${contentType || "unknown"}.`,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
status: "pass",
|
|
937
|
+
code: "connected",
|
|
938
|
+
host: current.host,
|
|
939
|
+
targetSha256: sha256Text(current.toString()),
|
|
940
|
+
httpStatus: response.status,
|
|
941
|
+
retryAfterSeconds,
|
|
942
|
+
detail: "Provider authentication and response contract passed.",
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
throw new Error("health check redirect policy failed");
|
|
946
|
+
}
|
|
947
|
+
catch (error) {
|
|
948
|
+
void error;
|
|
949
|
+
return {
|
|
950
|
+
status: "fail",
|
|
951
|
+
code: "transport-failed",
|
|
952
|
+
host: initial.host,
|
|
953
|
+
targetSha256: sha256Text(initial.toString()),
|
|
954
|
+
httpStatus: null,
|
|
955
|
+
retryAfterSeconds: null,
|
|
956
|
+
detail: "Provider connectivity probe failed before a valid response was received.",
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function capabilityVerificationErrors(verification, capabilityId) {
|
|
961
|
+
if (!verification)
|
|
962
|
+
return ["capability verification was not run"];
|
|
963
|
+
if (verification.status === "verified")
|
|
964
|
+
return [];
|
|
965
|
+
const scoped = verification.errors.filter((error) => error.startsWith(`${capabilityId}:`));
|
|
966
|
+
const global = verification.errors.filter((error) => !error.includes(":"));
|
|
967
|
+
return [...scoped, ...global];
|
|
968
|
+
}
|
|
969
|
+
function isExternalCapabilitySource(source) {
|
|
970
|
+
if (!source)
|
|
971
|
+
return false;
|
|
972
|
+
const normalized = source.locator.trim().replace(/\/+$/, "").toLowerCase();
|
|
973
|
+
try {
|
|
974
|
+
const locator = new URL(source.locator);
|
|
975
|
+
const repositoryPath = locator.pathname
|
|
976
|
+
.replace(/\/+$/, "")
|
|
977
|
+
.replace(/\.git$/i, "")
|
|
978
|
+
.toLowerCase();
|
|
979
|
+
if (locator.hostname.toLowerCase() === "github.com" &&
|
|
980
|
+
repositoryPath === "/tiangong-ai/skills") {
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
catch {
|
|
985
|
+
// Registry and local locators are not required to be URLs.
|
|
986
|
+
}
|
|
987
|
+
return !normalized.includes("tiangong-ai-workspace-suite/skills");
|
|
988
|
+
}
|
|
989
|
+
function parseRetryAfter(value) {
|
|
990
|
+
if (!value)
|
|
991
|
+
return null;
|
|
992
|
+
const seconds = Number(value);
|
|
993
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
994
|
+
return Math.ceil(seconds);
|
|
995
|
+
const timestamp = Date.parse(value);
|
|
996
|
+
if (!Number.isFinite(timestamp))
|
|
997
|
+
return null;
|
|
998
|
+
return Math.max(0, Math.ceil((timestamp - Date.now()) / 1000));
|
|
999
|
+
}
|
|
1000
|
+
//# sourceMappingURL=external-skills.js.map
|