@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
|
@@ -249,6 +249,14 @@ const int = (description: string): JsonSchema => ({ type: 'integer', description
|
|
|
249
249
|
|
|
250
250
|
const str = (description: string): JsonSchema => ({ type: 'string', description });
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* A write to the default branch is live at once, so the agent asks first. Said on the four
|
|
254
|
+
* tools behind 87 of the 91 writes there in the agent-guide benchmark (2026-10-26); `start-here`
|
|
255
|
+
* says it for the other write tools.
|
|
256
|
+
*/
|
|
257
|
+
const ASK_BEFORE_DEFAULT_BRANCH =
|
|
258
|
+
' On the default branch a write is live for everyone at once: unless the person already said where, ask them first whether to write there or propose it on a draft.';
|
|
259
|
+
|
|
252
260
|
/**
|
|
253
261
|
* The upload route, named on every tool that takes content as a JSON string.
|
|
254
262
|
*
|
|
@@ -1113,8 +1121,9 @@ export function registerWorkspaceTools(
|
|
|
1113
1121
|
*/
|
|
1114
1122
|
uploads?: AgentUploadStore,
|
|
1115
1123
|
/**
|
|
1116
|
-
* The platform's agent guide
|
|
1117
|
-
*
|
|
1124
|
+
* The start of the platform's agent guide and the list of its sections
|
|
1125
|
+
* (`guideStartText` in `modules/agent-guide`), which `read_file` and
|
|
1126
|
+
* `file_stat` answer at the guide's name in the repository root — after
|
|
1118
1127
|
* the knowledge base's own file of that name, when it has one. Optional for
|
|
1119
1128
|
* the harnesses that are about the file primitives; without it the two
|
|
1120
1129
|
* tools read the disk and nothing else.
|
|
@@ -2010,13 +2019,13 @@ export function registerWorkspaceTools(
|
|
|
2010
2019
|
const startSessionDef = toolDef({
|
|
2011
2020
|
name: 'start_session',
|
|
2012
2021
|
description:
|
|
2013
|
-
'Mint this conversation\'s id: the `sessionId` KnowledgeBase tools take. Call this ONCE, at the start of your work — minting a new id mid-run starts a second conversation as far as the server is concerned.
|
|
2022
|
+
'Mint this conversation\'s id: the `sessionId` KnowledgeBase tools take. Call this ONCE, at the start of your work — minting a new id mid-run starts a second conversation as far as the server is concerned. Pass the returned id as `sessionId` to every tool whose arguments list it, inside `call_tool_chain` too; the others refuse it. Where the deployment offers an `ask` tool, hand it the SAME id: your reads and your questions are then one conversation. RETRYING IS SAFE: a call that fails created nothing, so retry it. If a retry lands after a success you hold two independent ids, which is harmless: keep passing the one you already used and ignore the other. Returns `{ sessionId }`, plus `firstRun` while the knowledge base is still empty: a note to act on.',
|
|
2014
2023
|
path: '/api/agent/tools/start_session',
|
|
2015
2024
|
inputs: { type: 'object', properties: {}, additionalProperties: false },
|
|
2016
2025
|
outputs: {
|
|
2017
2026
|
type: 'object',
|
|
2018
2027
|
properties: {
|
|
2019
|
-
sessionId: str('The minted session id — pass it as `sessionId`
|
|
2028
|
+
sessionId: str('The minted session id — pass it as `sessionId` to the tools that take one, and to `ask` where there is one.'),
|
|
2020
2029
|
firstRun: str(
|
|
2021
2030
|
`Present only while the knowledge base holds nothing but its starter guide (and a starter pack's untouched pages): what to offer the person (the guide's \`${FIRST_RUN_SECTION_ID}\` section says how).`,
|
|
2022
2031
|
),
|
|
@@ -2234,7 +2243,7 @@ export function registerWorkspaceTools(
|
|
|
2234
2243
|
'Read a workspace file as text. Returns `{ path, content, canWrite }`. Per the shared content rule: text as ' +
|
|
2235
2244
|
'text, documents and email as extracted text, an image as the picture, anything else as a one-line ' +
|
|
2236
2245
|
'description. Also reads a `__tool_chain_spill__/…` ref back from a truncated `call_tool_chain`: it ' +
|
|
2237
|
-
'belongs to no workspace
|
|
2246
|
+
'belongs to no workspace: any existing `branch` will do, and no `canWrite` comes back. `offset`/`limit` slice the ' +
|
|
2238
2247
|
'content (characters; bytes for a spill ref; ignored for an image): page through large files and ' +
|
|
2239
2248
|
'spills, never read multi-MB whole. `canWrite`: may you change this file directly on the default branch. ' +
|
|
2240
2249
|
CAN_WRITE_CLAUSE,
|
|
@@ -2243,8 +2252,8 @@ export function registerWorkspaceTools(
|
|
|
2243
2252
|
properties: {
|
|
2244
2253
|
branch: BRANCH_INPUT,
|
|
2245
2254
|
path: str(`Path to read, under \`${kbDirName}/\` (e.g. \`${kbDirName}/KnowledgeBase/Foo.md\`), with or without a leading slash — a path without that prefix is placed under \`${kbDirName}/\` — or a \`__tool_chain_spill__/…\` ref from a truncated \`call_tool_chain\`, or a \`__tmp__/…\` ref to a file you staged in the workspace's tmp folder.`),
|
|
2246
|
-
offset: int('Start character index (default 0).'),
|
|
2247
|
-
limit: int('Max characters to return from `offset
|
|
2255
|
+
offset: int('Start character index (bytes for a spill ref; default 0).'),
|
|
2256
|
+
limit: int('Max characters to return from `offset` (bytes for a spill ref).'),
|
|
2248
2257
|
sessionId: SESSION_ID_INPUT,
|
|
2249
2258
|
},
|
|
2250
2259
|
required: ['branch', 'path'],
|
|
@@ -2919,6 +2928,7 @@ export function registerWorkspaceTools(
|
|
|
2919
2928
|
description:
|
|
2920
2929
|
'Write a workspace TEXT file. The change is committed + pushed as you. Returns `{ path, bytes, outcome }`, where `outcome` is ' +
|
|
2921
2930
|
'`created`, `replaced` or `updated`.' +
|
|
2931
|
+
ASK_BEFORE_DEFAULT_BRANCH +
|
|
2922
2932
|
UPLOAD_ROUTE_NOTE,
|
|
2923
2933
|
inputs: {
|
|
2924
2934
|
type: 'object',
|
|
@@ -3009,6 +3019,7 @@ export function registerWorkspaceTools(
|
|
|
3009
3019
|
'`created` / `replaced` / `updated` for a path it wrote, or `refused` with `error` (the code) and `message` (why) for a ' +
|
|
3010
3020
|
'path it could not. `count` is how many were written. A path it refuses — the mode said no, or the file is not text — ' +
|
|
3011
3021
|
'does not stop the others; read `files` to see what landed.' +
|
|
3022
|
+
ASK_BEFORE_DEFAULT_BRANCH +
|
|
3012
3023
|
UPLOAD_ROUTE_NOTE,
|
|
3013
3024
|
inputs: {
|
|
3014
3025
|
type: 'object',
|
|
@@ -3046,7 +3057,7 @@ export function registerWorkspaceTools(
|
|
|
3046
3057
|
enum: ['created', 'replaced', 'updated', 'refused'],
|
|
3047
3058
|
description: 'What happened at this path. `refused` means nothing was written there and the file is untouched.',
|
|
3048
3059
|
},
|
|
3049
|
-
error: str('Present when `outcome` is `refused`: the refusal code
|
|
3060
|
+
error: str('Present when `outcome` is `refused`: the refusal code, such as `exists`, `missing`, `binary_not_writable` or a code of the deployment\'s own write check.'),
|
|
3050
3061
|
message: str('Present when `outcome` is `refused`: the full refusal, the same one write_file would have given.'),
|
|
3051
3062
|
},
|
|
3052
3063
|
required: ['path', 'outcome'],
|
|
@@ -3181,6 +3192,7 @@ export function registerWorkspaceTools(
|
|
|
3181
3192
|
gated: true,
|
|
3182
3193
|
description:
|
|
3183
3194
|
'Replace an exact string in a workspace TEXT file. `old_string` must appear exactly once unless `replace_all`. Committed + pushed as you.' +
|
|
3195
|
+
ASK_BEFORE_DEFAULT_BRANCH +
|
|
3184
3196
|
UPLOAD_ROUTE_NOTE,
|
|
3185
3197
|
inputs: {
|
|
3186
3198
|
type: 'object',
|
|
@@ -3260,7 +3272,8 @@ export function registerWorkspaceTools(
|
|
|
3260
3272
|
name: 'delete_file',
|
|
3261
3273
|
gated: true,
|
|
3262
3274
|
description:
|
|
3263
|
-
'Delete ONE workspace file. Committed + pushed as you. Its folder stays, even when this was its last file. Files only: a folder is refused with a pointer to `delete_folder`.'
|
|
3275
|
+
'Delete ONE workspace file. Committed + pushed as you. Its folder stays, even when this was its last file. Files only: a folder is refused with a pointer to `delete_folder`.' +
|
|
3276
|
+
ASK_BEFORE_DEFAULT_BRANCH,
|
|
3264
3277
|
inputs: {
|
|
3265
3278
|
type: 'object',
|
|
3266
3279
|
properties: {
|
|
@@ -3484,10 +3497,11 @@ export function registerWorkspaceTools(
|
|
|
3484
3497
|
description:
|
|
3485
3498
|
'Move or rename a workspace FILE or FOLDER; a folder moves recursively, with everything under it. `dest` is the full new path, not the folder to move into. Committed + pushed as you. ' +
|
|
3486
3499
|
'The destination must not exist — a move never overwrites a file or merges into a folder. Access follows the ' +
|
|
3487
|
-
'DESTINATION folder, so a move can change
|
|
3488
|
-
'
|
|
3489
|
-
'
|
|
3490
|
-
'
|
|
3500
|
+
'DESTINATION folder, so a move can change who may open or edit the file. You must be able to read what moves and the folder it lands in, ' +
|
|
3501
|
+
'or it is refused as read_file refuses. The dry run answers ' +
|
|
3502
|
+
'`{ src, dest, kind, descendants, access: { before, after }, accessChanges, holderChanges?, allowed, reason? }`: your own ' +
|
|
3503
|
+
'access at both ends, and who else loses or gains it. A move that changes either wants `confirm: true`; tell the person ' +
|
|
3504
|
+
'who loses and who gains before you confirm. ' +
|
|
3491
3505
|
'Links are rewritten by default, in one commit with the move: those in the moved files and those in other markdown files pointing at them (`links` reports them). `rewriteLinks: false` turns that off. Path mentions in plain prose or code are never changed.',
|
|
3492
3506
|
inputs: {
|
|
3493
3507
|
type: 'object',
|
|
@@ -3496,7 +3510,7 @@ export function registerWorkspaceTools(
|
|
|
3496
3510
|
src: wsPath(kbDirName, 'Source path (file or folder)'),
|
|
3497
3511
|
dest: wsPath(kbDirName, 'Destination path — the full new path; must not exist yet'),
|
|
3498
3512
|
dryRun: { type: 'boolean', description: 'Answer with the impact and change nothing.' },
|
|
3499
|
-
confirm: { type: 'boolean', description: 'Required when the move changes your access. Set it only after a dry run.' },
|
|
3513
|
+
confirm: { type: 'boolean', description: 'Required when the move changes your access or anyone else\'s. Set it only after a dry run.' },
|
|
3500
3514
|
rewriteLinks: { type: 'boolean', description: 'Rewrite the links into, out of and between the moved files (default true). `false` moves without touching any link.' },
|
|
3501
3515
|
sessionId: SESSION_ID_INPUT,
|
|
3502
3516
|
},
|
|
@@ -3510,8 +3524,14 @@ export function registerWorkspaceTools(
|
|
|
3510
3524
|
dest: str('Destination path (echoes the input).'),
|
|
3511
3525
|
kind: str('`file` or `folder`.'),
|
|
3512
3526
|
descendants: int('Files that move: 1 for a file, the file count under a folder.'),
|
|
3513
|
-
access: { type: 'object', description: 'Your `{ read, write, download, owner }` at the source (`before`) and at the destination once the move has landed (`after`).' },
|
|
3527
|
+
access: { type: 'object', description: 'Your `{ read, write, download, owner }` at the source (`before`) and at the destination once the move has landed (`after`), with every `access.md` inside a moved folder counted at its new place.' },
|
|
3514
3528
|
accessChanges: { type: 'boolean', description: 'True when any of your verdicts differs between `before` and `after`.' },
|
|
3529
|
+
holderChanges: {
|
|
3530
|
+
type: 'object',
|
|
3531
|
+
description:
|
|
3532
|
+
'Who loses and who gains opening (`read`) or editing (`write`) it once the move has landed: `{ lose, gain }`, each ' +
|
|
3533
|
+
'`[{ kind, name, email?, verb }]` with `kind` `group`, `role`, `plugin` or `person`. Absent when nobody\'s access changes.',
|
|
3534
|
+
},
|
|
3515
3535
|
allowed: { type: 'boolean', description: 'Whether the move may run.' },
|
|
3516
3536
|
reason: str('Why it may not, when `allowed` is false.'),
|
|
3517
3537
|
dryRun: { type: 'boolean', description: 'True on a dry run.' },
|
|
@@ -3541,6 +3561,23 @@ export function registerWorkspaceTools(
|
|
|
3541
3561
|
// source already deleted.
|
|
3542
3562
|
await assertAgentWriteAllowed(agentAccessGate, ctx, branch, src);
|
|
3543
3563
|
await assertAgentWriteAllowed(agentAccessGate, ctx, branch, dest);
|
|
3564
|
+
// What moves and the folder it lands in must both be readable to the
|
|
3565
|
+
// caller, refused as read_file refuses them, on every branch: a move
|
|
3566
|
+
// preview reads both ends (the caller's own verdicts there, who else is
|
|
3567
|
+
// affected), and the folder's rules are what the names are made of. The
|
|
3568
|
+
// verdict comes BEFORE every other look, so neither end's existence is
|
|
3569
|
+
// told to someone who may not know it. The deployment's read hook is
|
|
3570
|
+
// asked first, as read_file asks it, and its refusal is the answer: a
|
|
3571
|
+
// move the hook would not let read is not previewed at all, since even
|
|
3572
|
+
// "this move asks for confirm" would tell what the comparison found. A
|
|
3573
|
+
// folder the caller may read moves with everything in it, files they
|
|
3574
|
+
// may not read included: those travel, they are not read.
|
|
3575
|
+
const destFolder = dest.slice(0, dest.lastIndexOf('/'));
|
|
3576
|
+
await notifyAgentRead(agentAccessGate, ctx, branch, src);
|
|
3577
|
+
await notifyAgentRead(agentAccessGate, ctx, branch, destFolder);
|
|
3578
|
+
const gate = readGateFor(branch, ctx);
|
|
3579
|
+
await assertCanRead(gate, src);
|
|
3580
|
+
await assertCanRead(gate, destFolder);
|
|
3544
3581
|
const fs = await ctx.getFilesystem(branch);
|
|
3545
3582
|
const root = await workspaceRoot(branch, ctx);
|
|
3546
3583
|
// Before the kind check, which stats THROUGH a link: a dangling link at
|
|
@@ -3564,6 +3601,17 @@ export function registerWorkspaceTools(
|
|
|
3564
3601
|
accessAfter(branch, ctx, src, dest),
|
|
3565
3602
|
]);
|
|
3566
3603
|
const accessChanges = verbsDiffer(before, after);
|
|
3604
|
+
// Who ELSE gains or loses opening or editing it: the lists the app's move
|
|
3605
|
+
// confirmation shows people. The caller may read both ends (the hook and
|
|
3606
|
+
// the rules, gated above, as the app's route gates), so the names go to
|
|
3607
|
+
// them, and a change asks for `confirm`. Outside the repository no rules
|
|
3608
|
+
// travel.
|
|
3609
|
+
const fromRel = toKbRelative(src, kbDirName);
|
|
3610
|
+
const toRel = toKbRelative(dest, kbDirName);
|
|
3611
|
+
const changes = fromRel !== null && toRel !== null
|
|
3612
|
+
? await accessControl.prospectiveHolderChanges(workspaceIdForBranch(branch), fromRel, toRel)
|
|
3613
|
+
: undefined;
|
|
3614
|
+
const othersChange = changes !== undefined && (changes.lose.length > 0 || changes.gain.length > 0);
|
|
3567
3615
|
// The placeholder moves with its folder, but it is never content.
|
|
3568
3616
|
const descendants = srcFiles.filter((f) => !isFolderPlaceholder(f)).length;
|
|
3569
3617
|
// Neither end may be the platform's own: a move neither takes a platform
|
|
@@ -3671,6 +3719,7 @@ export function registerWorkspaceTools(
|
|
|
3671
3719
|
descendants,
|
|
3672
3720
|
access: { before, after },
|
|
3673
3721
|
accessChanges,
|
|
3722
|
+
...(othersChange ? { holderChanges: changes } : {}),
|
|
3674
3723
|
allowed: moveReason === undefined,
|
|
3675
3724
|
...(moveReason !== undefined ? { reason: moveReason } : {}),
|
|
3676
3725
|
...(linkPlan ? { links: linkPlan.report } : {}),
|
|
@@ -3686,12 +3735,16 @@ export function registerWorkspaceTools(
|
|
|
3686
3735
|
}
|
|
3687
3736
|
if (collision) throw new ToolError(reason!, 409);
|
|
3688
3737
|
if (linkPlan?.overCap !== undefined) throw new ToolError(linkPlan.overCap, 400);
|
|
3689
|
-
if (accessChanges && a.confirm !== true) {
|
|
3738
|
+
if ((accessChanges || othersChange) && a.confirm !== true) {
|
|
3739
|
+
const why = [
|
|
3740
|
+
...(accessChanges ? [`your access at "${dest}" differs from "${src}"`] : []),
|
|
3741
|
+
...(othersChange ? ['it changes who else may open or edit it (`holderChanges`)'] : []),
|
|
3742
|
+
];
|
|
3690
3743
|
return {
|
|
3691
3744
|
...impact,
|
|
3692
3745
|
confirmationRequired: true,
|
|
3693
3746
|
moved: false,
|
|
3694
|
-
message: `Nothing was moved:
|
|
3747
|
+
message: `Nothing was moved: ${why.join(', and ')}, so this move requires confirm: true.`,
|
|
3695
3748
|
};
|
|
3696
3749
|
}
|
|
3697
3750
|
// The look above is what produces the sentence; the filesystem's own
|
|
@@ -3741,8 +3794,8 @@ export function registerWorkspaceTools(
|
|
|
3741
3794
|
gated: true,
|
|
3742
3795
|
description:
|
|
3743
3796
|
'Copy a workspace FILE to a new path. The destination must not exist — like a move, a copy never overwrites a file or a folder; to change what is in a file that already exists, write it. Committed + pushed as you. `src` may be a `__tmp__/<path>` ref to a file you staged in the workspace\'s tmp folder: the copy is how such a file is kept, as a normal repository file. ' +
|
|
3744
|
-
'Preflight first: `dryRun: true` changes nothing and answers `{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }` — `access` is your own `{ read, write, download, owner }` at the source and at the destination AS IT WILL BE once the copy has landed, with
|
|
3745
|
-
'One exception to that shape: when the destination is one you may not write, the answer is the refusal
|
|
3797
|
+
'Preflight first: `dryRun: true` changes nothing and answers `{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }` — `access` is your own `{ read, write, download, owner }` at the source and at the destination AS IT WILL BE once the copy has landed, with the copy\'s own rules counted at its new place. ' +
|
|
3798
|
+
'One exception to that shape: when the destination is one you may not write, the answer is the refusal (`allowed: false` with `reason`, and the access figures) with no `kind` and no `descendants`, because nothing about the source is read before that verdict.',
|
|
3746
3799
|
inputs: {
|
|
3747
3800
|
type: 'object',
|
|
3748
3801
|
properties: {
|
|
@@ -4203,7 +4256,7 @@ export function registerWorkspaceTools(
|
|
|
4203
4256
|
'`replaced` / `updated`, or `refused` with `error` (the code) and `message` (why). `count` is how many landed and ' +
|
|
4204
4257
|
'`total` how many paths there were; `files` is cut to the first 25 unless you pass `all: true`. ' +
|
|
4205
4258
|
'Every path is judged one by one — by your write access, the platform-file rules and what is already there — ' +
|
|
4206
|
-
'
|
|
4259
|
+
'as `write_file` judges it, except that platform files are refused and binary content is fine; a refused path does not stop the others. ' +
|
|
4207
4260
|
'The token is single-use: it is spent by the apply that lands it, and refused if you use it twice, let it ' +
|
|
4208
4261
|
'expire, or present one issued to somebody else. `mode` means what it means on `write_file`.',
|
|
4209
4262
|
inputs: {
|
|
@@ -4287,7 +4340,7 @@ export function registerWorkspaceTools(
|
|
|
4287
4340
|
'each one inside a folder too, is included only if you may read AND download that file. Returns ' +
|
|
4288
4341
|
'`{ expiresAt, expiresInSeconds, files: [{ path, bytes, sha256, downloadUrl }], folders: [{ path, bytes, ' +
|
|
4289
4342
|
'downloadUrl, files }], refused: [{ path, reason }] }`: a link per file, with its own content type, and per ' +
|
|
4290
|
-
`folder a zip at full repository paths (\`apply_file_upload\` it at \`${kbDirName}/\` to put
|
|
4343
|
+
`folder a zip at full repository paths (\`apply_file_upload\` it at \`${kbDirName}/\` to put them back). ` +
|
|
4291
4344
|
'Fetch with any HTTP client (`curl -o <name> "<downloadUrl>"`, or the address without its last segment and an ' +
|
|
4292
4345
|
'`x-download-token` header). Each link works ONCE, for 15 minutes, and serves the files as they are now. ' +
|
|
4293
4346
|
'Refused: `not found` (missing or named-but-unreadable; folders omit unreadable files), ' +
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { readCapped } from '../read-capped.js';
|
|
3
|
+
|
|
4
|
+
describe('readCapped', () => {
|
|
5
|
+
/**
|
|
6
|
+
* Counting a chunk against the budget after decoding it lets a provider that
|
|
7
|
+
* answers in one huge chunk put the whole thing through the decoder — the
|
|
8
|
+
* bound bypassed by exactly the party it exists to bound. Asserted on the
|
|
9
|
+
* reader directly because a caller's short snippet is identical either way.
|
|
10
|
+
*/
|
|
11
|
+
it('holds one oversized chunk to the byte budget, not just the loop', async () => {
|
|
12
|
+
const oneHugeChunk = new ReadableStream<Uint8Array>({
|
|
13
|
+
start(controller) {
|
|
14
|
+
controller.enqueue(new TextEncoder().encode('x'.repeat(5_000_000)));
|
|
15
|
+
controller.close();
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const out = await readCapped(new Response(oneHugeChunk), 8 * 1024);
|
|
20
|
+
|
|
21
|
+
expect(out.length).toBe(8 * 1024);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('stops pulling at the budget and cancels a stream that never ends', async () => {
|
|
25
|
+
// Without the cap this never returns: the stream has no end.
|
|
26
|
+
let pulls = 0;
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
const endless = new ReadableStream<Uint8Array>({
|
|
29
|
+
pull(controller) {
|
|
30
|
+
pulls += 1;
|
|
31
|
+
controller.enqueue(new TextEncoder().encode('z'.repeat(1024)));
|
|
32
|
+
},
|
|
33
|
+
cancel() {
|
|
34
|
+
cancelled = true;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const out = await readCapped(new Response(endless), 2048);
|
|
39
|
+
|
|
40
|
+
expect(out).toBe('z'.repeat(2048));
|
|
41
|
+
expect(pulls).toBe(2);
|
|
42
|
+
expect(cancelled).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns at once on a body of exactly the budget whose stream stays open', async () => {
|
|
46
|
+
// Asking for one more byte here would wait on a chunk that never comes.
|
|
47
|
+
let pulls = 0;
|
|
48
|
+
const openAfterFill = new ReadableStream<Uint8Array>({
|
|
49
|
+
pull(controller) {
|
|
50
|
+
pulls += 1;
|
|
51
|
+
if (pulls === 1) {
|
|
52
|
+
controller.enqueue(new TextEncoder().encode('a'.repeat(8)));
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
return new Promise<void>(() => {});
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
expect(await readCapped(new Response(openAfterFill), 8)).toBe('a'.repeat(8));
|
|
59
|
+
expect(pulls).toBe(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('reads a stream-less response whole, cut to the budget', async () => {
|
|
63
|
+
const res = new Response('abcdef');
|
|
64
|
+
// Consume the stream so only `text()` is left, as a mocked response may be.
|
|
65
|
+
Object.defineProperty(res, 'body', { value: null });
|
|
66
|
+
expect(await readCapped(res, 4)).toBe('abcd');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
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: readonly string[] = 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
|
+
|
|
26
|
+
/** Whether `code` is one of the standard token-endpoint error codes, and so safe to echo. */
|
|
27
|
+
export function isStandardTokenErrorCode(code: unknown): code is string {
|
|
28
|
+
return typeof code === 'string' && OAUTH_TOKEN_ERROR_CODES.includes(code);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The standard `error` code in a refusing token endpoint's JSON body, or null:
|
|
33
|
+
* when the body is not JSON, not an object, carries no string `error`, or
|
|
34
|
+
* carries one that is not a standard code.
|
|
35
|
+
*/
|
|
36
|
+
export function standardTokenErrorCode(bodyText: string): string | null {
|
|
37
|
+
let json: unknown;
|
|
38
|
+
try {
|
|
39
|
+
json = JSON.parse(bodyText);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (!json || typeof json !== 'object' || Array.isArray(json)) return null;
|
|
44
|
+
const { error } = json as Record<string, unknown>;
|
|
45
|
+
return isStandardTokenErrorCode(error) ? error : null;
|
|
46
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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: Response, limit: number): Promise<string> {
|
|
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) return (await res.text()).slice(0, limit);
|
|
23
|
+
const decoder = new TextDecoder();
|
|
24
|
+
let out = '';
|
|
25
|
+
let read = 0;
|
|
26
|
+
try {
|
|
27
|
+
while (read < limit) {
|
|
28
|
+
const { done, value } = await reader.read();
|
|
29
|
+
if (done) break;
|
|
30
|
+
const remaining = limit - read;
|
|
31
|
+
const chunk = value.byteLength > remaining ? value.subarray(0, remaining) : value;
|
|
32
|
+
read += chunk.byteLength;
|
|
33
|
+
out += decoder.decode(chunk, { stream: true });
|
|
34
|
+
if (chunk.byteLength < value.byteLength) break;
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
await reader.cancel().catch(() => {});
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
## Finding things
|
|
2
|
-
|
|
3
|
-
- `grep` for keywords across `{{knowledgeBaseDir}}/`.
|
|
4
|
-
- Follow markdown links: when you read `[Some Page](relative/path/Some Page.md)`,
|
|
5
|
-
that path is relative to the file you are reading.
|
|
6
|
-
- `list_files` to see the shape of a folder before assuming where something
|
|
7
|
-
lives.
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Knowledge base
|
|
2
|
-
|
|
3
|
-
This is a git-backed knowledge base. You are the primary agent responsible for
|
|
4
|
-
maintaining it.
|
|
5
|
-
|
|
6
|
-
> **This guide is served by the platform.** It is not a file in the
|
|
7
|
-
> repository: `get_agent_guide` returns it, whole or one section at a time,
|
|
8
|
-
> and so does `read_file` on `AGENTS.md` at the repository root — after the
|
|
9
|
-
> knowledge base's own `AGENTS.md`, when it has one; `grep` searches it there
|
|
10
|
-
> too. An `AGENTS.md` you find on disk is the organisation's own conventions
|
|
11
|
-
> file, written by its people; follow it, and never write this text into it.
|
|
12
|
-
> Deployment- or team-specific conventions belong there, or in files of your
|
|
13
|
-
> own anywhere under `{{knowledgeBaseDir}}/`, linked from wherever they are
|
|
14
|
-
> needed.
|
|
15
|
-
|
|
16
|
-
**Read `mcp-description.md` at the repository root first.** It says what this
|
|
17
|
-
knowledge base contains and when to consult it. Agents connected over MCP
|
|
18
|
-
receive the default branch's copy inline at the start of every session; a
|
|
19
|
-
clone reads the copy on its own branch.
|
|
20
|
-
|
|
21
|
-
**There is no required format for knowledge.** Write markdown the way the
|
|
22
|
-
subject wants to be written: prose, tables, checklists, diagrams, whatever
|
|
23
|
-
serves the reader. Nothing here parses your files into a schema or rejects a
|
|
24
|
-
document for having the wrong shape. If a deployment layers a structured
|
|
25
|
-
knowledge graph on top, it brings its own conventions, in sections of this
|
|
26
|
-
guide of its own; what follows describes the platform underneath, which stores
|
|
27
|
-
files and controls who may change them.
|