@bevel-software/platform-core-backend 0.34.0 → 0.35.0
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/agent-guide/access-control.md +146 -300
- package/agent-guide/conventions.md +23 -25
- package/agent-guide/directory-structure.md +72 -120
- package/agent-guide/drafts-and-change-requests.md +92 -0
- package/agent-guide/for-git-users.md +47 -0
- package/agent-guide/glossary.md +36 -0
- package/agent-guide/how-agents-find-things.md +49 -0
- package/agent-guide/html-views.md +16 -11
- package/agent-guide/new-knowledge-base.md +9 -8
- package/agent-guide/skills.md +67 -42
- package/agent-guide/start-here.md +65 -0
- package/agent-guide/tool-manuals.md +132 -154
- package/agent-guide/tools.md +67 -0
- package/agent-guide/where-a-new-file-goes.md +45 -28
- package/dist/core/create-core-server.js +1 -1
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +6 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +7 -4
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +22 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +3 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +75 -0
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +23 -4
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/index.d.ts +1 -1
- package/dist/modules/access/index.d.ts.map +1 -1
- package/dist/modules/agent-guide/agent-guide.d.ts +47 -7
- package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
- package/dist/modules/agent-guide/agent-guide.js +103 -13
- package/dist/modules/agent-guide/agent-guide.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.tools.d.ts +6 -2
- package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -1
- package/dist/modules/agent-guide/agent-guide.tools.js +45 -31
- package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -1
- package/dist/modules/agent-guide/guide-budget.d.ts +48 -0
- package/dist/modules/agent-guide/guide-budget.d.ts.map +1 -0
- package/dist/modules/agent-guide/guide-budget.js +67 -0
- package/dist/modules/agent-guide/guide-budget.js.map +1 -0
- package/dist/modules/agent-guide/index.d.ts +2 -1
- package/dist/modules/agent-guide/index.d.ts.map +1 -1
- package/dist/modules/agent-guide/index.js +2 -1
- package/dist/modules/agent-guide/index.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +4 -0
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +10 -8
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +2 -2
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/connection-probe/connection-probe.service.d.ts +0 -12
- package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
- package/dist/modules/connection-probe/connection-probe.service.js +1 -38
- package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +6 -35
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +11 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.js +159 -24
- package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
- package/dist/modules/secrets-vault/index.d.ts +1 -1
- package/dist/modules/secrets-vault/index.d.ts.map +1 -1
- package/dist/modules/secrets-vault/index.js +1 -1
- package/dist/modules/secrets-vault/index.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +53 -11
- package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.contract.js +27 -0
- package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
- package/dist/modules/secrets-vault/secrets-vault.routes.js +85 -39
- package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
- package/dist/modules/settings/oidc-check.d.ts.map +1 -1
- package/dist/modules/settings/oidc-check.js +5 -17
- package/dist/modules/settings/oidc-check.js.map +1 -1
- package/dist/modules/skills/skills.tools.js +1 -1
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-helpers/tool-def.js +1 -1
- package/dist/modules/tool-helpers/tool-def.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +7 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.js +69 -38
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +27 -0
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +12 -8
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +115 -44
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +8 -2
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +31 -10
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +22 -18
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -1
- package/dist/modules/workspace/agent-access.gate.js +1 -1
- package/dist/modules/workspace/agent-access.gate.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +3 -2
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +71 -21
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/oauth-token-error.d.ts +23 -0
- package/dist/shared/oauth-token-error.d.ts.map +1 -0
- package/dist/shared/oauth-token-error.js +47 -0
- package/dist/shared/oauth-token-error.js.map +1 -0
- package/dist/shared/read-capped.d.ts +19 -0
- package/dist/shared/read-capped.d.ts.map +1 -0
- package/dist/shared/read-capped.js +45 -0
- package/dist/shared/read-capped.js.map +1 -0
- package/package.json +4 -3
- package/src/__tests__/hexis-catalog.ts +144 -0
- package/src/__tests__/real-disk.ts +21 -0
- package/src/__tests__/tokens.ts +19 -0
- package/src/__tests__/tool-manuals.ts +32 -0
- package/src/__tests__/tool-wiring.ts +25 -0
- package/src/core/__tests__/oauth-sign-in-real-file.test.ts +179 -0
- package/src/core/create-core-server.ts +1 -1
- package/src/core/create-core-services.ts +13 -3
- package/src/modules/access/__tests__/access.routes.prospective.test.ts +52 -12
- package/src/modules/access/__tests__/holder-changes.test.ts +127 -0
- package/src/modules/access/access-control.interface.ts +26 -0
- package/src/modules/access/access-control.service.ts +92 -0
- package/src/modules/access/access.routes.ts +23 -4
- package/src/modules/access/index.ts +1 -1
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +79 -25
- package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +38 -11
- package/src/modules/agent-guide/__tests__/guide-budget.test.ts +95 -0
- package/src/modules/agent-guide/__tests__/guide-eval.test.ts +182 -0
- package/src/modules/agent-guide/__tests__/guide-matches-code.test.ts +263 -0
- package/src/modules/agent-guide/__tests__/tool-surface.json +514 -0
- package/src/modules/agent-guide/__tests__/tool-surface.test.ts +75 -0
- package/src/modules/agent-guide/agent-guide.tools.ts +49 -31
- package/src/modules/agent-guide/agent-guide.ts +120 -13
- package/src/modules/agent-guide/guide-budget.ts +73 -0
- package/src/modules/agent-guide/index.ts +12 -0
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +4 -4
- package/src/modules/agent-instructions/compose.ts +4 -0
- package/src/modules/agent-instructions/shared-file-rules.ts +10 -8
- package/src/modules/code-mode/code-mode.tool.ts +2 -2
- package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +5 -24
- package/src/modules/connection-probe/connection-probe.service.ts +1 -34
- package/src/modules/mcp/mcp.service.ts +5 -35
- package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +430 -3
- package/src/modules/secrets-vault/__tests__/oauth-live-declaration.route.test.ts +218 -0
- package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +39 -6
- package/src/modules/secrets-vault/db-secrets-vault.service.ts +170 -27
- package/src/modules/secrets-vault/index.ts +1 -1
- package/src/modules/secrets-vault/secrets-vault.contract.ts +61 -11
- package/src/modules/secrets-vault/secrets-vault.routes.ts +91 -39
- package/src/modules/settings/oidc-check.ts +5 -18
- package/src/modules/skills/skills.tools.ts +1 -1
- package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +19 -11
- package/src/modules/tool-helpers/__tests__/route-tool-schemas.catalog.test.ts +38 -0
- package/src/modules/tool-helpers/tool-def.ts +1 -1
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +197 -11
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +96 -5
- package/src/modules/tool-manuals/mcp-json-discovery.ts +73 -34
- package/src/modules/tool-manuals/tool-manuals.contract.ts +22 -0
- package/src/modules/tool-manuals/tool-manuals.service.ts +138 -46
- package/src/modules/tool-manuals/tool-manuals.tools.ts +53 -14
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +6 -113
- package/src/modules/workflow/agent-tools/workflow.tools.ts +22 -18
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +8 -10
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +156 -49
- package/src/modules/workspace/agent-access.gate.ts +1 -1
- package/src/modules/workspace/workspace.tools.ts +74 -21
- package/src/shared/__tests__/read-capped.test.ts +68 -0
- package/src/shared/oauth-token-error.ts +46 -0
- package/src/shared/read-capped.ts +40 -0
- package/agent-guide/finding-things.md +0 -7
- package/agent-guide/introduction.md +0 -27
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
type ToolProbeTarget,
|
|
60
60
|
type ToolVariableScope,
|
|
61
61
|
type ToolVariableOAuth,
|
|
62
|
+
type LiveDeclaration,
|
|
62
63
|
} from './tool-manuals.contract.js';
|
|
63
64
|
|
|
64
65
|
const CACHE_TTL_MS = 60_000;
|
|
@@ -182,6 +183,19 @@ export function describeManualFault(err: unknown): string {
|
|
|
182
183
|
interface ScanResult {
|
|
183
184
|
manuals: ToolManualDescriptor[];
|
|
184
185
|
invalid: InvalidToolManual[];
|
|
186
|
+
/**
|
|
187
|
+
* Every manual the parse refused whose file still says which manual it is,
|
|
188
|
+
* by slug, with the reason. Not a listing surface (`invalid` is that, by
|
|
189
|
+
* path): it lets a sign-in started on a refused declaration answer why,
|
|
190
|
+
* where the slug alone would otherwise find nothing.
|
|
191
|
+
*/
|
|
192
|
+
refused: RefusedManual[];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
interface RefusedManual {
|
|
196
|
+
slug: string;
|
|
197
|
+
path: string;
|
|
198
|
+
reason: string;
|
|
185
199
|
}
|
|
186
200
|
|
|
187
201
|
/**
|
|
@@ -191,7 +205,7 @@ interface ScanResult {
|
|
|
191
205
|
* convention instance is one push away from every empty scan in the process
|
|
192
206
|
* inheriting another's contents.
|
|
193
207
|
*/
|
|
194
|
-
const emptyScan = (): ScanResult => ({ manuals: [], invalid: [] });
|
|
208
|
+
const emptyScan = (): ScanResult => ({ manuals: [], invalid: [], refused: [] });
|
|
195
209
|
|
|
196
210
|
const manualSerializer = new UtcpManualSerializer();
|
|
197
211
|
const callTemplateSerializer = new CallTemplateSerializer();
|
|
@@ -209,8 +223,8 @@ export interface McpAuthDiscoveryPort {
|
|
|
209
223
|
/**
|
|
210
224
|
* The sign-in endpoints for an OWNER-REGISTERED client: the same metadata
|
|
211
225
|
* walk as `statusFor`, stopping short of dynamic registration — the manual
|
|
212
|
-
* already names its `clientId`. Nothing is persisted;
|
|
213
|
-
*
|
|
226
|
+
* already names its `clientId`. Nothing is persisted; every sign-in start
|
|
227
|
+
* completes the declaration afresh (`liveDeclaration`). Optional so a port that
|
|
214
228
|
* only knows the zero-config path still satisfies the interface.
|
|
215
229
|
*/
|
|
216
230
|
providerForDeclaredClient?(manualName: string, mcpUrl: string, clientId: string): Promise<McpAuthDiscoveryResult>;
|
|
@@ -342,6 +356,38 @@ export class ToolManualService implements IToolManualService {
|
|
|
342
356
|
};
|
|
343
357
|
}
|
|
344
358
|
|
|
359
|
+
async liveDeclaration(
|
|
360
|
+
userEmail: string,
|
|
361
|
+
slug: string,
|
|
362
|
+
varName: string,
|
|
363
|
+
): Promise<LiveDeclaration | null> {
|
|
364
|
+
// The disk walk the catalog's scan is built from, run afresh and never
|
|
365
|
+
// cached: a sign-in must follow the file as it is, and the catalog's scan
|
|
366
|
+
// can be a refresh behind it. The whole walk rather than the one file, so
|
|
367
|
+
// the descriptor comes out of exactly the parse, plugin discovery and
|
|
368
|
+
// namespace dedup the catalog applies — a sign-in start is rare and
|
|
369
|
+
// interactive, and a second, single-file reading of the same files is a
|
|
370
|
+
// second place for those rules to drift. A workspace that cannot be opened
|
|
371
|
+
// is an error here, never an empty answer: "no sign-in declared" would be
|
|
372
|
+
// the wrong diagnosis for a server that could not read its files.
|
|
373
|
+
const { manuals, refused } = await this.readableBy(await this.scanDisk(undefined, 'throw'), userEmail);
|
|
374
|
+
const m = manuals.find((x) => x.slug === slug);
|
|
375
|
+
if (!m) {
|
|
376
|
+
// The file is there and says it is this manual, but the parse refused
|
|
377
|
+
// it — e.g. its sign-in names an http:// or private token endpoint. That
|
|
378
|
+
// reason is the answer; "not found" would hide the one thing to fix.
|
|
379
|
+
const r = refused.find((x) => x.slug === slug);
|
|
380
|
+
return r ? { refused: r.reason } : null;
|
|
381
|
+
}
|
|
382
|
+
const variable = (m.variables ?? []).find((v) => v.name === varName);
|
|
383
|
+
// Completed by the catalog's own decoration, on this one fresh descriptor.
|
|
384
|
+
// Only when THIS variable is a sign-in: then the manual has a declared one,
|
|
385
|
+
// so the pass completes its endpoints from the server's metadata and never
|
|
386
|
+
// probes for (or registers) a zero-config client.
|
|
387
|
+
if (variable?.oauth) await this.decorateMcpOAuth([m]);
|
|
388
|
+
return { manual: toSummary(m), ...(variable ? { variable } : {}) };
|
|
389
|
+
}
|
|
390
|
+
|
|
345
391
|
async listLocalOnly(userEmail: string, within?: ConnectorScope): Promise<{ slug: string; name: string; path: string }[]> {
|
|
346
392
|
const manuals = await this.accessibleManuals(userEmail, within);
|
|
347
393
|
return manuals.filter((m) => m.remote === false).map((m) => ({ slug: m.slug, name: m.name, path: m.path }));
|
|
@@ -616,11 +662,16 @@ export class ToolManualService implements IToolManualService {
|
|
|
616
662
|
* in. Absent, the answer is the caller's whole readable catalog.
|
|
617
663
|
*/
|
|
618
664
|
private async accessibleScan(userEmail: string, within?: ConnectorScope): Promise<ScanResult> {
|
|
619
|
-
|
|
620
|
-
|
|
665
|
+
return this.readableBy(await this.scan(), userEmail, within);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** A scan cut to what this caller may read — see {@link accessibleScan}. */
|
|
669
|
+
private async readableBy(scan: ScanResult, userEmail: string, within?: ConnectorScope): Promise<ScanResult> {
|
|
670
|
+
const { manuals, invalid, refused } = scan;
|
|
671
|
+
if (manuals.length === 0 && invalid.length === 0 && refused.length === 0) return emptyScan();
|
|
621
672
|
const wsId = this.kb.defaultWorkspaceId();
|
|
622
673
|
const allowed = await this.accessControl.canReadBatch(wsId, userEmail, [
|
|
623
|
-
...new Set([...manuals.map((m) => m.path), ...invalid.map((i) => i.path)]),
|
|
674
|
+
...new Set([...manuals.map((m) => m.path), ...invalid.map((i) => i.path), ...refused.map((r) => r.path)]),
|
|
624
675
|
]);
|
|
625
676
|
// Fail closed: keep an entry only on an explicit `true` verdict (a missing
|
|
626
677
|
// entry is treated as denied, matching the KB's default-deny read model).
|
|
@@ -631,6 +682,7 @@ export class ToolManualService implements IToolManualService {
|
|
|
631
682
|
return {
|
|
632
683
|
manuals: manuals.filter((m) => inScope(m.path)),
|
|
633
684
|
invalid: invalid.filter((i) => inScope(i.path)),
|
|
685
|
+
refused: refused.filter((r) => inScope(r.path)),
|
|
634
686
|
};
|
|
635
687
|
}
|
|
636
688
|
|
|
@@ -655,14 +707,14 @@ export class ToolManualService implements IToolManualService {
|
|
|
655
707
|
// See `TtlCache.begin`: taken before the read so an `invalidate()` that
|
|
656
708
|
// lands mid-scan discards this result instead of being overwritten by it.
|
|
657
709
|
const token = this.cache.begin();
|
|
658
|
-
const
|
|
710
|
+
const scanned = await this.scanDisk();
|
|
711
|
+
const { manuals } = scanned;
|
|
659
712
|
await this.decorateMcpOAuth(manuals);
|
|
660
713
|
// AFTER the oauth decoration, so an injected `${MCP_OAUTH}` header ref is
|
|
661
714
|
// already declared and isn't re-surfaced as a bare admin key.
|
|
662
715
|
for (const m of manuals) this.surfaceReferencedVariables(m);
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
return result;
|
|
716
|
+
this.cache.set(scanned, token);
|
|
717
|
+
return scanned;
|
|
666
718
|
}
|
|
667
719
|
|
|
668
720
|
/**
|
|
@@ -833,11 +885,12 @@ export class ToolManualService implements IToolManualService {
|
|
|
833
885
|
* (the owner registered an app with a provider that offers no dynamic
|
|
834
886
|
* registration — HubSpot, Google) gets its endpoints, PKCE and resource
|
|
835
887
|
* indicator from the server's own OAuth metadata, exactly as the zero-config
|
|
836
|
-
* path would. The descriptor is completed IN MEMORY
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
* incomplete and `setup.reason` says so — the secret
|
|
840
|
-
* with the same reason instead of
|
|
888
|
+
* path would. The descriptor is completed IN MEMORY, for the catalog's scan
|
|
889
|
+
* and for every sign-in start (`liveDeclaration`) alike, so nothing here
|
|
890
|
+
* persists. When the metadata can't be had, the declaration stays
|
|
891
|
+
* incomplete and `setup.reason` says so — the client-secret save and the
|
|
892
|
+
* sign-in start then refuse with the same reason instead of running a
|
|
893
|
+
* provider with no endpoints.
|
|
841
894
|
*/
|
|
842
895
|
private async completeDeclaredOAuth(
|
|
843
896
|
discovery: McpAuthDiscoveryPort,
|
|
@@ -896,11 +949,22 @@ export class ToolManualService implements IToolManualService {
|
|
|
896
949
|
* calls: a refusal lives only in the scan it came from, so a fixed file is a
|
|
897
950
|
* valid manual on the very next scan.
|
|
898
951
|
*/
|
|
899
|
-
private async scanDisk(
|
|
952
|
+
private async scanDisk(
|
|
953
|
+
branch: string = this.kb.defaultBranch,
|
|
954
|
+
/**
|
|
955
|
+
* What a workspace that cannot be opened means. For the catalog, an empty
|
|
956
|
+
* scan: a listing surface answers "nothing released" and the next scan
|
|
957
|
+
* tries again. For a sign-in start (`liveDeclaration`), the error itself:
|
|
958
|
+
* an empty answer there would read as "this tool has no sign-in", a
|
|
959
|
+
* misconfiguration, when the server could not read its files at all.
|
|
960
|
+
*/
|
|
961
|
+
workspaceFailure: 'empty' | 'throw' = 'empty',
|
|
962
|
+
): Promise<ScanResult> {
|
|
900
963
|
let wsId: string;
|
|
901
964
|
try {
|
|
902
965
|
wsId = (await this.workspaceService.getOrCreateForBranch(branch)).id;
|
|
903
|
-
} catch {
|
|
966
|
+
} catch (err) {
|
|
967
|
+
if (workspaceFailure === 'throw') throw err;
|
|
904
968
|
return emptyScan();
|
|
905
969
|
}
|
|
906
970
|
const kbRoot = path.join(await this.workspaceService.getWorkspacePath(wsId), this.kbDirName);
|
|
@@ -921,6 +985,7 @@ export class ToolManualService implements IToolManualService {
|
|
|
921
985
|
// The plugins (and their mcp.json text) come from the configured source, so
|
|
922
986
|
// a dialect that expands servers from a registry lands here unchanged.
|
|
923
987
|
const parsed: ToolManualDescriptor[] = [];
|
|
988
|
+
const refused: RefusedManual[] = [];
|
|
924
989
|
const discovered = await this.source.discover(kbRoot);
|
|
925
990
|
for (const plugin of discovered.plugins) {
|
|
926
991
|
if (plugin.mcpJsonText === null) continue; // no servers is the common case, not an error
|
|
@@ -929,7 +994,9 @@ export class ToolManualService implements IToolManualService {
|
|
|
929
994
|
// (a URL, a header, an added or dropped server, a `local: true` flip)
|
|
930
995
|
// moves every server's digest — which is correct, if slightly generous.
|
|
931
996
|
const from = sourceDigest(plugin.mcpJsonText + '\u0000' + (plugin.manifestText ?? ''));
|
|
932
|
-
|
|
997
|
+
const onRefused = (slug: string, reason: string, path: string) =>
|
|
998
|
+
refused.push({ slug, path, reason: describeManualFault(reason) });
|
|
999
|
+
for (const d of descriptorsFromMcpJson(plugin.relFolder, plugin.mcpJsonText, plugin.manifestText, layout, onRefused)) {
|
|
933
1000
|
d.sourceRevision = from;
|
|
934
1001
|
parsed.push(d);
|
|
935
1002
|
}
|
|
@@ -965,6 +1032,8 @@ export class ToolManualService implements IToolManualService {
|
|
|
965
1032
|
// outage from where the author stands: the tool is gone from every
|
|
966
1033
|
// listing, with nothing anywhere saying which file or why.
|
|
967
1034
|
invalid.push({ path: f.rel, reason: describeManualFault(err) });
|
|
1035
|
+
const slug = refusedToolManualName(baseName(f.rel), content);
|
|
1036
|
+
if (slug !== null) refused.push({ slug, path: f.rel, reason: describeManualFault(err) });
|
|
968
1037
|
continue;
|
|
969
1038
|
}
|
|
970
1039
|
// The route slug IS the id (unique after dedup below, snake_case → URL-safe),
|
|
@@ -1013,7 +1082,7 @@ export class ToolManualService implements IToolManualService {
|
|
|
1013
1082
|
// draft workspace, and a draft's half-written `.tool` is not news about the
|
|
1014
1083
|
// served catalog — it would also flap the log every time an agent saves.
|
|
1015
1084
|
if (branch === this.kb.defaultBranch) this.logInvalidOnChange(invalid);
|
|
1016
|
-
return { manuals, invalid };
|
|
1085
|
+
return { manuals, invalid, refused };
|
|
1017
1086
|
}
|
|
1018
1087
|
}
|
|
1019
1088
|
|
|
@@ -1125,6 +1194,54 @@ function manualName(raw: string): string {
|
|
|
1125
1194
|
return name;
|
|
1126
1195
|
}
|
|
1127
1196
|
|
|
1197
|
+
/**
|
|
1198
|
+
* The object a `.tool` file holds: its `---` block when fenced, the whole file
|
|
1199
|
+
* when not. Throws when that is not an object.
|
|
1200
|
+
*/
|
|
1201
|
+
function toolFileObject(content: string): Record<string, unknown> {
|
|
1202
|
+
const fm = extractFrontmatter(content);
|
|
1203
|
+
const source = fm ? fm.frontmatter : content;
|
|
1204
|
+
const parsed = parseYaml(source) as unknown;
|
|
1205
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1206
|
+
throw new Error('`.tool` file must be a YAML/JSON object (in the `---` block when fenced)');
|
|
1207
|
+
}
|
|
1208
|
+
return parsed as Record<string, unknown>;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* A `.tool`'s manual name — its UTCP namespace, and the slug the scan serves it
|
|
1213
|
+
* under. An explicit `id` is used VERBATIM, so it must be lowercase snake_case
|
|
1214
|
+
* (UTCP's namespace grammar). With no explicit id, fall back to `name` then the
|
|
1215
|
+
* filename and sanitize (legacy behaviour — keeps existing tools' namespaces).
|
|
1216
|
+
*/
|
|
1217
|
+
function toolManualName(obj: Record<string, unknown>, provisionalSlug: string): string {
|
|
1218
|
+
const explicitId = typeof obj.id === 'string' ? obj.id.trim() : '';
|
|
1219
|
+
if (!explicitId) return manualName(resolveDeclaredId(obj, provisionalSlug));
|
|
1220
|
+
if (!isValidId(explicitId)) {
|
|
1221
|
+
// The rejected id is NOT quoted back — see `describeManualFault`: this
|
|
1222
|
+
// message becomes an `invalid[].reason` on an agent transcript, a browser
|
|
1223
|
+
// and a log, and `id` is a field an author can paste anything into,
|
|
1224
|
+
// including the token they meant for a header. The field name plus the
|
|
1225
|
+
// rule is what the author needs; the value is in front of them.
|
|
1226
|
+
throw new Error('tool `id` must be lowercase snake_case (letters, digits, underscores)');
|
|
1227
|
+
}
|
|
1228
|
+
return explicitId;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* The manual name of a `.tool` the parser REFUSED, when the file still says
|
|
1233
|
+
* which manual it is — by the same rule {@link normalizeToolManual} names a
|
|
1234
|
+
* manual it accepts. `null` when even that cannot be read (unparseable YAML,
|
|
1235
|
+
* a malformed `id`): then nothing can claim the refusal by name.
|
|
1236
|
+
*/
|
|
1237
|
+
function refusedToolManualName(provisionalSlug: string, content: string): string | null {
|
|
1238
|
+
try {
|
|
1239
|
+
return toolManualName(toolFileObject(content), provisionalSlug);
|
|
1240
|
+
} catch {
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1128
1245
|
/**
|
|
1129
1246
|
* Parse + normalize a `.tool` file into a descriptor. THE TOOL IS THE
|
|
1130
1247
|
* FRONTMATTER: a fenced file's single `---` YAML block carries everything —
|
|
@@ -1148,34 +1265,9 @@ export function normalizeToolManual(
|
|
|
1148
1265
|
// finds its verbs inside the fence, the id index finds `id`/`name`, and the
|
|
1149
1266
|
// config keys live in the same object. A fence-less file is the legacy form
|
|
1150
1267
|
// (the whole file is the object) — JSON `.tool`s can't carry fences.
|
|
1151
|
-
const
|
|
1152
|
-
const source = fm ? fm.frontmatter : content;
|
|
1153
|
-
const parsed = parseYaml(source) as unknown;
|
|
1154
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1155
|
-
throw new Error('`.tool` file must be a YAML/JSON object (in the `---` block when fenced)');
|
|
1156
|
-
}
|
|
1157
|
-
const obj = parsed as Record<string, unknown>;
|
|
1268
|
+
const obj = toolFileObject(content);
|
|
1158
1269
|
const type = normalizeType(obj.type);
|
|
1159
|
-
|
|
1160
|
-
// Identity: an explicit `id` is used VERBATIM as the UTCP manual namespace, so
|
|
1161
|
-
// it must be lowercase snake_case (UTCP's namespace grammar). With no explicit
|
|
1162
|
-
// id, fall back to `name` then the filename and sanitize (legacy behaviour —
|
|
1163
|
-
// keeps existing tools' namespaces).
|
|
1164
|
-
const explicitId = typeof obj.id === 'string' ? obj.id.trim() : '';
|
|
1165
|
-
let name: string;
|
|
1166
|
-
if (explicitId) {
|
|
1167
|
-
if (!isValidId(explicitId)) {
|
|
1168
|
-
// The rejected id is NOT quoted back — see `describeManualFault`: this
|
|
1169
|
-
// message becomes an `invalid[].reason` on an agent transcript, a browser
|
|
1170
|
-
// and a log, and `id` is a field an author can paste anything into,
|
|
1171
|
-
// including the token they meant for a header. The field name plus the
|
|
1172
|
-
// rule is what the author needs; the value is in front of them.
|
|
1173
|
-
throw new Error('tool `id` must be lowercase snake_case (letters, digits, underscores)');
|
|
1174
|
-
}
|
|
1175
|
-
name = explicitId;
|
|
1176
|
-
} else {
|
|
1177
|
-
name = manualName(resolveDeclaredId(obj, provisionalSlug));
|
|
1178
|
-
}
|
|
1270
|
+
const name = toolManualName(obj, provisionalSlug);
|
|
1179
1271
|
|
|
1180
1272
|
// A `.tool` may not reproduce a built-in manual's UTCP namespace — that would
|
|
1181
1273
|
// let it read the loopback creds seeded under that namespace on the agent's
|
|
@@ -7,18 +7,32 @@ import type { IAccessControl } from '../access/index.js';
|
|
|
7
7
|
import { utcpNamespacedKey } from '../../shared/utcp-namespace.js';
|
|
8
8
|
import type { ConnectorScope, IToolManualService } from './tool-manuals.contract.js';
|
|
9
9
|
import type { HiddenToolSource } from '../../shared/hidden-tools.js';
|
|
10
|
+
import { userSecretGap } from '../secrets-vault/index.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* What `list_tool_setup` needs from the secrets vault (structurally satisfied
|
|
13
14
|
* by `ISecretsVaultService.statusFor`). A local port keeps this module free of
|
|
14
|
-
*
|
|
15
|
-
* `McpAuthDiscoveryPort` in the service.
|
|
15
|
+
* the vault service — the same decoupling discipline as
|
|
16
|
+
* `McpAuthDiscoveryPort` in the service. Only the vault's pure readiness rule
|
|
17
|
+
* (`userSecretGap`, its exported contract) is imported, the one the MCP proxy
|
|
18
|
+
* hides tools by.
|
|
16
19
|
*/
|
|
17
20
|
export interface VariableStatusPort {
|
|
18
21
|
statusFor(
|
|
19
22
|
userId: string,
|
|
20
23
|
keys: string[],
|
|
21
|
-
): Promise<
|
|
24
|
+
): Promise<
|
|
25
|
+
{
|
|
26
|
+
key: string;
|
|
27
|
+
adminConfigured: boolean;
|
|
28
|
+
/** What the owner's row is: a sign-in needs an `oauth` one, not a key left from when the variable was static. */
|
|
29
|
+
adminKind?: 'static' | 'oauth' | null;
|
|
30
|
+
userConfigured: boolean;
|
|
31
|
+
userAuthorized?: boolean;
|
|
32
|
+
userKind?: 'static' | 'oauth' | null;
|
|
33
|
+
grantedScopes?: string;
|
|
34
|
+
}[]
|
|
35
|
+
>;
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
/**
|
|
@@ -240,7 +254,9 @@ export function registerToolManualsTools(
|
|
|
240
254
|
},
|
|
241
255
|
note: {
|
|
242
256
|
type: 'string',
|
|
243
|
-
description:
|
|
257
|
+
description:
|
|
258
|
+
'Present when something is left to do: what it takes for the tools in `onBranchOnly` to go live, and for ' +
|
|
259
|
+
'tools that need your own value or sign-in to work.',
|
|
244
260
|
},
|
|
245
261
|
},
|
|
246
262
|
required: ['tools', 'invalid', 'onBranchOnly'],
|
|
@@ -302,17 +318,40 @@ export function registerToolManualsTools(
|
|
|
302
318
|
}),
|
|
303
319
|
);
|
|
304
320
|
const onBranchOnly = pending.map((p) => ({ ...p, branch: branch! }));
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
invalid,
|
|
309
|
-
onBranchOnly,
|
|
310
|
-
note:
|
|
321
|
+
const notes: string[] = [];
|
|
322
|
+
if (onBranchOnly.length > 0) {
|
|
323
|
+
notes.push(
|
|
311
324
|
`${onBranchOnly.map((p) => `\`${p.name}\``).join(', ')} ${onBranchOnly.length === 1 ? 'is' : 'are'} declared on ` +
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
325
|
+
`\`${branch}\` only. Tools are served from \`${deps.kb.defaultBranch}\`, so ${onBranchOnly.length === 1 ? 'it stays' : 'they stay'} ` +
|
|
326
|
+
'unlisted, uncallable and without a sign-in until that branch is merged: open a change request with ' +
|
|
327
|
+
`\`open_change_request\` (target \`${deps.kb.defaultBranch}\`), then ask the user to review and merge it in the app.`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
// The person's own part, said where the agent is when a tool "does not work": the
|
|
331
|
+
// reconnect step is the one agents leave out (the agent-guide benchmark, 2026-10-26).
|
|
332
|
+
// By the rule the proxy hides a tool by (`userSecretGap`), so the note
|
|
333
|
+
// names every tool that is hidden for want of the caller's own value. A
|
|
334
|
+
// sign-in waits on the owner's provider setup (an OAuth admin row, not a
|
|
335
|
+
// key left from when the variable was static — the Connect page counts
|
|
336
|
+
// that setup incomplete and the sign-in start refuses it): until then
|
|
337
|
+
// there is nothing to sign in to.
|
|
338
|
+
const needYou = manuals.filter((m) =>
|
|
339
|
+
(m.variables ?? []).some((v) => {
|
|
340
|
+
if (v.scope !== 'user') return false;
|
|
341
|
+
const st = statusByKey.get(varKey(m.name, v.name));
|
|
342
|
+
const gap = userSecretGap({ oauth: v.oauth != null, oauthScopes: v.oauth?.scopes }, st);
|
|
343
|
+
return gap !== null && (v.oauth == null || (st?.adminConfigured === true && st.adminKind === 'oauth'));
|
|
344
|
+
}),
|
|
345
|
+
);
|
|
346
|
+
if (needYou.length > 0) {
|
|
347
|
+
notes.push(
|
|
348
|
+
`${needYou.map((t) => `\`${t.name}\``).join(', ')} ${needYou.length === 1 ? 'needs' : 'need'} your own value or sign-in. ` +
|
|
349
|
+
'Set it yourself on the Connect page, never in the chat; then reconnect your assistant, or start a new chat, ' +
|
|
350
|
+
'so the tool is listed.',
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
if (notes.length === 0) return { tools, invalid, onBranchOnly };
|
|
354
|
+
return { tools, invalid, onBranchOnly, note: notes.join(' ') };
|
|
316
355
|
}),
|
|
317
356
|
);
|
|
318
357
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
1
|
import { describe, expect, it } from 'vitest';
|
|
3
2
|
import {
|
|
4
3
|
CALL_TOOL_CHAIN_NAME,
|
|
@@ -9,21 +8,9 @@ import {
|
|
|
9
8
|
splitCallLine,
|
|
10
9
|
} from '@bevel-software/platform-mcp-core';
|
|
11
10
|
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
12
|
-
import { EXTERNAL_KB_MANUAL_NAME } from '../../tool-manuals/
|
|
13
|
-
import { ToolRegistry } from '../tool-registry.js';
|
|
11
|
+
import { EXTERNAL_KB_MANUAL_NAME } from '../../tool-manuals/index.js';
|
|
14
12
|
import type { UtcpTool } from '../tool.contract.js';
|
|
15
|
-
import {
|
|
16
|
-
import type { ToolAuth } from '../../tool-auth/tool-auth.middleware.js';
|
|
17
|
-
import { registerWorkspaceTools } from '../../workspace/workspace.tools.js';
|
|
18
|
-
import { RipgrepPatternMatcher } from '../../workspace/index.js';
|
|
19
|
-
import { registerWorkflowTools } from '../../workflow/agent-tools/workflow.tools.js';
|
|
20
|
-
import { registerPluginsTools } from '../../plugins/plugins.tools.js';
|
|
21
|
-
import { registerSkillsTools } from '../../skills/skills.tools.js';
|
|
22
|
-
import { registerToolManualsTools } from '../../tool-manuals/tool-manuals.tools.js';
|
|
23
|
-
import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
|
|
24
|
-
import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
|
|
25
|
-
import { UuidSessionSink } from '../../workspace/session-sink.js';
|
|
26
|
-
import { RoutineWritePolicyService } from '../../workspace/routine-write-policy.js';
|
|
13
|
+
import type { registerSkillsTools } from '../../skills/index.js';
|
|
27
14
|
import { CLIENT_SHORT_CUT, TOOL_DESCRIPTION_CAP, clientVisibleLength, firstSentenceEnd } from '../description-length.js';
|
|
28
15
|
import { CAN_WRITE_CLAUSE } from '../../tool-helpers/index.js';
|
|
29
16
|
import {
|
|
@@ -31,9 +18,8 @@ import {
|
|
|
31
18
|
sharedFileRules,
|
|
32
19
|
} from '../../agent-instructions/index.js';
|
|
33
20
|
import { isPlatformFile, platformFilesByDepth } from '@bevel-software/platform-shared';
|
|
34
|
-
import { GET_AGENT_GUIDE_TOOL, GUIDE_FIRST_SENTENCE
|
|
35
|
-
import {
|
|
36
|
-
import { agentGuideSections } from '../../agent-guide/agent-guide.js';
|
|
21
|
+
import { GET_AGENT_GUIDE_TOOL, GUIDE_FIRST_SENTENCE } from '../guide-first.js';
|
|
22
|
+
import { emptySkills, hexisTools, servedMetaTools, unused } from '../../../__tests__/hexis-catalog.js';
|
|
37
23
|
|
|
38
24
|
/**
|
|
39
25
|
* The cap exists because clients cut a long tool description, and they cut it
|
|
@@ -43,104 +29,11 @@ import { agentGuideSections } from '../../agent-guide/agent-guide.js';
|
|
|
43
29
|
* NAMING the tool: the next paragraph someone appends to a description has to
|
|
44
30
|
* answer to this test rather than to an agent's truncated catalog.
|
|
45
31
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* is honest about that. Tools PROXIED from connected MCP servers are not
|
|
32
|
+
* The catalog is every registrar Hexis owns, mounted on stand-ins (see
|
|
33
|
+
* `hexis-catalog.ts`). Tools PROXIED from connected MCP servers are not
|
|
49
34
|
* measured — their text is the other server's.
|
|
50
35
|
*/
|
|
51
36
|
|
|
52
|
-
/** Dependencies the registrars take but never touch while they are only building defs. */
|
|
53
|
-
const unused = <T,>(): T => ({}) as T;
|
|
54
|
-
|
|
55
|
-
/** A `.tool` catalog with nothing in it: the shortest honest answer for a listing. */
|
|
56
|
-
const emptyManuals = {
|
|
57
|
-
listLocalOnly: async () => [],
|
|
58
|
-
listAll: async () => [],
|
|
59
|
-
list: async () => [],
|
|
60
|
-
listInvalid: async () => [],
|
|
61
|
-
} as unknown as Parameters<typeof registerToolManualsTools>[4];
|
|
62
|
-
|
|
63
|
-
/** A skill catalog with nothing in it — the per-user skill line is then one fixed sentence. */
|
|
64
|
-
const emptySkills = { listSkills: async () => [] } as unknown as Parameters<typeof registerSkillsTools>[4];
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The meta-tools as the hosted endpoint builds them (`mcp.service.ts`):
|
|
68
|
-
* examples written against the namespace it registers the knowledge-base tools
|
|
69
|
-
* under and against the catalog it serves, and the chain without the rules it
|
|
70
|
-
* states only in the shared places. So the chain description measured here
|
|
71
|
-
* carries the worked call a client is really sent, which is its longest form.
|
|
72
|
-
*/
|
|
73
|
-
function servedMetaTools(external: readonly UtcpTool[]) {
|
|
74
|
-
return codeModeMetaTools(
|
|
75
|
-
EXTERNAL_KB_MANUAL_NAME,
|
|
76
|
-
external.map((t) => ({ utcpName: `${EXTERNAL_KB_MANUAL_NAME}.${t.name}`, inputSchema: t.inputs })),
|
|
77
|
-
{ sharedRulesPointer: '' },
|
|
78
|
-
// As the hosted endpoint serves them: opening with the guide-first
|
|
79
|
-
// sentence, through the one helper the endpoint itself uses.
|
|
80
|
-
).map((t) => ({ ...t, description: guideFirstDescription(t.description) }));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Every tool Hexis itself registers, on both surfaces, deduplicated by name. */
|
|
84
|
-
async function hexisTools(skills: Parameters<typeof registerSkillsTools>[4] = emptySkills): Promise<UtcpTool[]> {
|
|
85
|
-
const registry = new ToolRegistry();
|
|
86
|
-
const router = express.Router();
|
|
87
|
-
const toolAuth = ((_req, _res, next) => next()) as unknown as ToolAuth;
|
|
88
|
-
const toolHandler = createToolHandlerFactory(unused());
|
|
89
|
-
const kb = testKbContext();
|
|
90
|
-
const gate = { recoveryBotEmail: 'bot@x', hooks: new WorkflowHooks(), notes: new ToolDescriptionNotes() };
|
|
91
|
-
|
|
92
|
-
registerWorkspaceTools(
|
|
93
|
-
registry,
|
|
94
|
-
router,
|
|
95
|
-
toolAuth,
|
|
96
|
-
toolHandler,
|
|
97
|
-
unused(),
|
|
98
|
-
unused(),
|
|
99
|
-
unused(),
|
|
100
|
-
kb,
|
|
101
|
-
gate,
|
|
102
|
-
new RoutineWritePolicyService(),
|
|
103
|
-
new UuidSessionSink(),
|
|
104
|
-
undefined,
|
|
105
|
-
undefined,
|
|
106
|
-
// The two upload tools are mounted only when a store is supplied, and
|
|
107
|
-
// every real composition supplies one: without it they would be the two
|
|
108
|
-
// descriptions this suite never measured.
|
|
109
|
-
unused(),
|
|
110
|
-
// The guide reader, not needed to build defs; then the download store,
|
|
111
|
-
// mounted on the same terms as the upload one.
|
|
112
|
-
undefined,
|
|
113
|
-
unused(),
|
|
114
|
-
// Starter packs and the tree walk, not needed; then the grep matcher,
|
|
115
|
-
// mounted on the same terms (it spawns no worker until a search runs).
|
|
116
|
-
undefined,
|
|
117
|
-
undefined,
|
|
118
|
-
new RipgrepPatternMatcher(),
|
|
119
|
-
);
|
|
120
|
-
registerWorkflowTools(registry, router, toolAuth, toolHandler, kb, async () => true, { canRead: async () => true, canReadAtRef: async () => true, canReadBatch: async () => new Map(), canReadBatchAtRef: async () => new Map(), canWrite: async () => true } as never, { recoveryBotEmail: 'recovery@x', hooks: { runAgentRead: async () => undefined }, notes: { onChange: () => undefined } } as never);
|
|
121
|
-
registerPluginsTools(registry, kb);
|
|
122
|
-
registerSkillsTools(registry, router, toolAuth, toolHandler, skills);
|
|
123
|
-
registerToolManualsTools(registry, router, toolAuth, toolHandler, emptyManuals, {
|
|
124
|
-
accessControl: unused(),
|
|
125
|
-
variableStatus: unused(),
|
|
126
|
-
kb,
|
|
127
|
-
});
|
|
128
|
-
// The guide's own tool, with the platform's sections: the one description
|
|
129
|
-
// that lists the sections is measured with the list it really carries.
|
|
130
|
-
registerAgentGuideTool(registry, router, toolAuth, toolHandler, () => agentGuideSections(kb.layout));
|
|
131
|
-
|
|
132
|
-
const byName = new Map<string, UtcpTool>();
|
|
133
|
-
// The meta-tools as a client is served them (`mcp.service.ts` opens each
|
|
134
|
-
// with the guide-first sentence at the mount), so the catalog measured here
|
|
135
|
-
// is the catalog that goes out rather than the bare constant.
|
|
136
|
-
const external = (await registry.listExternal()) as UtcpTool[];
|
|
137
|
-
const metaTools = servedMetaTools(external);
|
|
138
|
-
for (const tool of [...(await registry.listInternal()), ...external, ...metaTools]) {
|
|
139
|
-
byName.set(tool.name, tool as UtcpTool);
|
|
140
|
-
}
|
|
141
|
-
return [...byName.values()];
|
|
142
|
-
}
|
|
143
|
-
|
|
144
37
|
describe('no Hexis tool description is long enough to be cut', () => {
|
|
145
38
|
it(`keeps every description a client is handed within ${TOOL_DESCRIPTION_CAP} characters`, async () => {
|
|
146
39
|
const over = (await hexisTools())
|