@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error codes a token endpoint may answer with, as RFC 6749 §5.2 and OIDC
|
|
3
|
+
* Core §3.1.2.6 define them. A finite list, on purpose: a code is taken from
|
|
4
|
+
* a refusing provider's body only when it is exactly one of these, so no other
|
|
5
|
+
* string a provider puts in that field (a secret, a secret with a suffix, free
|
|
6
|
+
* text) reaches a message, a verdict or a log.
|
|
7
|
+
*
|
|
8
|
+
* Shared, not a module's own: the OIDC configuration check reads a provider's
|
|
9
|
+
* refusal through it and the secrets vault a token endpoint's, and one list is
|
|
10
|
+
* the only way the two cannot disagree about which codes are standard. Each
|
|
11
|
+
* caller keeps its own sentence per code (what a tool owner should do differs
|
|
12
|
+
* from what a sign-in administrator should do); the set of codes is one.
|
|
13
|
+
*/
|
|
14
|
+
export const OAUTH_TOKEN_ERROR_CODES = Object.freeze([
|
|
15
|
+
'invalid_request',
|
|
16
|
+
'invalid_client',
|
|
17
|
+
'invalid_grant',
|
|
18
|
+
'unauthorized_client',
|
|
19
|
+
'unsupported_grant_type',
|
|
20
|
+
'invalid_scope',
|
|
21
|
+
'access_denied',
|
|
22
|
+
'server_error',
|
|
23
|
+
'temporarily_unavailable',
|
|
24
|
+
]);
|
|
25
|
+
/** Whether `code` is one of the standard token-endpoint error codes, and so safe to echo. */
|
|
26
|
+
export function isStandardTokenErrorCode(code) {
|
|
27
|
+
return typeof code === 'string' && OAUTH_TOKEN_ERROR_CODES.includes(code);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The standard `error` code in a refusing token endpoint's JSON body, or null:
|
|
31
|
+
* when the body is not JSON, not an object, carries no string `error`, or
|
|
32
|
+
* carries one that is not a standard code.
|
|
33
|
+
*/
|
|
34
|
+
export function standardTokenErrorCode(bodyText) {
|
|
35
|
+
let json;
|
|
36
|
+
try {
|
|
37
|
+
json = JSON.parse(bodyText);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (!json || typeof json !== 'object' || Array.isArray(json))
|
|
43
|
+
return null;
|
|
44
|
+
const { error } = json;
|
|
45
|
+
return isStandardTokenErrorCode(error) ? error : null;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=oauth-token-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-token-error.js","sourceRoot":"","sources":["../../src/shared/oauth-token-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAsB,MAAM,CAAC,MAAM,CAAC;IACtE,iBAAiB;IACjB,gBAAgB;IAChB,eAAe;IACf,qBAAqB;IACrB,wBAAwB;IACxB,eAAe;IACf,eAAe;IACf,cAAc;IACd,yBAAyB;CAC1B,CAAC,CAAC;AAEH,6FAA6F;AAC7F,MAAM,UAAU,wBAAwB,CAAC,IAAa;IACpD,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,uBAAuB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC5E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAAgB;IACrD,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1E,MAAM,EAAE,KAAK,EAAE,GAAG,IAA+B,CAAC;IAClD,OAAO,wBAAwB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACxD,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* At most `limit` bytes of a response body, decoded as text, the rest of the
|
|
3
|
+
* stream cancelled.
|
|
4
|
+
*
|
|
5
|
+
* Each CHUNK is sliced to the remaining budget before being decoded, not just
|
|
6
|
+
* counted against it: a provider that answers in one 5MB chunk would otherwise
|
|
7
|
+
* put the whole thing through the decoder before the loop noticed it was over,
|
|
8
|
+
* which is the bound bypassed by the very party it is meant to bound. Never a
|
|
9
|
+
* byte past the limit: once the budget is spent the read stops without asking
|
|
10
|
+
* for more, so a body of exactly `limit` bytes on a stream that stays open
|
|
11
|
+
* returns at once. And the read never falls back to `res.text()` on a live
|
|
12
|
+
* stream: a cap applied after that read bounds nothing.
|
|
13
|
+
*
|
|
14
|
+
* Shared, not a module's own: the connection probe reads a provider's
|
|
15
|
+
* rejection with it and the secrets vault a token endpoint's refusal, and one
|
|
16
|
+
* reader is the only way the two cannot disagree about what "bounded" means.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readCapped(res: Response, limit: number): Promise<string>;
|
|
19
|
+
//# sourceMappingURL=read-capped.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-capped.d.ts","sourceRoot":"","sources":["../../src/shared/read-capped.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsB9E"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* At most `limit` bytes of a response body, decoded as text, the rest of the
|
|
3
|
+
* stream cancelled.
|
|
4
|
+
*
|
|
5
|
+
* Each CHUNK is sliced to the remaining budget before being decoded, not just
|
|
6
|
+
* counted against it: a provider that answers in one 5MB chunk would otherwise
|
|
7
|
+
* put the whole thing through the decoder before the loop noticed it was over,
|
|
8
|
+
* which is the bound bypassed by the very party it is meant to bound. Never a
|
|
9
|
+
* byte past the limit: once the budget is spent the read stops without asking
|
|
10
|
+
* for more, so a body of exactly `limit` bytes on a stream that stays open
|
|
11
|
+
* returns at once. And the read never falls back to `res.text()` on a live
|
|
12
|
+
* stream: a cap applied after that read bounds nothing.
|
|
13
|
+
*
|
|
14
|
+
* Shared, not a module's own: the connection probe reads a provider's
|
|
15
|
+
* rejection with it and the secrets vault a token endpoint's refusal, and one
|
|
16
|
+
* reader is the only way the two cannot disagree about what "bounded" means.
|
|
17
|
+
*/
|
|
18
|
+
export async function readCapped(res, limit) {
|
|
19
|
+
const reader = res.body?.getReader();
|
|
20
|
+
// No stream (a mocked or already-consumed response): fall back to the whole
|
|
21
|
+
// body, which for those cases is ours and small.
|
|
22
|
+
if (!reader)
|
|
23
|
+
return (await res.text()).slice(0, limit);
|
|
24
|
+
const decoder = new TextDecoder();
|
|
25
|
+
let out = '';
|
|
26
|
+
let read = 0;
|
|
27
|
+
try {
|
|
28
|
+
while (read < limit) {
|
|
29
|
+
const { done, value } = await reader.read();
|
|
30
|
+
if (done)
|
|
31
|
+
break;
|
|
32
|
+
const remaining = limit - read;
|
|
33
|
+
const chunk = value.byteLength > remaining ? value.subarray(0, remaining) : value;
|
|
34
|
+
read += chunk.byteLength;
|
|
35
|
+
out += decoder.decode(chunk, { stream: true });
|
|
36
|
+
if (chunk.byteLength < value.byteLength)
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await reader.cancel().catch(() => { });
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=read-capped.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-capped.js","sourceRoot":"","sources":["../../src/shared/read-capped.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAa,EAAE,KAAa;IAC3D,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC;IACrC,4EAA4E;IAC5E,iDAAiD;IACjD,IAAI,CAAC,MAAM;QAAE,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IACvD,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,KAAK,EAAE,CAAC;YACpB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI;gBAAE,MAAM;YAChB,MAAM,SAAS,GAAG,KAAK,GAAG,IAAI,CAAC;YAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YAClF,IAAI,IAAI,KAAK,CAAC,UAAU,CAAC;YACzB,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/C,IAAI,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU;gBAAE,MAAM;QACjD,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bevel-software/platform-core-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Open-source core backend of the Bevel platform: git-backed knowledge workspace, workflow (branches/change requests/locks/SSE), skills, tools, secrets vault, access control and the remote MCP surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
|
56
56
|
"yaml": "^2.9.0",
|
|
57
57
|
"zod": "^3.24.0",
|
|
58
|
-
"@bevel-software/platform-mcp-core": "0.
|
|
59
|
-
"@bevel-software/platform-shared": "0.
|
|
58
|
+
"@bevel-software/platform-mcp-core": "0.35.0",
|
|
59
|
+
"@bevel-software/platform-shared": "0.35.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/adm-zip": "^0.5.8",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"@types/pg": "^8.20.0",
|
|
69
69
|
"@types/qs": "^6.15.1",
|
|
70
70
|
"drizzle-kit": "^0.31.10",
|
|
71
|
+
"js-tiktoken": "^1.0.21",
|
|
71
72
|
"tsx": "^4.19.0",
|
|
72
73
|
"typescript": "^5.8.0",
|
|
73
74
|
"vitest": "^3.2.6"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { codeModeMetaTools } from '@bevel-software/platform-mcp-core';
|
|
3
|
+
import { testKbContext } from './kb-context.js';
|
|
4
|
+
import { EXTERNAL_KB_MANUAL_NAME, registerToolManualsTools } from '../modules/tool-manuals/index.js';
|
|
5
|
+
import { ToolRegistry, guideFirstDescription, type UtcpTool } from '../modules/tool-registry/index.js';
|
|
6
|
+
import type { ToolAuth } from '../modules/tool-auth/index.js';
|
|
7
|
+
import { RipgrepPatternMatcher, ToolDescriptionNotes } from '../modules/workspace/index.js';
|
|
8
|
+
import { registerPluginsTools } from '../modules/plugins/index.js';
|
|
9
|
+
import { registerSkillsTools } from '../modules/skills/index.js';
|
|
10
|
+
import { agentGuideSections, registerAgentGuideTool } from '../modules/agent-guide/index.js';
|
|
11
|
+
import {
|
|
12
|
+
RoutineWritePolicyService,
|
|
13
|
+
UuidSessionSink,
|
|
14
|
+
WorkflowHooks,
|
|
15
|
+
createToolHandlerFactory,
|
|
16
|
+
registerChangeRequestReadTools,
|
|
17
|
+
registerEmbedTools,
|
|
18
|
+
registerWorkflowTools,
|
|
19
|
+
registerWorkspaceTools,
|
|
20
|
+
} from './tool-wiring.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The tool catalog Hexis itself registers, built on stand-ins for the tests
|
|
24
|
+
* that read what an agent is TOLD: the description caps, the guide checks and
|
|
25
|
+
* the guide's budgets. Every registrar Hexis owns is mounted, because the
|
|
26
|
+
* definitions are the only thing under test: a stand-in that is never called
|
|
27
|
+
* is honest about that. Tools PROXIED from connected MCP servers are not here:
|
|
28
|
+
* their text is the other server's.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** Dependencies the registrars take but never touch while they are only building defs. */
|
|
32
|
+
export const unused = <T,>(): T => ({}) as T;
|
|
33
|
+
|
|
34
|
+
/** A `.tool` catalog with nothing in it: the shortest honest answer for a listing. */
|
|
35
|
+
const emptyManuals = {
|
|
36
|
+
listLocalOnly: async () => [],
|
|
37
|
+
listAll: async () => [],
|
|
38
|
+
list: async () => [],
|
|
39
|
+
listInvalid: async () => [],
|
|
40
|
+
} as unknown as Parameters<typeof registerToolManualsTools>[4];
|
|
41
|
+
|
|
42
|
+
/** A skill catalog with nothing in it — the per-user skill line is then one fixed sentence. */
|
|
43
|
+
export const emptySkills = { listSkills: async () => [] } as unknown as Parameters<typeof registerSkillsTools>[4];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The meta-tools as the hosted endpoint builds them (`mcp.service.ts`):
|
|
47
|
+
* examples written against the namespace it registers the knowledge-base tools
|
|
48
|
+
* under and against the catalog it serves, and the chain without the rules it
|
|
49
|
+
* states only in the shared places. So the chain description measured here
|
|
50
|
+
* carries the worked call a client is really sent, which is its longest form.
|
|
51
|
+
*/
|
|
52
|
+
export function servedMetaTools(external: readonly UtcpTool[]) {
|
|
53
|
+
return codeModeMetaTools(
|
|
54
|
+
EXTERNAL_KB_MANUAL_NAME,
|
|
55
|
+
external.map((t) => ({ utcpName: `${EXTERNAL_KB_MANUAL_NAME}.${t.name}`, inputSchema: t.inputs })),
|
|
56
|
+
{ sharedRulesPointer: '' },
|
|
57
|
+
// As the hosted endpoint serves them: opening with the guide-first
|
|
58
|
+
// sentence, through the one helper the endpoint itself uses.
|
|
59
|
+
).map((t) => ({ ...t, description: guideFirstDescription(t.description) }));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Every tool Hexis itself registers, by surface: what the in-process agent is
|
|
64
|
+
* handed, what the hosted endpoint lists to an MCP client, and the three
|
|
65
|
+
* meta-tools it lists beside them.
|
|
66
|
+
*/
|
|
67
|
+
export async function hexisCatalog(
|
|
68
|
+
skills: Parameters<typeof registerSkillsTools>[4] = emptySkills,
|
|
69
|
+
): Promise<{ internal: UtcpTool[]; external: UtcpTool[]; metaTools: ReturnType<typeof servedMetaTools> }> {
|
|
70
|
+
const registry = new ToolRegistry();
|
|
71
|
+
const router = express.Router();
|
|
72
|
+
const toolAuth = ((_req, _res, next) => next()) as unknown as ToolAuth;
|
|
73
|
+
const toolHandler = createToolHandlerFactory(unused());
|
|
74
|
+
const kb = testKbContext();
|
|
75
|
+
const gate = { recoveryBotEmail: 'bot@x', hooks: new WorkflowHooks(), notes: new ToolDescriptionNotes() };
|
|
76
|
+
|
|
77
|
+
// The change-request readers come first, as the root mounts them: they
|
|
78
|
+
// gate their whole payload on the caller's read access and take the access
|
|
79
|
+
// service and nothing else.
|
|
80
|
+
registerChangeRequestReadTools(registry, router, toolAuth, toolHandler, unused(), kb);
|
|
81
|
+
const workspaceTools = registerWorkspaceTools(
|
|
82
|
+
registry,
|
|
83
|
+
router,
|
|
84
|
+
toolAuth,
|
|
85
|
+
toolHandler,
|
|
86
|
+
unused(),
|
|
87
|
+
unused(),
|
|
88
|
+
unused(),
|
|
89
|
+
kb,
|
|
90
|
+
gate,
|
|
91
|
+
new RoutineWritePolicyService(),
|
|
92
|
+
new UuidSessionSink(),
|
|
93
|
+
undefined,
|
|
94
|
+
undefined,
|
|
95
|
+
// The two upload tools are mounted only when a store is supplied, and
|
|
96
|
+
// every real composition supplies one: without it they would be the two
|
|
97
|
+
// descriptions this suite never measured.
|
|
98
|
+
unused(),
|
|
99
|
+
// The guide reader, not needed to build defs; then the download store,
|
|
100
|
+
// mounted on the same terms as the upload one.
|
|
101
|
+
undefined,
|
|
102
|
+
unused(),
|
|
103
|
+
// Starter packs and the tree walk, not needed; then the grep matcher,
|
|
104
|
+
// mounted on the same terms (it spawns no worker until a search runs).
|
|
105
|
+
undefined,
|
|
106
|
+
undefined,
|
|
107
|
+
new RipgrepPatternMatcher(),
|
|
108
|
+
);
|
|
109
|
+
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);
|
|
110
|
+
registerPluginsTools(registry, kb);
|
|
111
|
+
registerSkillsTools(registry, router, toolAuth, toolHandler, skills);
|
|
112
|
+
registerToolManualsTools(registry, router, toolAuth, toolHandler, emptyManuals, {
|
|
113
|
+
accessControl: unused(),
|
|
114
|
+
variableStatus: unused(),
|
|
115
|
+
kb,
|
|
116
|
+
});
|
|
117
|
+
// `open_page`, on the file tools' router with `read_file`'s own read, as the
|
|
118
|
+
// root mounts it; its stand-ins answer nothing, since only its def is read.
|
|
119
|
+
registerEmbedTools(registry, router, toolAuth, toolHandler, {
|
|
120
|
+
embedService: unused(),
|
|
121
|
+
kb,
|
|
122
|
+
readForTool: workspaceTools.readForTool,
|
|
123
|
+
canBeReached: () => true,
|
|
124
|
+
appUrlFor: () => '',
|
|
125
|
+
notes: gate.notes,
|
|
126
|
+
});
|
|
127
|
+
// The guide's own tool, with the platform's sections: the one description
|
|
128
|
+
// that lists the sections is measured with the list it really carries.
|
|
129
|
+
registerAgentGuideTool(registry, router, toolAuth, toolHandler, () => agentGuideSections(kb.layout));
|
|
130
|
+
|
|
131
|
+
// The meta-tools as a client is served them (`mcp.service.ts` opens each
|
|
132
|
+
// with the guide-first sentence at the mount), so the catalog measured here
|
|
133
|
+
// is the catalog that goes out rather than the bare constant.
|
|
134
|
+
const external = (await registry.listExternal()) as UtcpTool[];
|
|
135
|
+
return { internal: (await registry.listInternal()) as UtcpTool[], external, metaTools: servedMetaTools(external) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Every tool Hexis itself registers, on both surfaces, deduplicated by name. */
|
|
139
|
+
export async function hexisTools(skills: Parameters<typeof registerSkillsTools>[4] = emptySkills): Promise<UtcpTool[]> {
|
|
140
|
+
const { internal, external, metaTools } = await hexisCatalog(skills);
|
|
141
|
+
const byName = new Map<string, UtcpTool>();
|
|
142
|
+
for (const tool of [...internal, ...external, ...metaTools]) byName.set(tool.name, tool as UtcpTool);
|
|
143
|
+
return [...byName.values()];
|
|
144
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NodeFs } from '../modules/kb-fs/node-fs.js';
|
|
2
|
+
import { KbPluginSource } from '../modules/plugins/index.js';
|
|
3
|
+
import type { KbContext } from '../shared/kb-context.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The real disk, and the plugin source that reads it, wired the way the
|
|
7
|
+
* composition root wires them (`core/create-core-services.ts`: one `NodeFs`,
|
|
8
|
+
* one `KbPluginSource` over it). A module's suite that drives its service over
|
|
9
|
+
* real files takes them from HERE rather than constructing another module's
|
|
10
|
+
* implementation itself: the root is the one place that knows those files
|
|
11
|
+
* (`NodeFs` is on no module entry, by design — a filesystem arrives by
|
|
12
|
+
* injection), and this fixture stands in for the root, as `kb-context.ts`
|
|
13
|
+
* does for the knowledge-base context.
|
|
14
|
+
*/
|
|
15
|
+
export function realDisk(): NodeFs {
|
|
16
|
+
return new NodeFs();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function realPluginSource(disk: NodeFs, kb: KbContext): KbPluginSource {
|
|
20
|
+
return new KbPluginSource(disk, kb);
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Tiktoken } from 'js-tiktoken/lite';
|
|
2
|
+
import o200kBase from 'js-tiktoken/ranks/o200k_base';
|
|
3
|
+
import { GUIDE_TOKEN_ENCODING } from '../modules/agent-guide/index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Token counts for the tests that hold agent-facing text to a budget
|
|
7
|
+
* (`modules/agent-guide/guide-budget.ts`): a real BPE tokenizer, offline and
|
|
8
|
+
* deterministic, so a count in CI is the count on a laptop. Built once: the
|
|
9
|
+
* rank table is large and the counts never change between calls.
|
|
10
|
+
*/
|
|
11
|
+
const encoder = new Tiktoken(o200kBase);
|
|
12
|
+
|
|
13
|
+
if (GUIDE_TOKEN_ENCODING !== 'o200k_base') {
|
|
14
|
+
throw new Error(`The budgets name ${GUIDE_TOKEN_ENCODING}, but the counter is built on o200k_base.`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function countTokens(text: string): number {
|
|
18
|
+
return encoder.encode(text).length;
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ToolManualService, type McpAuthDiscoveryPort } from '../modules/tool-manuals/tool-manuals.service.js';
|
|
2
|
+
import type { IAccessControl } from '../modules/access/index.js';
|
|
3
|
+
import type { WorkspaceService } from '../modules/workspace/index.js';
|
|
4
|
+
import type { KbContext } from '../shared/kb-context.js';
|
|
5
|
+
import { realDisk, realPluginSource } from './real-disk.js';
|
|
6
|
+
|
|
7
|
+
export type { McpAuthDiscoveryPort };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The tool-manual service over real files, wired the way the composition root
|
|
11
|
+
* wires it (`core/create-core-services.ts`): the real disk, the plugin source
|
|
12
|
+
* reading that same disk, and the discovery port set afterwards. A suite that
|
|
13
|
+
* drives the service over real tool files takes it from HERE and never
|
|
14
|
+
* constructs the class from its file — the class is on no module entry (a
|
|
15
|
+
* service arrives by injection), and this fixture stands in for the root, as
|
|
16
|
+
* `kb-context.ts` and `real-disk.ts` do for what they wire.
|
|
17
|
+
*/
|
|
18
|
+
export function realToolManualService(
|
|
19
|
+
deps: {
|
|
20
|
+
workspaceService: WorkspaceService;
|
|
21
|
+
accessControl: IAccessControl;
|
|
22
|
+
kb: KbContext;
|
|
23
|
+
/** The disk the suite writes its files to; the service reads the same one. */
|
|
24
|
+
disk?: ReturnType<typeof realDisk>;
|
|
25
|
+
},
|
|
26
|
+
discovery?: McpAuthDiscoveryPort,
|
|
27
|
+
): ToolManualService {
|
|
28
|
+
const disk = deps.disk ?? realDisk();
|
|
29
|
+
const svc = new ToolManualService(deps.workspaceService, deps.accessControl, deps.kb, disk, realPluginSource(disk, deps.kb));
|
|
30
|
+
if (discovery) svc.setMcpAuthDiscovery(discovery);
|
|
31
|
+
return svc;
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pieces the composition root wires from module internals, for a suite
|
|
3
|
+
* that builds a tool surface the way `core/create-core-server.ts` does: the
|
|
4
|
+
* handler factory and context resolver, the tool-auth middleware, the two
|
|
5
|
+
* registrars that take their dependencies by hand, the session sink, the
|
|
6
|
+
* routine write policy, the access resolver and the workflow hooks.
|
|
7
|
+
*
|
|
8
|
+
* A module's entry holds its contracts, and a suite asserts through entries.
|
|
9
|
+
* These are not contracts: they are what the root composes, so a suite that
|
|
10
|
+
* needs them reaches them here, the one root fixture that imports past the
|
|
11
|
+
* entries, as the root itself does. A suite that imports one of these files
|
|
12
|
+
* directly is a second wiring that drifts from the root's.
|
|
13
|
+
*/
|
|
14
|
+
export { createToolHandlerFactory } from '../modules/tool-helpers/tool-handler.js';
|
|
15
|
+
export { createToolContextResolver } from '../modules/tool-helpers/tool-context.js';
|
|
16
|
+
export { createToolAuthMiddleware } from '../modules/tool-auth/tool-auth.middleware.js';
|
|
17
|
+
export { InternalTokenService } from '../modules/tool-auth/internal-token.service.js';
|
|
18
|
+
export { registerWorkspaceTools } from '../modules/workspace/workspace.tools.js';
|
|
19
|
+
export { registerWorkflowTools } from '../modules/workflow/agent-tools/workflow.tools.js';
|
|
20
|
+
export { registerChangeRequestReadTools } from '../modules/workflow/agent-tools/change-request-read.tools.js';
|
|
21
|
+
export { registerEmbedTools } from '../modules/embed/embed.tools.js';
|
|
22
|
+
export { UuidSessionSink } from '../modules/workspace/session-sink.js';
|
|
23
|
+
export { RoutineWritePolicyService } from '../modules/workspace/routine-write-policy.js';
|
|
24
|
+
export { AccessControlService, loadActiveGroups } from '../modules/access/access-control.service.js';
|
|
25
|
+
export { WorkflowHooks, type AgentOperationContext } from '../modules/workflow/workflow-hooks.js';
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { Server as HttpServer } from 'node:http';
|
|
2
|
+
import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
|
|
8
|
+
import { createSecretsVaultRoutes } from '../../modules/secrets-vault/index.js';
|
|
9
|
+
import { KbPluginSource } from '../../modules/plugins/index.js';
|
|
10
|
+
import type { WorkspaceService } from '../../modules/workspace/index.js';
|
|
11
|
+
import type { IAccessControl } from '../../modules/access/index.js';
|
|
12
|
+
// The two concrete services this suite wires, it wires the way the composition
|
|
13
|
+
// root does (`create-core-services.ts`): their modules publish them as types
|
|
14
|
+
// only, and the composition root is the one place that constructs them from
|
|
15
|
+
// their files. This suite stands in for that root — the routes of one module
|
|
16
|
+
// over the real parser of another — which is why it lives beside it.
|
|
17
|
+
import { ToolManualService } from '../../modules/tool-manuals/tool-manuals.service.js';
|
|
18
|
+
import { realDisk } from '../../__tests__/real-disk.js';
|
|
19
|
+
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
20
|
+
import { testKbContext } from '../../__tests__/kb-context.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The sign-in start against REAL tool files: the routes are wired to the real
|
|
24
|
+
* `ToolManualService` over a workspace on disk, so what the start answers is
|
|
25
|
+
* what the parser makes of the file, not what a stub says it makes of it.
|
|
26
|
+
*
|
|
27
|
+
* The case this exists for (Local Testing, 2026-10-27): a declaration edited to
|
|
28
|
+
* `tokenUrl: http://127.0.0.1/token` is refused by the parser before the vault
|
|
29
|
+
* ever validates it — and the start answered "Tool or variable not found"
|
|
30
|
+
* instead of the validation's message.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const KB_DIR = 'knowledge-base';
|
|
34
|
+
const wsId = workspaceIdForBranch(DEFAULT_BRANCH);
|
|
35
|
+
const USER = 'owner@x.com';
|
|
36
|
+
const AUTHORIZE = 'https://auth.crm.example/authorize';
|
|
37
|
+
const TOKEN = 'https://auth.crm.example/token';
|
|
38
|
+
|
|
39
|
+
let root: string;
|
|
40
|
+
let httpServer: HttpServer | undefined;
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
root = await mkdtemp(join(tmpdir(), 'signin-live-'));
|
|
44
|
+
await mkdir(join(root, wsId, KB_DIR, 'Plugins', 'GTM'), { recursive: true });
|
|
45
|
+
});
|
|
46
|
+
afterEach(async () => {
|
|
47
|
+
await new Promise<void>((r) => (httpServer ? httpServer.close(() => r()) : r()));
|
|
48
|
+
httpServer = undefined;
|
|
49
|
+
await rm(root, { recursive: true, force: true });
|
|
50
|
+
vi.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const toolPath = () => join(root, wsId, KB_DIR, 'Plugins', 'crm.tool');
|
|
54
|
+
const writeTool = (oauth: Record<string, unknown>) =>
|
|
55
|
+
writeFile(
|
|
56
|
+
toolPath(),
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
name: 'crm',
|
|
59
|
+
type: 'http',
|
|
60
|
+
url: 'https://api.crm.example/openapi.json',
|
|
61
|
+
variables: [{ name: 'SIGNIN', scope: 'user', oauth }],
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
const writePlugin = (oauth: Record<string, unknown>) =>
|
|
65
|
+
Promise.all([
|
|
66
|
+
writeFile(
|
|
67
|
+
join(root, wsId, KB_DIR, 'Plugins', 'GTM', 'mcp.json'),
|
|
68
|
+
JSON.stringify({ mcpServers: { hubspot: { type: 'streamable-http', url: 'https://mcp.hubspot.example/mcp' } } }),
|
|
69
|
+
),
|
|
70
|
+
writeFile(
|
|
71
|
+
join(root, wsId, KB_DIR, 'Plugins', 'GTM', 'plugin.json'),
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
name: 'gtm',
|
|
74
|
+
extensions: {
|
|
75
|
+
'software.bevel.hexis': {
|
|
76
|
+
mcpServers: {
|
|
77
|
+
hubspot: {
|
|
78
|
+
headers: { Authorization: 'Bearer ${HUBSPOT_TOKEN}' },
|
|
79
|
+
variables: [{ name: 'HUBSPOT_TOKEN', scope: 'user', oauth }],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
async function serve() {
|
|
89
|
+
const disk = realDisk();
|
|
90
|
+
const kb = testKbContext({ kbDirName: KB_DIR });
|
|
91
|
+
const accessControl = {
|
|
92
|
+
canRead: async () => true,
|
|
93
|
+
canWrite: async () => true,
|
|
94
|
+
canReadBatch: async (_w: string, _e: string, paths: string[]) => new Map(paths.map((p) => [p, true])),
|
|
95
|
+
} as unknown as IAccessControl;
|
|
96
|
+
const workspaceService = {
|
|
97
|
+
getOrCreateForBranch: async () => ({ id: wsId }),
|
|
98
|
+
getWorkspacePath: async (id: string) => join(root, id),
|
|
99
|
+
} as unknown as WorkspaceService;
|
|
100
|
+
const toolManualService = new ToolManualService(workspaceService, accessControl, kb, disk, new KbPluginSource(disk, kb));
|
|
101
|
+
toolManualService.setMcpAuthDiscovery({ statusFor: async () => ({ status: 'open' as const }) });
|
|
102
|
+
const beginToolOAuthByKey = vi.fn(async () => ({ id: 'row-1', url: `${AUTHORIZE}?client_id=x` }));
|
|
103
|
+
const deps = {
|
|
104
|
+
secretsVault: { beginToolOAuthByKey },
|
|
105
|
+
toolManualService,
|
|
106
|
+
accessControl,
|
|
107
|
+
connectionProbe: {},
|
|
108
|
+
kb: { defaultWorkspaceId: () => wsId },
|
|
109
|
+
stateSecret: 'state-secret',
|
|
110
|
+
publicBackendUrl: 'http://localhost:3000',
|
|
111
|
+
publicFrontendUrl: 'http://localhost:5173',
|
|
112
|
+
} as unknown as Parameters<typeof createSecretsVaultRoutes>[0];
|
|
113
|
+
const app = express();
|
|
114
|
+
app.use(express.json());
|
|
115
|
+
app.use((req, _res, next) => {
|
|
116
|
+
req.userId = 'user-1';
|
|
117
|
+
req.userEmail = USER;
|
|
118
|
+
next();
|
|
119
|
+
});
|
|
120
|
+
app.use('/api', createSecretsVaultRoutes(deps));
|
|
121
|
+
httpServer = await new Promise<HttpServer>((resolve) => {
|
|
122
|
+
const s = app.listen(0, () => resolve(s));
|
|
123
|
+
});
|
|
124
|
+
const base = `http://127.0.0.1:${(httpServer.address() as { port: number }).port}/api/secrets/tools`;
|
|
125
|
+
const start = (slug: string, varName: string) => fetch(`${base}/${slug}/vars/${varName}/oauth/start`, { method: 'POST' });
|
|
126
|
+
return { toolManualService, beginToolOAuthByKey, start };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
describe('POST …/oauth/start — through a real tool file', () => {
|
|
130
|
+
it('a client id edited in the .tool after the catalog was scanned is the one handed to the vault', async () => {
|
|
131
|
+
await writeTool({ clientId: 'client-A', authorizationUrl: AUTHORIZE, tokenUrl: TOKEN });
|
|
132
|
+
const s = await serve();
|
|
133
|
+
await s.toolManualService.listAccessible(USER); // the catalog caches client A
|
|
134
|
+
await writeTool({ clientId: 'client-B', authorizationUrl: AUTHORIZE, tokenUrl: TOKEN });
|
|
135
|
+
|
|
136
|
+
expect((await s.start('crm', 'SIGNIN')).status).toBe(200);
|
|
137
|
+
expect(s.beginToolOAuthByKey.mock.calls[0][0]).toMatchObject({
|
|
138
|
+
key: 'crm_SIGNIN',
|
|
139
|
+
provider: { clientId: 'client-B', authorizationUrl: AUTHORIZE, tokenUrl: TOKEN, pkce: true },
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it.each([
|
|
144
|
+
['an http:// token endpoint', 'http://127.0.0.1/token', '`variables[0].oauth.tokenUrl` must use https'],
|
|
145
|
+
['a private token endpoint', 'https://10.0.0.5/token', '`variables[0].oauth.tokenUrl` host is not allowed'],
|
|
146
|
+
])('a .tool naming %s is refused with the validation message, sending nothing', async (_n, tokenUrl, message) => {
|
|
147
|
+
await writeTool({ clientId: 'client-A', authorizationUrl: AUTHORIZE, tokenUrl: TOKEN });
|
|
148
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
149
|
+
const s = await serve();
|
|
150
|
+
await s.toolManualService.listAccessible(USER); // the catalog still holds the valid version
|
|
151
|
+
await writeTool({ clientId: 'client-A', authorizationUrl: AUTHORIZE, tokenUrl });
|
|
152
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
|
153
|
+
|
|
154
|
+
const res = await s.start('crm', 'SIGNIN');
|
|
155
|
+
expect(res.status).toBe(422);
|
|
156
|
+
expect(((await res.json()) as { error: string }).error).toBe(message);
|
|
157
|
+
expect(s.beginToolOAuthByKey).not.toHaveBeenCalled();
|
|
158
|
+
// The one fetch is the test's own call to the route: nothing went to a provider.
|
|
159
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('a plugin.json sign-in naming an http:// token endpoint is refused with the validation message, sending nothing', async () => {
|
|
163
|
+
await writePlugin({ clientId: 'owner-app', authorizationUrl: AUTHORIZE, tokenUrl: 'http://127.0.0.1/token' });
|
|
164
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
165
|
+
const s = await serve();
|
|
166
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
|
167
|
+
|
|
168
|
+
const res = await s.start('hubspot', 'HUBSPOT_TOKEN');
|
|
169
|
+
expect(res.status).toBe(422);
|
|
170
|
+
expect(((await res.json()) as { error: string }).error).toContain('`variables[0].oauth.tokenUrl` must use https');
|
|
171
|
+
expect(s.beginToolOAuthByKey).not.toHaveBeenCalled();
|
|
172
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('a slug no file declares is still not found', async () => {
|
|
176
|
+
const s = await serve();
|
|
177
|
+
expect((await s.start('nothing', 'SIGNIN')).status).toBe(404);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -487,7 +487,7 @@ export async function createCoreServer(
|
|
|
487
487
|
// because they are the only ones that gate their whole payload on the
|
|
488
488
|
// caller's read access, so they take the access service and nothing else.
|
|
489
489
|
registerChangeRequestReadTools(core.toolRegistry, toolsRouter, ta, th, core.accessControl, core.kb);
|
|
490
|
-
const workspaceTools = registerWorkspaceTools(core.toolRegistry, toolsRouter, ta, th, core.spillStore, core.docExtractService, core.accessControl, core.kb, agentAccessGate, core.routineWritePolicy, core.sessionSink, allowedToolsChecker, core.changeGate, core.agentUploadStore, core.
|
|
490
|
+
const workspaceTools = registerWorkspaceTools(core.toolRegistry, toolsRouter, ta, th, core.spillStore, core.docExtractService, core.accessControl, core.kb, agentAccessGate, core.routineWritePolicy, core.sessionSink, allowedToolsChecker, core.changeGate, core.agentUploadStore, core.agentGuideStart, core.agentDownloadStore, core.starterPackService, core.disk, core.patternMatcher);
|
|
491
491
|
// `open_page` — the knowledge-base page shown inside a chat. Registered
|
|
492
492
|
// here, on the same router as the file tools, because it answers with
|
|
493
493
|
// `read_file`'s own read: the read hook, the access gate and the
|
|
@@ -171,6 +171,7 @@ import { AgentAuditService, retentionDaysFrom } from '../modules/audit/agent-aud
|
|
|
171
171
|
import { readAgentPreamble, type AgentPreambleReader } from '../modules/agent-instructions/index.js';
|
|
172
172
|
import {
|
|
173
173
|
agentGuideSections,
|
|
174
|
+
guideStartText,
|
|
174
175
|
joinGuideSections,
|
|
175
176
|
type AgentGuideReader,
|
|
176
177
|
type AgentGuideSectionsReader,
|
|
@@ -296,6 +297,12 @@ export interface CoreServices {
|
|
|
296
297
|
* `read_file` of the guide's name answer. See modules/agent-guide.
|
|
297
298
|
*/
|
|
298
299
|
agentGuide: AgentGuideReader;
|
|
300
|
+
/**
|
|
301
|
+
* The start of that guide and the list of its sections: what a `read_file`
|
|
302
|
+
* of the guide's name answers, the same as `get_agent_guide` with no
|
|
303
|
+
* `section`. See `guideStartText`.
|
|
304
|
+
*/
|
|
305
|
+
agentGuideStart: AgentGuideReader;
|
|
299
306
|
/** The same guide as its sections, each with its title — what `get_agent_guide` lists and serves one of. */
|
|
300
307
|
agentGuideSections: AgentGuideSectionsReader;
|
|
301
308
|
mcpServerEditService: McpServerEditService;
|
|
@@ -1376,11 +1383,13 @@ export async function createCoreServices(
|
|
|
1376
1383
|
// serves the same composition to the local bridge and the frontend card.
|
|
1377
1384
|
const readPreamble: AgentPreambleReader = () => readAgentPreamble(workspaceService, kb, disk);
|
|
1378
1385
|
// The guide every agent is told to read first, composed when asked for —
|
|
1379
|
-
// the layout
|
|
1380
|
-
// without a restart, and the distribution's hook sees
|
|
1386
|
+
// the layout and the default branch are read per call, so a name the setup
|
|
1387
|
+
// save applies lands without a restart, and the distribution's hook sees
|
|
1388
|
+
// every composition.
|
|
1381
1389
|
const agentGuideSectionsReader: AgentGuideSectionsReader = () =>
|
|
1382
|
-
agentGuideSections(kb.layout, ports.agentGuide, { kbDirName });
|
|
1390
|
+
agentGuideSections(kb.layout, ports.agentGuide, { kbDirName, defaultBranch: kb.defaultBranch });
|
|
1383
1391
|
const agentGuide: AgentGuideReader = async () => joinGuideSections(await agentGuideSectionsReader());
|
|
1392
|
+
const agentGuideStart: AgentGuideReader = async () => guideStartText(await agentGuideSectionsReader());
|
|
1384
1393
|
// The Audit log. Records through the proxy below (every call an external
|
|
1385
1394
|
// agent makes), reads keys through the key service so their shape is
|
|
1386
1395
|
// defined once, and prunes past the retention setting — read per sweep, so
|
|
@@ -1723,6 +1732,7 @@ export async function createCoreServices(
|
|
|
1723
1732
|
pendingToolsService,
|
|
1724
1733
|
readAgentPreamble: readPreamble,
|
|
1725
1734
|
agentGuide,
|
|
1735
|
+
agentGuideStart,
|
|
1726
1736
|
agentGuideSections: agentGuideSectionsReader,
|
|
1727
1737
|
pluginIndexService,
|
|
1728
1738
|
pluginProvisionService,
|