@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
|
@@ -279,9 +279,10 @@ export function registerWorkflowTools(
|
|
|
279
279
|
mount({
|
|
280
280
|
name: 'commit_change',
|
|
281
281
|
description:
|
|
282
|
-
'
|
|
283
|
-
'
|
|
284
|
-
'listing them; commit them one at a time.
|
|
282
|
+
'Commit, as you, the one file left uncommitted on `branch`, without sharing it. Writes commit and share ' +
|
|
283
|
+
'on their own moments later (`branch_status` lists those still landing); `unzip` leaves its files uncommitted. ' +
|
|
284
|
+
'If several files are uncommitted, returns a structured error listing them; commit them one at a time. ' +
|
|
285
|
+
'Refused on protected branches without write permission.',
|
|
285
286
|
inputs: {
|
|
286
287
|
type: 'object',
|
|
287
288
|
properties: {
|
|
@@ -309,7 +310,7 @@ export function registerWorkflowTools(
|
|
|
309
310
|
name: 'save_file',
|
|
310
311
|
description:
|
|
311
312
|
'Schedule the file at <path> for commit + push using whatever bytes are on disk (no content ' +
|
|
312
|
-
'rewrite) — for
|
|
313
|
+
'rewrite) — for files left uncommitted, such as those `unzip` extracts. On lock contention returns ' +
|
|
313
314
|
'`{ saved: false, reason }` rather than throwing. The commit lands asynchronously.',
|
|
314
315
|
inputs: {
|
|
315
316
|
type: 'object',
|
|
@@ -357,8 +358,9 @@ export function registerWorkflowTools(
|
|
|
357
358
|
mount({
|
|
358
359
|
name: 'share_current_branch',
|
|
359
360
|
description:
|
|
360
|
-
'Publish
|
|
361
|
-
'
|
|
361
|
+
'Publish `branch` to the shared remote so teammates and any change request pick up its commits. ' +
|
|
362
|
+
'Writes already share on their own: this is for sharing again after a refused push (`branch_status` shows ' +
|
|
363
|
+
'the branch ahead). Refused on protected branches without write permission.',
|
|
362
364
|
inputs: { type: 'object', properties: {}, additionalProperties: false },
|
|
363
365
|
outputs: {
|
|
364
366
|
type: 'object',
|
|
@@ -377,10 +379,11 @@ export function registerWorkflowTools(
|
|
|
377
379
|
description:
|
|
378
380
|
'Create a new unprotected draft named `name`, forked from the existing branch `branch`. The ' +
|
|
379
381
|
`backend rejects protected names (${protectedInline(kb)}) and filesystem-invalid names — surface ` +
|
|
380
|
-
'those errors verbatim.
|
|
381
|
-
'
|
|
382
|
+
'those errors verbatim. Name it `<you>/<kebab-slug>`: `<you>` is the person\'s email before the @, in lower case, ' +
|
|
383
|
+
'with each run of characters other than letters and digits as one `-` (`paul.brandt@` gives `paul-brandt/`). ' +
|
|
384
|
+
'Only that prefix makes the draft theirs, to delete later. Does NOT switch the agent onto the new draft.',
|
|
382
385
|
// `branch` here is the fork BASE — its own meaning — so skip the generic
|
|
383
|
-
// workspace-`branch` injection ("the branch
|
|
386
|
+
// workspace-`branch` injection ("the branch to work on: ... or your draft"),
|
|
384
387
|
// whose wording invites filling in the draft being created instead.
|
|
385
388
|
skipBranch: true,
|
|
386
389
|
// The handler refuses a missing `name` with a message that says what the
|
|
@@ -389,7 +392,7 @@ export function registerWorkflowTools(
|
|
|
389
392
|
inputs: {
|
|
390
393
|
type: 'object',
|
|
391
394
|
properties: {
|
|
392
|
-
name: { type: 'string', minLength: 1, description: 'Name of the draft to create (`<
|
|
395
|
+
name: { type: 'string', minLength: 1, description: 'Name of the draft to create (`<you>/<kebab-slug>` for user drafts, as the description says). Must not exist yet.' },
|
|
393
396
|
branch: { type: 'string', minLength: 1, description: 'Existing branch to fork the new draft from — usually the branch you are currently on.' },
|
|
394
397
|
},
|
|
395
398
|
required: ['name', 'branch'],
|
|
@@ -437,7 +440,7 @@ export function registerWorkflowTools(
|
|
|
437
440
|
description:
|
|
438
441
|
'Open a change request from a draft branch into a target branch. Auto-merges the latest target ' +
|
|
439
442
|
'into the source first, pushes, then creates the CR. On conflicts returns a ' +
|
|
440
|
-
'`change-request-conflicts` error with affected paths.
|
|
443
|
+
'`change-request-conflicts` error with affected paths. ' +
|
|
441
444
|
'Answers a SUMMARY, not the request\'s content: `url` first (hand it to the user), then `number`, ' +
|
|
442
445
|
'`title`, `state`, `sourceBranch`, `targetBranch`, `approvals` (who must approve each file), ' +
|
|
443
446
|
'`mergeBlockedReasons` (what blocks the merge), and `files` — the changed paths with their kind of ' +
|
|
@@ -549,12 +552,13 @@ export function registerWorkflowTools(
|
|
|
549
552
|
name: 'merge_branch',
|
|
550
553
|
description:
|
|
551
554
|
'Merge branch `source` into branch `target` as you, and publish `target`. An agent proposes and syncs; ' +
|
|
552
|
-
'a person merges: this
|
|
555
|
+
'a person merges: this never lands a change request. It refuses while a change request from `source` ' +
|
|
553
556
|
'into `target` is open (naming it): ask the user to review it in the app. It refuses ' +
|
|
554
|
-
'when `target` is a protected branch unless you could commit every changed file directly to it
|
|
557
|
+
'when `target` is a protected branch unless you could commit every changed file directly to it (never ' +
|
|
558
|
+
'for a change to `roles.yaml`). ' +
|
|
555
559
|
'SYNC: merging the target into your draft (`source` = the change request\'s target, `target` = your draft) ' +
|
|
556
560
|
'is always allowed, even with the draft\'s request open. ' +
|
|
557
|
-
'Writes are committed asynchronously;
|
|
561
|
+
'Writes are committed asynchronously; it waits up to 20s for those on `source`. ' +
|
|
558
562
|
'Returns `merged` with the merge commit; `nothing-to-merge` with `target`\'s tip when it already holds ' +
|
|
559
563
|
'all of `source`; `pending-commits` (nothing merged) when writes are still committing — retry shortly — ' +
|
|
560
564
|
'or one failed (`needsAttention`: tell the user); or `conflicts-need-resolution` with the conflicting ' +
|
|
@@ -618,7 +622,7 @@ export function registerWorkflowTools(
|
|
|
618
622
|
name: 'delete_branch',
|
|
619
623
|
description:
|
|
620
624
|
'Delete branch `name` from the shared repository and the server. Allowed for the ' +
|
|
621
|
-
'branch\'s author (`<
|
|
625
|
+
'branch\'s author (`<you>/…` per `create_branch`, or your own `suggestions/…` bundle) or an Admin. Refuses a ' +
|
|
622
626
|
'protected branch, a name that is not a branch, a branch with saves still landing or a file held ' +
|
|
623
627
|
'(retry once they land). An open change request from or into it that proposes nothing is closed and the ' +
|
|
624
628
|
'delete goes ahead; one that still proposes something refuses it, naming and linking it: its author ' +
|
|
@@ -836,13 +840,13 @@ export function registerWorkflowTools(
|
|
|
836
840
|
'One file as it was at a point in history: `at` is a commit (from `list_file_changes`, on `branch`\'s ' +
|
|
837
841
|
'own history) or the name of another branch (its published tip). Answers `content` (null when the ' +
|
|
838
842
|
'file was not there) and `sha`, the commit it was read at; for a commit, also `before`, the file ' +
|
|
839
|
-
'just before that commit. To put an earlier version back, write it with `write_file`. Takes
|
|
840
|
-
'the file
|
|
843
|
+
'just before that commit. To put an earlier version back, write it with `write_file`. Takes your ' +
|
|
844
|
+
'read of the file on `branch` now and, for a branch, also at its tip.',
|
|
841
845
|
inputs: {
|
|
842
846
|
type: 'object',
|
|
843
847
|
properties: {
|
|
844
848
|
path: { type: 'string', minLength: 1, description: 'The file, workspace-relative.' },
|
|
845
|
-
at: { type: 'string', minLength: 1, description: 'A commit sha, or a branch name.' },
|
|
849
|
+
at: { type: 'string', minLength: 1, description: 'A full commit sha (40 or 64 hex characters), or a branch name.' },
|
|
846
850
|
sessionId: SESSION_ID_INPUT,
|
|
847
851
|
},
|
|
848
852
|
required: ['path', 'at'],
|
|
@@ -4,19 +4,17 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { DEFAULT_KB_LAYOUT } from '@bevel-software/platform-shared';
|
|
6
6
|
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
7
|
-
import { ToolRegistry } from '../../tool-registry/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import type {
|
|
11
|
-
import type { IAccessControl } from '../../access/access-control.interface.js';
|
|
7
|
+
import { GUIDE_FIRST_SENTENCE, ToolRegistry } from '../../tool-registry/index.js';
|
|
8
|
+
import { WorkflowHooks, createToolHandlerFactory } from '../../../__tests__/tool-wiring.js';
|
|
9
|
+
import type { ToolAuth } from '../../tool-auth/index.js';
|
|
10
|
+
import type { IAccessControl } from '../../access/index.js';
|
|
12
11
|
import { DocExtractService } from '../file-readers/doc-extract.service.js';
|
|
13
12
|
import { WorkerDocumentExtractor } from '../file-readers/extract-in-worker.js';
|
|
14
13
|
import { RoutineWritePolicyService } from '../routine-write-policy.js';
|
|
15
14
|
import { SpillStore } from '../spill-store.js';
|
|
16
|
-
import { WorkflowHooks } from '../../workflow/workflow-hooks.js';
|
|
17
15
|
import { ToolDescriptionNotes } from '../../workspace/agent-access.gate.js';
|
|
18
16
|
import { registerWorkspaceTools } from '../workspace.tools.js';
|
|
19
|
-
import { sharedFileRulesSection } from '../../agent-instructions/
|
|
17
|
+
import { sharedFileRulesSection } from '../../agent-instructions/index.js';
|
|
20
18
|
import { RipgrepPatternMatcher } from '../index.js';
|
|
21
19
|
|
|
22
20
|
/** The grep matcher this file's harness wires, closed once the file is done. */
|
|
@@ -80,10 +78,10 @@ async function listed(registry: ToolRegistry): Promise<Map<string, string>> {
|
|
|
80
78
|
afterEach(() => kb.applyLayout({ ...DEFAULT_KB_LAYOUT }));
|
|
81
79
|
|
|
82
80
|
describe('what tells an agent to read the guide first', () => {
|
|
83
|
-
it('tells an agent to call get_agent_guide first, and that AGENTS.md answers with
|
|
81
|
+
it('tells an agent to call get_agent_guide first, and that AGENTS.md answers with that start after the organisation\'s own', () => {
|
|
84
82
|
const rules = sharedFileRulesSection(kb.layout);
|
|
85
|
-
expect(rules).toContain("call `get_agent_guide
|
|
86
|
-
expect(rules).toContain('read_file on `AGENTS.md` at the KB root answers with
|
|
83
|
+
expect(rules).toContain("call `get_agent_guide`: it answers the guide's start");
|
|
84
|
+
expect(rules).toContain('read_file on `AGENTS.md` at the KB root answers with that start');
|
|
87
85
|
expect(rules).toContain("after the organisation's own conventions file of that name");
|
|
88
86
|
expect(rules).toContain('`CLAUDE.md`');
|
|
89
87
|
// One name on every deployment: the rule names no other.
|
|
@@ -10,40 +10,38 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
10
10
|
import { testKbContext } from '../../../__tests__/kb-context.js';
|
|
11
11
|
import { slowRipgrep } from '../../../__tests__/slow-ripgrep.js';
|
|
12
12
|
import { LocalFilesystem } from '@mastra/core/workspace';
|
|
13
|
-
import { ToolRegistry } from '../../tool-registry/
|
|
14
|
-
import { createToolHandlerFactory } from '
|
|
15
|
-
import { ToolError, type ToolContext } from '../../tool-helpers/
|
|
16
|
-
import type { ToolAuth } from '../../tool-auth/
|
|
13
|
+
import { GUIDE_FIRST_SENTENCE, ToolRegistry } from '../../tool-registry/index.js';
|
|
14
|
+
import { AccessControlService, WorkflowHooks, createToolHandlerFactory, type AgentOperationContext } from '../../../__tests__/tool-wiring.js';
|
|
15
|
+
import { ToolError, type ToolContext } from '../../tool-helpers/index.js';
|
|
16
|
+
import type { ToolAuth } from '../../tool-auth/index.js';
|
|
17
17
|
import { registerWorkspaceTools } from '../workspace.tools.js';
|
|
18
18
|
import { RipgrepPatternMatcher, type RipgrepPatternMatcherOptions } from '../index.js';
|
|
19
|
-
import { sharedFileRules, sharedFileRulesSection } from '../../agent-instructions/
|
|
20
|
-
import { composeAgentGuide } from '../../agent-guide/
|
|
21
|
-
import { GUIDE_FIRST_SENTENCE } from '../../tool-registry/guide-first.js';
|
|
19
|
+
import { sharedFileRules, sharedFileRulesSection } from '../../agent-instructions/index.js';
|
|
20
|
+
import { composeAgentGuide } from '../../agent-guide/index.js';
|
|
22
21
|
import { RoutineWritePolicyService } from '../routine-write-policy.js';
|
|
23
22
|
import { UuidSessionSink, type ISessionSink } from '../session-sink.js';
|
|
24
23
|
import { FIRST_RUN_SECTION_ID, STARTER_GUIDE_FILE, firstRunNote, type FirstRunStarterSource } from '../first-run.js';
|
|
25
|
-
import { WorkflowHooks, type AgentOperationContext } from '../../workflow/workflow-hooks.js';
|
|
26
24
|
import { SESSION_ID_DESCRIPTION, ToolDescriptionNotes } from '../agent-access.gate.js';
|
|
27
25
|
import { SpillStore } from '../spill-store.js';
|
|
28
26
|
import { DocExtractService } from '../file-readers/doc-extract.service.js';
|
|
29
27
|
import { WorkerDocumentExtractor } from '../file-readers/extract-in-worker.js';
|
|
30
28
|
import { OCTET_STREAM_FALLBACK_NOTE } from '../file-readers/content-mode.js';
|
|
31
|
-
import type { IAccessControl } from '../../access/
|
|
32
|
-
import {
|
|
33
|
-
import { NodeFs } from '../../kb-fs/node-fs.js';
|
|
29
|
+
import type { IAccessControl } from '../../access/index.js';
|
|
30
|
+
import { realDisk } from '../../../__tests__/real-disk.js';
|
|
34
31
|
import type { WorkspaceService } from '../workspace.service.js';
|
|
35
32
|
import { DEFAULT_KB_LAYOUT, isBranchAuthoredBy, isOwnSuggestionsBranch } from '@bevel-software/platform-shared';
|
|
36
|
-
import { normalizeWorkspacePath } from '../../kb-fs/
|
|
33
|
+
import { normalizeWorkspacePath } from '../../kb-fs/index.js';
|
|
37
34
|
import { GIT_INTERNALS_MESSAGE, PathNotFoundError } from '../../../shared/domain-errors.js';
|
|
38
|
-
import { AccessDeniedError } from '../../access-model/
|
|
35
|
+
import { AccessDeniedError } from '../../access-model/index.js';
|
|
39
36
|
import { proposalTitleFor } from '../write-denial.js';
|
|
40
37
|
import { NOT_FOUND_NEXT_STEP } from '../not-found.js';
|
|
41
38
|
import { compileCheck, exampleArguments } from '@bevel-software/platform-mcp-core';
|
|
42
|
-
import { routeToolSchemas } from '../../tool-helpers/route-tool-schemas.js';
|
|
43
|
-
import { TOOL_DESCRIPTION_CAP, clientVisibleLength } from '../../tool-registry/description-length.js';
|
|
44
39
|
|
|
45
40
|
const KB_DIR = 'knowledge-base';
|
|
46
41
|
|
|
42
|
+
/** A move that changes nobody's access: what a stand-in without rules answers. */
|
|
43
|
+
const NO_CHANGES = { lose: [], gain: [] };
|
|
44
|
+
|
|
47
45
|
/** Allow-all access control — the default for tests not exercising read gating. */
|
|
48
46
|
const allowAll = {
|
|
49
47
|
canRead: async () => true,
|
|
@@ -67,6 +65,8 @@ const allowAll = {
|
|
|
67
65
|
// a folder's `access.md` files actually does to it is exercised against
|
|
68
66
|
// REAL rules in "a move preview judges the destination as it will be".
|
|
69
67
|
previewAccessAfterRelocation: async () => ({ read: true, write: true, download: true, owner: true }),
|
|
68
|
+
// Who else loses or gains access in a move: nobody, here.
|
|
69
|
+
prospectiveHolderChanges: async () => NO_CHANGES,
|
|
70
70
|
} as unknown as IAccessControl;
|
|
71
71
|
|
|
72
72
|
/** Access control that denies `canRead` for an explicit set of repo-relative paths. */
|
|
@@ -2124,30 +2124,12 @@ describe('office documents and PDFs', () => {
|
|
|
2124
2124
|
// Narrowed by hand: the assertion above already failed the test if not.
|
|
2125
2125
|
if (!compiled.checkable) throw new Error(compiled.reason);
|
|
2126
2126
|
expect(compiled.check(exampleArguments(def.inputs)), def.name).toEqual([]);
|
|
2127
|
-
// And the FLAT schema, which is what the tool's own route checks the
|
|
2128
|
-
// call against: the two must agree, or a call the example produced would
|
|
2129
|
-
// be refused one layer in.
|
|
2130
|
-
const flat = routeToolSchemas(def.name)?.flat;
|
|
2131
|
-
expect(flat, def.name).toBeDefined();
|
|
2132
|
-
const flatCheck = compileCheck(flat);
|
|
2133
|
-
expect(flatCheck.checkable, `${def.name} (flat): ${flatCheck.checkable ? '' : flatCheck.reason}`).toBe(true);
|
|
2134
|
-
if (!flatCheck.checkable) throw new Error(flatCheck.reason);
|
|
2135
|
-
expect(flatCheck.check(exampleArguments(flat)), `${def.name} (flat)`).toEqual([]);
|
|
2136
|
-
}
|
|
2137
|
-
});
|
|
2138
|
-
|
|
2139
|
-
it('every description, call example and purpose prefix included, stays inside the cap a client shows', async () => {
|
|
2140
|
-
await start();
|
|
2141
|
-
const tools = await toolRegistry.listInternal();
|
|
2142
|
-
for (const def of tools) {
|
|
2143
|
-
// Measured as a CLIENT receives it: the call line, the purpose prefix at
|
|
2144
|
-
// its own cap (the four knowledge-base tools carry one), and the
|
|
2145
|
-
// description — the three things that ride one tool's entry.
|
|
2146
|
-
const received = clientVisibleLength(def);
|
|
2147
|
-
expect(received, `${def.name} is ${received} characters (cap ${TOOL_DESCRIPTION_CAP})`).toBeLessThanOrEqual(
|
|
2148
|
-
TOOL_DESCRIPTION_CAP,
|
|
2149
|
-
);
|
|
2150
2127
|
}
|
|
2128
|
+
// That the FLAT schema each route checks a call against agrees with the
|
|
2129
|
+
// example is the route layer's own property: tool-helpers'
|
|
2130
|
+
// `route-tool-schemas.catalog.test.ts` holds it over the whole catalog.
|
|
2131
|
+
// And that every description stays inside the cap a client shows is the
|
|
2132
|
+
// registry's: `tool-description-length.test.ts`, over the same catalog.
|
|
2151
2133
|
});
|
|
2152
2134
|
|
|
2153
2135
|
describe('binary capability contract: a text file, a document, an image and a zip', () => {
|
|
@@ -2475,7 +2457,7 @@ describe('start_session', () => {
|
|
|
2475
2457
|
undefined,
|
|
2476
2458
|
undefined /* downloads */,
|
|
2477
2459
|
starterPacks,
|
|
2478
|
-
|
|
2460
|
+
realDisk(),
|
|
2479
2461
|
);
|
|
2480
2462
|
app.use('/api', router);
|
|
2481
2463
|
server = await new Promise<HttpServer>((r) => {
|
|
@@ -3256,7 +3238,8 @@ describe('preflight for moves and deletes', () => {
|
|
|
3256
3238
|
if (rel === 'sealed.md' || rel.endsWith('/sealed.md')) return { read: true, write: false, download: false, owner: false };
|
|
3257
3239
|
if (rel.startsWith('HR/')) return { read: true, write: true, download: false, owner: false };
|
|
3258
3240
|
if (rel.startsWith('Locked/')) return { read: true, write: false, download: false, owner: false };
|
|
3259
|
-
|
|
3241
|
+
// The folder itself answers as its contents do, as the resolver's scopes do.
|
|
3242
|
+
if (rel === 'Secret' || rel.startsWith('Secret/')) return { read: false, write: false, download: false, owner: false };
|
|
3260
3243
|
return { read: true, write: true, download: true, owner: true };
|
|
3261
3244
|
};
|
|
3262
3245
|
const rules = {
|
|
@@ -3273,6 +3256,7 @@ describe('preflight for moves and deletes', () => {
|
|
|
3273
3256
|
// preview learned to relocate them, which keeps every ordering and
|
|
3274
3257
|
// oracle test below about what it is about.
|
|
3275
3258
|
previewAccessAfterRelocation: async (_w: string, _u: string, _from: string, to: string) => verbsFor(to),
|
|
3259
|
+
prospectiveHolderChanges: async () => NO_CHANGES,
|
|
3276
3260
|
} as unknown as IAccessControl;
|
|
3277
3261
|
|
|
3278
3262
|
const call = async (base: string, tool: string, body: Record<string, unknown>) => {
|
|
@@ -3401,10 +3385,12 @@ describe('preflight for moves and deletes', () => {
|
|
|
3401
3385
|
writes.push(op.wsPath ?? '');
|
|
3402
3386
|
});
|
|
3403
3387
|
expect((await call(base, 'move_file', args)).body).toMatchObject({ moved: true });
|
|
3404
|
-
//
|
|
3405
|
-
//
|
|
3406
|
-
//
|
|
3407
|
-
|
|
3388
|
+
// The move's two ends first, as read_file tells it (what moves, and
|
|
3389
|
+
// the folder it lands in); then `Locked/refs.md` is named (left for
|
|
3390
|
+
// want of write access, its links listed), so the read hook hears of
|
|
3391
|
+
// it; the unreadable page and the transcript are never named, so it
|
|
3392
|
+
// never hears of them.
|
|
3393
|
+
expect(reads.sort()).toEqual([KB('Locked/refs.md'), KB('Sales/2026'), KB('Sales/deal.md'), KB('Sales/deal.md'), KB('Sales/index.md')]);
|
|
3408
3394
|
expect([...new Set(writes)].sort()).toEqual([KB('Sales/2026/deal.md'), KB('Sales/deal.md'), KB('Sales/index.md')]);
|
|
3409
3395
|
});
|
|
3410
3396
|
|
|
@@ -3450,6 +3436,32 @@ describe('preflight for moves and deletes', () => {
|
|
|
3450
3436
|
expect(await exists(args.src)).toBe(false);
|
|
3451
3437
|
});
|
|
3452
3438
|
|
|
3439
|
+
it('a caller who cannot read what moves is refused as read_file refuses them, on a draft too, and nothing is looked at', async () => {
|
|
3440
|
+
const resolved: string[] = [];
|
|
3441
|
+
const base = await start('write', {
|
|
3442
|
+
...rules,
|
|
3443
|
+
prospectiveHolderChanges: async (_w: string, from: string) => {
|
|
3444
|
+
resolved.push(from);
|
|
3445
|
+
return { lose: [{ kind: 'group', name: 'Board', verb: 'read' }], gain: [] };
|
|
3446
|
+
},
|
|
3447
|
+
} as unknown as IAccessControl);
|
|
3448
|
+
await fs.writeFile(KB('Secret/plan.md'), 'plan');
|
|
3449
|
+
// A draft is not write-gated, so only the read gate stands between a
|
|
3450
|
+
// caller and a move of a file they may not open — and it does: the dry
|
|
3451
|
+
// run, the unconfirmed move and the confirmed one are all read_file's
|
|
3452
|
+
// own refusal, with nobody resolved and nothing moved.
|
|
3453
|
+
const args = { branch: 'someone/draft', src: KB('Secret/plan.md'), dest: KB('Secret/old/plan.md') };
|
|
3454
|
+
const read = await call(base, 'read_file', { branch: args.branch, path: args.src });
|
|
3455
|
+
expect(read.status).toBe(403);
|
|
3456
|
+
for (const body of [{ ...args, dryRun: true }, args, { ...args, confirm: true }]) {
|
|
3457
|
+
const res = await call(base, 'move_file', body);
|
|
3458
|
+
expect(res.status).toBe(403);
|
|
3459
|
+
expect(res.body).toEqual(read.body);
|
|
3460
|
+
}
|
|
3461
|
+
expect(resolved).toEqual([]);
|
|
3462
|
+
expect(await exists(args.src)).toBe(true);
|
|
3463
|
+
});
|
|
3464
|
+
|
|
3453
3465
|
it('an access-changing move shows before/after and runs only with confirm: true', async () => {
|
|
3454
3466
|
const base = await seeded();
|
|
3455
3467
|
const args = { src: KB('Sales/deal.md'), dest: KB('HR/deal.md') };
|
|
@@ -3648,13 +3660,18 @@ describe('preflight for moves and deletes', () => {
|
|
|
3648
3660
|
expect(await exists(KB('Sales/moved'))).toBe(false);
|
|
3649
3661
|
});
|
|
3650
3662
|
|
|
3651
|
-
it('a
|
|
3663
|
+
it('a move into a folder the caller cannot read is refused as read_file refuses the folder, before the write verdict', async () => {
|
|
3652
3664
|
const base = await seeded();
|
|
3665
|
+
// The folder's rules are what a move preview would read back (the
|
|
3666
|
+
// caller's verdicts there, who else is affected), so the read refusal
|
|
3667
|
+
// comes first — not the write denial, which would say whether the path
|
|
3668
|
+
// can be proposed and name the folder's contents on a collision.
|
|
3653
3669
|
const run = await call(base, 'move_file', { src: KB('Sales/deal.md'), dest: KB('Secret/deal.md'), confirm: true });
|
|
3654
3670
|
expect(run.status).toBe(403);
|
|
3655
|
-
expect(run.body).
|
|
3671
|
+
expect(run.body.error).toContain(`permission to read "${KB('Secret')}"`);
|
|
3672
|
+
expect(run.body.kind).toBeUndefined();
|
|
3656
3673
|
expect(run.body.proposal).toBeUndefined();
|
|
3657
|
-
expect(
|
|
3674
|
+
expect(await exists(KB('Sales/deal.md'))).toBe(true);
|
|
3658
3675
|
});
|
|
3659
3676
|
|
|
3660
3677
|
it('a draft branch is not write-gated, so the same move is allowed there', async () => {
|
|
@@ -4225,7 +4242,8 @@ describe('preflight for moves and deletes', () => {
|
|
|
4225
4242
|
// The protocol itself, and what a platform folder does to it, are shared.
|
|
4226
4243
|
const rules = sharedFileRulesSection(testKbContext().layout);
|
|
4227
4244
|
expect(rules).toContain('`dryRun: true`');
|
|
4228
|
-
|
|
4245
|
+
// Anyone's: a move asks when it changes who else may open or edit the file too (`holderChanges`).
|
|
4246
|
+
expect(rules).toContain("A non-empty folder is deleted, and a move that changes anyone's access runs, only with `confirm: true`");
|
|
4229
4247
|
expect(rules).toContain('platform folder');
|
|
4230
4248
|
const dry = await call(base, 'delete_folder', { path: KB('Sales/archive'), dryRun: true });
|
|
4231
4249
|
const unconfirmed = await call(base, 'delete_folder', { path: KB('Sales/archive') });
|
|
@@ -4291,6 +4309,7 @@ describe('a write refused for permissions says whether and how to propose it', (
|
|
|
4291
4309
|
canOwner: async () => true,
|
|
4292
4310
|
canWriteBatchAtRef: async () => null,
|
|
4293
4311
|
previewAccessAfterRelocation: async () => ({ read: true, write: true, download: true, owner: true }),
|
|
4312
|
+
prospectiveHolderChanges: async () => NO_CHANGES,
|
|
4294
4313
|
eligibleWritersAtRef: async () => ({ roles: ['Sales Lead'], users: [{ name: 'Owner', email: 'owner@x' }] }),
|
|
4295
4314
|
} as unknown as IAccessControl;
|
|
4296
4315
|
return { ac, calls };
|
|
@@ -5134,7 +5153,7 @@ describe('a move preview judges the destination as it will be', () => {
|
|
|
5134
5153
|
ensureRemotesFetched: async () => undefined,
|
|
5135
5154
|
} as unknown as WorkspaceService,
|
|
5136
5155
|
KB_DIR,
|
|
5137
|
-
|
|
5156
|
+
realDisk(),
|
|
5138
5157
|
);
|
|
5139
5158
|
return {
|
|
5140
5159
|
canRead: (w: string, u: string, r: string) => fresh().canRead(w, u, r),
|
|
@@ -5146,6 +5165,7 @@ describe('a move preview judges the destination as it will be', () => {
|
|
|
5146
5165
|
previewAccessAfterRelocation: (
|
|
5147
5166
|
w: string, u: string, from: string, to: string, opts?: { sourceRemains?: boolean },
|
|
5148
5167
|
) => fresh().previewAccessAfterRelocation(w, u, from, to, opts),
|
|
5168
|
+
prospectiveHolderChanges: (w: string, from: string, to: string) => fresh().prospectiveHolderChanges(w, from, to),
|
|
5149
5169
|
} as unknown as IAccessControl;
|
|
5150
5170
|
};
|
|
5151
5171
|
|
|
@@ -5189,6 +5209,8 @@ describe('a move preview judges the destination as it will be', () => {
|
|
|
5189
5209
|
expect(dry.body).toMatchObject({ kind: 'folder', accessChanges: false, allowed: true, moved: false });
|
|
5190
5210
|
expect(dry.body.access.after).toEqual(dry.body.access.before);
|
|
5191
5211
|
expect(dry.body.access.after.owner).toBe(true);
|
|
5212
|
+
// A rename carries its own rules along: nobody else gains or loses either.
|
|
5213
|
+
expect(dry.body.holderChanges).toBeUndefined();
|
|
5192
5214
|
// Nothing moved and nothing was written to answer the question.
|
|
5193
5215
|
expect(await fs.exists(args.dest)).toBe(false);
|
|
5194
5216
|
expect(await fs.exists(KB('Sales/access.md'))).toBe(true);
|
|
@@ -5213,6 +5235,91 @@ describe('a move preview judges the destination as it will be', () => {
|
|
|
5213
5235
|
expect(await verbsAt(base, args.dest)).toEqual(dry.body.access.after);
|
|
5214
5236
|
});
|
|
5215
5237
|
|
|
5238
|
+
it('a move that changes only who ELSE may edit it names them, and asks before it moves', async () => {
|
|
5239
|
+
const base = await seeded();
|
|
5240
|
+
// `Ops/` gives the caller what `Sales/` gives, and Ops Lead write as well.
|
|
5241
|
+
await fs.writeFile(
|
|
5242
|
+
KB('Ops/access.md'),
|
|
5243
|
+
rules(`write:\n - Mover <${MOVER}>\n - Ops Lead <ops@x.io>\nowner:\n - Mover <${MOVER}>\n`),
|
|
5244
|
+
);
|
|
5245
|
+
const args = { src: KB('Sales/deal.md'), dest: KB('Ops/deal.md') };
|
|
5246
|
+
|
|
5247
|
+
const dry = await call(base, 'move_file', { ...args, dryRun: true });
|
|
5248
|
+
expect(dry.body).toMatchObject({ accessChanges: false, allowed: true });
|
|
5249
|
+
// Everyone may read on both sides, so the only change is who may edit.
|
|
5250
|
+
expect(dry.body.holderChanges).toEqual({
|
|
5251
|
+
lose: [],
|
|
5252
|
+
gain: [{ kind: 'person', name: 'ops@x.io', email: 'ops@x.io', verb: 'write' }],
|
|
5253
|
+
});
|
|
5254
|
+
|
|
5255
|
+
const unconfirmed = await call(base, 'move_file', args);
|
|
5256
|
+
expect(unconfirmed.body).toMatchObject({ confirmationRequired: true, moved: false });
|
|
5257
|
+
expect(unconfirmed.body.message).toContain('`holderChanges`');
|
|
5258
|
+
expect(await fs.exists(args.src)).toBe(true);
|
|
5259
|
+
expect((await call(base, 'move_file', { ...args, confirm: true })).body).toMatchObject({ moved: true });
|
|
5260
|
+
});
|
|
5261
|
+
|
|
5262
|
+
it('a source the read hook refuses is refused as read_file refuses it: no preview, not even that the move would ask', async () => {
|
|
5263
|
+
const base = await seeded();
|
|
5264
|
+
await fs.writeFile(
|
|
5265
|
+
KB('Ops/access.md'),
|
|
5266
|
+
rules(`write:\n - Mover <${MOVER}>\n - Ops Lead <ops@x.io>\nowner:\n - Mover <${MOVER}>\n`),
|
|
5267
|
+
);
|
|
5268
|
+
const args = { src: KB('Sales/deal.md'), dest: KB('Ops/deal.md'), rewriteLinks: false };
|
|
5269
|
+
// The rules let the caller read it; a deployment's read hook does not. The
|
|
5270
|
+
// hook's refusal is the whole answer: a "this move asks for confirm" would
|
|
5271
|
+
// already say what the comparison found behind the hook.
|
|
5272
|
+
hooks.onAgentRead(async (op) => {
|
|
5273
|
+
if (op.wsPath === args.src) throw new Error('not for agents');
|
|
5274
|
+
});
|
|
5275
|
+
const read = await call(base, 'read_file', { path: args.src });
|
|
5276
|
+
expect(read.status).toBeGreaterThanOrEqual(400);
|
|
5277
|
+
for (const body of [{ ...args, dryRun: true }, args, { ...args, confirm: true }]) {
|
|
5278
|
+
const res = await call(base, 'move_file', body);
|
|
5279
|
+
expect(res.status).toBe(read.status);
|
|
5280
|
+
expect(res.body).toEqual(read.body);
|
|
5281
|
+
expect(JSON.stringify(res.body)).not.toContain('ops@x.io');
|
|
5282
|
+
}
|
|
5283
|
+
expect(await fs.exists(args.src)).toBe(true);
|
|
5284
|
+
});
|
|
5285
|
+
|
|
5286
|
+
it('a caller who reads what moves but not the folder it lands in is refused, on a draft too, and told nothing of the folder', async () => {
|
|
5287
|
+
const base = await seeded();
|
|
5288
|
+
// `Closed/` is somebody else's: its rules are what a move preview would
|
|
5289
|
+
// read back (who it lets in, whom it denies). On a draft no write gate
|
|
5290
|
+
// stands, so the read gate is what refuses — before anything is looked at.
|
|
5291
|
+
await fs.writeFile(
|
|
5292
|
+
KB('Closed/access.md'),
|
|
5293
|
+
rules('read:\n - Other <other@x.io>\n - deny everyone\nwrite:\n - Other <other@x.io>\n'),
|
|
5294
|
+
);
|
|
5295
|
+
await fs.writeFile(KB('Closed/deal.md'), 'taken');
|
|
5296
|
+
const args = { branch: 'someone/draft', src: KB('Sales/deal.md'), dest: KB('Closed/deal.md'), rewriteLinks: false };
|
|
5297
|
+
|
|
5298
|
+
for (const body of [{ ...args, dryRun: true }, args, { ...args, confirm: true }]) {
|
|
5299
|
+
const res = await call(base, 'move_file', body);
|
|
5300
|
+
expect(res.status).toBe(403);
|
|
5301
|
+
expect(res.body.error).toContain(`permission to read "${KB('Closed')}"`);
|
|
5302
|
+
// Neither the folder's rules nor what sits in it: the name clash at
|
|
5303
|
+
// `Closed/deal.md` is never reached.
|
|
5304
|
+
const text = JSON.stringify(res.body);
|
|
5305
|
+
expect(text).not.toContain('other@x.io');
|
|
5306
|
+
expect(text).not.toContain('already exists');
|
|
5307
|
+
}
|
|
5308
|
+
expect(await fs.exists(args.src)).toBe(true);
|
|
5309
|
+
expect(await fs.readFile(KB('Closed/deal.md'), { encoding: 'utf8' })).toBe('taken');
|
|
5310
|
+
});
|
|
5311
|
+
|
|
5312
|
+
it('a move out of a folder names everyone it leaves behind', async () => {
|
|
5313
|
+
const base = await seeded();
|
|
5314
|
+
const args = { src: KB('Work/Notes'), dest: KB('Legal/Notes') };
|
|
5315
|
+
const dry = await call(base, 'move_file', { ...args, dryRun: true });
|
|
5316
|
+
// Everyone may still read it in Legal/: the caller loses editing, not opening.
|
|
5317
|
+
expect(dry.body.holderChanges).toEqual({
|
|
5318
|
+
lose: [{ kind: 'person', name: MOVER, email: MOVER, verb: 'write' }],
|
|
5319
|
+
gain: [],
|
|
5320
|
+
});
|
|
5321
|
+
});
|
|
5322
|
+
|
|
5216
5323
|
it('what the folder inherited is left behind; what its own access.md gives comes along', async () => {
|
|
5217
5324
|
const base = await seeded();
|
|
5218
5325
|
const args = { src: KB('Work/Team'), dest: KB('Legal/Team') };
|
|
@@ -32,7 +32,7 @@ import type { WorkflowHooks } from '../workflow/index.js';
|
|
|
32
32
|
* takes, so a run's reads and its questions share one conversation.
|
|
33
33
|
*/
|
|
34
34
|
export const SESSION_ID_DESCRIPTION =
|
|
35
|
-
"This conversation's id
|
|
35
|
+
"This conversation's id: call `start_session` once, at the start of your work, and pass its id here, inside `call_tool_chain` too (and to `ask`, where the deployment offers it). The in-process agent runtime sets it itself.";
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* The `sessionId` input every gated tool declares. The description a MOUNTED
|