@bevel-software/platform-core-backend 0.12.1 → 0.13.3
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/THIRD-PARTY-NOTICES.md +5 -3
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +8 -1
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +58 -2
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +174 -33
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/admin-locked-commit.d.ts.map +1 -1
- package/dist/modules/access/admin-locked-commit.js +1 -0
- package/dist/modules/access/admin-locked-commit.js.map +1 -1
- package/dist/modules/access/synced-groups-committer.js +1 -1
- package/dist/modules/access/synced-groups-committer.js.map +1 -1
- package/dist/modules/access-model/access-errors.d.ts +11 -0
- package/dist/modules/access-model/access-errors.d.ts.map +1 -1
- package/dist/modules/access-model/access-errors.js +14 -0
- package/dist/modules/access-model/access-errors.js.map +1 -1
- package/dist/modules/access-model/access-grammar.d.ts +24 -8
- package/dist/modules/access-model/access-grammar.d.ts.map +1 -1
- package/dist/modules/access-model/access-grammar.js +123 -7
- package/dist/modules/access-model/access-grammar.js.map +1 -1
- package/dist/modules/declared-variables/declared-variables.routes.d.ts +42 -0
- package/dist/modules/declared-variables/declared-variables.routes.d.ts.map +1 -0
- package/dist/modules/declared-variables/declared-variables.routes.js +135 -0
- package/dist/modules/declared-variables/declared-variables.routes.js.map +1 -0
- package/dist/modules/declared-variables/index.d.ts +2 -0
- package/dist/modules/declared-variables/index.d.ts.map +1 -0
- package/dist/modules/declared-variables/index.js +2 -0
- package/dist/modules/declared-variables/index.js.map +1 -0
- package/dist/modules/diff/diff.routes.d.ts +1 -1
- package/dist/modules/diff/diff.routes.d.ts.map +1 -1
- package/dist/modules/diff/diff.routes.js +3 -3
- package/dist/modules/diff/diff.routes.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts +16 -0
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +20 -0
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/kb-fs/repo-path.d.ts +32 -0
- package/dist/modules/kb-fs/repo-path.d.ts.map +1 -0
- package/dist/modules/kb-fs/repo-path.js +54 -0
- package/dist/modules/kb-fs/repo-path.js.map +1 -0
- package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
- package/dist/modules/secrets-vault/db-secrets-vault.service.js +60 -18
- package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
- package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts +20 -0
- package/dist/modules/secrets-vault/mcp-oauth-discovery.service.d.ts.map +1 -1
- package/dist/modules/secrets-vault/mcp-oauth-discovery.service.js +121 -47
- package/dist/modules/secrets-vault/mcp-oauth-discovery.service.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 +20 -2
- package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
- package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
- package/dist/modules/tool-helpers/tool-context.js +1 -0
- package/dist/modules/tool-helpers/tool-context.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-json-discovery.js +45 -12
- package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.js +2 -1
- package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +40 -8
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +38 -14
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +164 -55
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +10 -5
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-manuals/utcp-cli-parse-only.d.ts +55 -0
- package/dist/modules/tool-manuals/utcp-cli-parse-only.d.ts.map +1 -0
- package/dist/modules/tool-manuals/utcp-cli-parse-only.js +76 -0
- package/dist/modules/tool-manuals/utcp-cli-parse-only.js.map +1 -0
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts +3 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +19 -2
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +32 -1
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +70 -5
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts +3 -3
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +20 -2
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.d.ts +8 -0
- package/dist/modules/workflow/pending-commits.worker.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.worker.js +74 -16
- package/dist/modules/workflow/pending-commits.worker.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +7 -0
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +12 -0
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +1 -0
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +4 -0
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +31 -15
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/kb-template/AGENTS.md +52 -8
- package/package.json +4 -3
- package/src/core/create-core-server.ts +13 -1
- package/src/core/create-core-services.ts +1 -0
- package/src/modules/access/__tests__/access-control.atref-cache.test.ts +260 -0
- package/src/modules/access/__tests__/access-groups.test.ts +28 -0
- package/src/modules/access/__tests__/access-own-read-grant.test.ts +116 -0
- package/src/modules/access/access-control.service.ts +198 -37
- package/src/modules/access/admin-locked-commit.ts +1 -0
- package/src/modules/access/synced-groups-committer.ts +1 -1
- package/src/modules/access-model/__tests__/access-grammar.test.ts +199 -1
- package/src/modules/access-model/access-errors.ts +19 -0
- package/src/modules/access-model/access-grammar.ts +123 -6
- package/src/modules/declared-variables/__tests__/declared-variables.route.test.ts +166 -0
- package/src/modules/declared-variables/declared-variables.routes.ts +151 -0
- package/src/modules/declared-variables/index.ts +1 -0
- package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +4 -4
- package/src/modules/diff/diff.routes.ts +3 -2
- package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +306 -131
- package/src/modules/kb-fs/__tests__/repo-path.test.ts +106 -0
- package/src/modules/kb-fs/locking-filesystem.ts +30 -0
- package/src/modules/kb-fs/repo-path.ts +56 -0
- package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +104 -0
- package/src/modules/secrets-vault/__tests__/mcp-oauth-discovery.service.test.ts +52 -0
- package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +48 -1
- package/src/modules/secrets-vault/db-secrets-vault.service.ts +73 -22
- package/src/modules/secrets-vault/mcp-oauth-discovery.service.ts +141 -50
- package/src/modules/secrets-vault/secrets-vault.routes.ts +600 -582
- package/src/modules/tool-helpers/tool-context.ts +1 -0
- package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +38 -0
- package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +2 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.cli.test.ts +243 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +95 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +17 -1
- package/src/modules/tool-manuals/mcp-json-discovery.ts +39 -15
- package/src/modules/tool-manuals/mcp-server-edit.service.ts +2 -1
- package/src/modules/tool-manuals/tool-manuals.contract.ts +40 -9
- package/src/modules/tool-manuals/tool-manuals.service.ts +156 -28
- package/src/modules/tool-manuals/tool-manuals.tools.ts +10 -5
- package/src/modules/tool-manuals/utcp-cli-parse-only.ts +76 -0
- package/src/modules/workflow/__tests__/pending-commits.worker.test.ts +46 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +60 -1
- package/src/modules/workflow/agent-tools/workflow.tools.ts +18 -1
- package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +56 -0
- package/src/modules/workflow/git/__tests__/git.service.commitFile.strayPath.test.ts +162 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.getPrDetail.test.ts +114 -0
- package/src/modules/workflow/git/git.service.ts +73 -6
- package/src/modules/workflow/git/pull-request.service.ts +22 -4
- package/src/modules/workflow/pending-commits.worker.ts +80 -18
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +60 -0
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +5 -0
- package/src/modules/workflow/workflow.routes.ts +12 -0
- package/src/modules/workflow/workflow.service.ts +5 -1
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +45 -0
- package/src/modules/workspace/workspace.tools.ts +35 -15
|
@@ -13,6 +13,7 @@ import type { WorkspaceService } from '../../../workspace/workspace.service.js';
|
|
|
13
13
|
import type { GitService } from '../../git/git.service.js';
|
|
14
14
|
import type { IAccessControl } from '../../../access/access-control.interface.js';
|
|
15
15
|
import { hashEmail as hash } from '../../../../shared/hash-email.js';
|
|
16
|
+
import { AccessUnreadableError } from '../../../access-model/access-errors.js';
|
|
16
17
|
|
|
17
18
|
function file(overrides: Partial<PullRequestFile>): PullRequestFile {
|
|
18
19
|
return {
|
|
@@ -557,3 +558,62 @@ describe('mergePr — roles.yaml privilege-escalation guard', () => {
|
|
|
557
558
|
expect(mergeChangeRequestMock).toHaveBeenCalledTimes(1);
|
|
558
559
|
});
|
|
559
560
|
});
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* An access tree that could not be READ is not the same as "no eligible
|
|
564
|
+
* writers": the latter drops a file out of the merge gate, so answering it for
|
|
565
|
+
* a lost git read would let a change merge past a deny nobody managed to
|
|
566
|
+
* load. The unreadable case propagates (the caller fails closed, 503); any
|
|
567
|
+
* other lookup failure still degrades to empty eligibility as documented.
|
|
568
|
+
*/
|
|
569
|
+
describe('ReviewWorkflowService.getApprovalStates — unreadable access tree', () => {
|
|
570
|
+
const FILES = [file({ path: 'Knowledge/Foo.md' })];
|
|
571
|
+
function serviceWith(overrides: Partial<IAccessControl>) {
|
|
572
|
+
const workspace = {
|
|
573
|
+
ensureRemotesFetched: vi.fn(async () => undefined),
|
|
574
|
+
getOrCreateForBranch: vi.fn(async () => ({ id: 'ws-base' })),
|
|
575
|
+
} as unknown as WorkspaceService;
|
|
576
|
+
const git = { mergeChangeRequest: mergeChangeRequestMock } as unknown as GitService;
|
|
577
|
+
return new ReviewWorkflowService(makeDb([]), { ...makeAccessControl({}), ...overrides }, workspace, git);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
it('propagates AccessUnreadableError from the eligibility lookup', async () => {
|
|
581
|
+
const svc = serviceWith({
|
|
582
|
+
eligibleWritersForPathsAtRef: async () => {
|
|
583
|
+
throw new AccessUnreadableError('origin/current-company-state', 'Knowledge/access.md');
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
await expect(
|
|
587
|
+
svc.getApprovalStates(1, FILES, 'head-sha-1', 'current-company-state', null, 'ws-base', 'alice@bevel.software'),
|
|
588
|
+
).rejects.toBeInstanceOf(AccessUnreadableError);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('propagates AccessUnreadableError from the viewer batch too', async () => {
|
|
592
|
+
const svc = serviceWith({
|
|
593
|
+
canWriteBatchAtRef: async () => {
|
|
594
|
+
throw new AccessUnreadableError('origin/current-company-state', 'Knowledge/access.md');
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
await expect(
|
|
598
|
+
svc.getApprovalStates(1, FILES, 'head-sha-1', 'current-company-state', null, 'ws-base', 'alice@bevel.software'),
|
|
599
|
+
).rejects.toBeInstanceOf(AccessUnreadableError);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('still degrades any other lookup failure to empty eligibility', async () => {
|
|
603
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
604
|
+
try {
|
|
605
|
+
const svc = serviceWith({
|
|
606
|
+
eligibleWritersForPathsAtRef: async () => {
|
|
607
|
+
throw new Error('origin not fetched yet');
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
const states = await svc.getApprovalStates(1, FILES, 'head-sha-1', 'current-company-state', null, 'ws-base');
|
|
611
|
+
expect(states).toHaveLength(1);
|
|
612
|
+
expect(states[0].eligibleApprovers).toEqual({ roles: [], users: [] });
|
|
613
|
+
expect(states[0].isApproved).toBe(false);
|
|
614
|
+
expect(warn).toHaveBeenCalled();
|
|
615
|
+
} finally {
|
|
616
|
+
warn.mockRestore();
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
});
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
} from '@bevel-software/platform-shared';
|
|
13
13
|
import type { Database } from '../../database/connection.js';
|
|
14
14
|
import { changeRequests, prComments, prFileApprovals, prMergeLog } from '../../database/schema.js';
|
|
15
|
+
import { AccessUnreadableError } from '../../access-model/access-errors.js';
|
|
15
16
|
import type { IAccessControl } from '../../access/access-control.interface.js';
|
|
16
17
|
import { isAccessMdPath } from '../../access-model/access-grammar.js';
|
|
17
18
|
import type { GitService } from '../git/git.service.js';
|
|
@@ -374,6 +375,9 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
|
|
|
374
375
|
);
|
|
375
376
|
if (resolved) eligibilityByPath = resolved;
|
|
376
377
|
} catch (err) {
|
|
378
|
+
// An unreadable tree is not "no eligible writers": that answer would
|
|
379
|
+
// drop every file out of the merge gate. Fail closed instead.
|
|
380
|
+
if (err instanceof AccessUnreadableError) throw err;
|
|
377
381
|
console.warn(
|
|
378
382
|
`[review-workflow] eligibleWritersForPathsAtRef failed for PR #${prNumber} (base=${baseBranch}):`,
|
|
379
383
|
err,
|
|
@@ -390,6 +394,7 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
|
|
|
390
394
|
);
|
|
391
395
|
if (batch) viewerCanApproveByPath = batch;
|
|
392
396
|
} catch (err) {
|
|
397
|
+
if (err instanceof AccessUnreadableError) throw err;
|
|
393
398
|
console.warn(
|
|
394
399
|
`[review-workflow] canWriteBatchAtRef failed for PR #${prNumber} viewer=${viewerEmail}:`,
|
|
395
400
|
err,
|
|
@@ -764,6 +764,10 @@ export function createWorkflowRoutes(
|
|
|
764
764
|
fresh: true,
|
|
765
765
|
workspaceId: workspace.id,
|
|
766
766
|
viewerEmail: user.email,
|
|
767
|
+
// Internal read: this route pins its work on the detail and never
|
|
768
|
+
// reads a patch; skipping them is one git subprocess per file saved
|
|
769
|
+
// on every click, and the result stays out of the client cache.
|
|
770
|
+
patches: false,
|
|
767
771
|
});
|
|
768
772
|
if (!detail) {
|
|
769
773
|
res.status(404).json({ error: 'change request not found' });
|
|
@@ -805,6 +809,10 @@ export function createWorkflowRoutes(
|
|
|
805
809
|
fresh: true,
|
|
806
810
|
workspaceId: workspace.id,
|
|
807
811
|
viewerEmail: user.email,
|
|
812
|
+
// Internal read: this route pins its work on the detail and never
|
|
813
|
+
// reads a patch; skipping them is one git subprocess per file saved
|
|
814
|
+
// on every click, and the result stays out of the client cache.
|
|
815
|
+
patches: false,
|
|
808
816
|
});
|
|
809
817
|
if (!detail) {
|
|
810
818
|
res.status(404).json({ error: 'change request not found' });
|
|
@@ -841,6 +849,10 @@ export function createWorkflowRoutes(
|
|
|
841
849
|
fresh: true,
|
|
842
850
|
workspaceId: workspace.id,
|
|
843
851
|
viewerEmail: user.email,
|
|
852
|
+
// Internal read: this route pins its work on the detail and never
|
|
853
|
+
// reads a patch; skipping them is one git subprocess per file saved
|
|
854
|
+
// on every click, and the result stays out of the client cache.
|
|
855
|
+
patches: false,
|
|
844
856
|
});
|
|
845
857
|
if (!detail) {
|
|
846
858
|
res.status(404).json({ error: 'change request not found' });
|
|
@@ -1241,7 +1241,7 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1241
1241
|
|
|
1242
1242
|
getChangeRequestDetail(
|
|
1243
1243
|
number: number,
|
|
1244
|
-
opts?: { fresh?: boolean; workspaceId?: string; viewerEmail?: string },
|
|
1244
|
+
opts?: { fresh?: boolean; workspaceId?: string; viewerEmail?: string; patches?: boolean },
|
|
1245
1245
|
): Promise<ChangeRequestDetail | null> {
|
|
1246
1246
|
return this.prs.getPrDetail(number, opts);
|
|
1247
1247
|
}
|
|
@@ -1896,6 +1896,10 @@ export class WorkflowService implements IWorkflowService {
|
|
|
1896
1896
|
fresh: true,
|
|
1897
1897
|
workspaceId,
|
|
1898
1898
|
viewerEmail: user.email,
|
|
1899
|
+
// Only `files[].path` is read below; a patch per file would be one
|
|
1900
|
+
// git subprocess each, generated for nothing, on every reject by a
|
|
1901
|
+
// non-author.
|
|
1902
|
+
patches: false,
|
|
1899
1903
|
});
|
|
1900
1904
|
const paths = detail?.files.map((f) => f.path) ?? [];
|
|
1901
1905
|
if (paths.length > 0) {
|
|
@@ -1065,3 +1065,48 @@ describe('branch is a required parameter in the tool contract', () => {
|
|
|
1065
1065
|
}
|
|
1066
1066
|
});
|
|
1067
1067
|
});
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* The tools are rooted at the WORKSPACE dir, one level above the git clone, so
|
|
1071
|
+
* a path has to start with the clone folder to reach git at all. Every place an
|
|
1072
|
+
* agent reads before choosing a path says so, in the `path` input itself rather
|
|
1073
|
+
* than only in prose it may not read: the root listing (where the folder is
|
|
1074
|
+
* discoverable) and each content-writing tool.
|
|
1075
|
+
*/
|
|
1076
|
+
describe('path inputs tell the agent about the repository folder', () => {
|
|
1077
|
+
// `toolDef` wraps a tool's inputs under a single `body` property.
|
|
1078
|
+
const inputDescription = (def: { inputs?: unknown } | undefined, ...keys: string[]): string => {
|
|
1079
|
+
let node = (def?.inputs ?? {}) as Record<string, unknown>;
|
|
1080
|
+
for (const key of ['body', ...keys]) {
|
|
1081
|
+
node = ((node.properties as Record<string, unknown> | undefined)?.[key] ?? {}) as Record<string, unknown>;
|
|
1082
|
+
if (key === 'files') node = (node.items ?? {}) as Record<string, unknown>;
|
|
1083
|
+
}
|
|
1084
|
+
return typeof node.description === 'string' ? node.description : '';
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
it('every content-writing tool says paths start with `knowledge-base/`, so a repo-relative path is never guessed', async () => {
|
|
1088
|
+
await start();
|
|
1089
|
+
const tools = await toolRegistry.listInternal();
|
|
1090
|
+
const byName = (name: string) => {
|
|
1091
|
+
const def = tools.find((t) => t.name === name);
|
|
1092
|
+
expect(def, name).toBeDefined();
|
|
1093
|
+
return def;
|
|
1094
|
+
};
|
|
1095
|
+
for (const name of ['write_file', 'edit_file', 'mkdir', 'delete_file', 'unzip']) {
|
|
1096
|
+
expect(inputDescription(byName(name), 'path'), name).toContain(`\`${KB_DIR}/\``);
|
|
1097
|
+
}
|
|
1098
|
+
expect(inputDescription(byName('write_files'), 'files', 'path'), 'write_files').toContain(`\`${KB_DIR}/\``);
|
|
1099
|
+
for (const name of ['move_file', 'copy_file']) {
|
|
1100
|
+
expect(inputDescription(byName(name), 'dest'), name).toContain(`\`${KB_DIR}/\``);
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it('the root listing names the clone folder, so an agent that lists first learns the prefix', async () => {
|
|
1105
|
+
await start();
|
|
1106
|
+
const tools = await toolRegistry.listInternal();
|
|
1107
|
+
const list = tools.find((t) => t.name === 'list_files');
|
|
1108
|
+
expect(list).toBeDefined();
|
|
1109
|
+
expect(list!.description).toContain(`\`${KB_DIR}/\``);
|
|
1110
|
+
expect(inputDescription(list, 'path')).toContain(`\`${KB_DIR}/\``);
|
|
1111
|
+
});
|
|
1112
|
+
});
|
|
@@ -20,6 +20,7 @@ import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
|
20
20
|
// Leaf-level shared primitive (same exception `workspace.service.ts` already
|
|
21
21
|
// relies on) — not a workflow service, so this stays inside the module boundary.
|
|
22
22
|
import { assertValidBranchName } from '../kb-fs/branch-name.js';
|
|
23
|
+
import { assertInsideRepo } from '../kb-fs/repo-path.js';
|
|
23
24
|
import type { ISessionSink } from './session-sink.js';
|
|
24
25
|
import type { IAccessControl } from '../access/access-control.interface.js';
|
|
25
26
|
import { toKbRelative, resolveReadableMap } from '../access-model/kb-read-filter.js';
|
|
@@ -111,6 +112,22 @@ const int = (description: string): JsonSchema => ({ type: 'integer', description
|
|
|
111
112
|
|
|
112
113
|
const str = (description: string): JsonSchema => ({ type: 'string', description });
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* A path input that names the clone folder. The tools are rooted at the
|
|
117
|
+
* WORKSPACE dir, one level above the git clone, so a path only reaches git
|
|
118
|
+
* when it starts with that folder; an agent that reads `KnowledgeBase/Foo.md`
|
|
119
|
+
* in a URL or a doc and passes it verbatim would otherwise write beside the
|
|
120
|
+
* repository. Saying so in the input itself, not only in prose, is what the
|
|
121
|
+
* agent actually sees when it fills the argument. `refused` is false for the
|
|
122
|
+
* inputs that may legitimately name a stray (a source to rescue, a file to
|
|
123
|
+
* remove).
|
|
124
|
+
*/
|
|
125
|
+
const wsPath = (kbDirName: string, what: string, refused = true): JsonSchema =>
|
|
126
|
+
str(
|
|
127
|
+
`${what}: starts with \`${kbDirName}/\` (e.g. \`${kbDirName}/KnowledgeBase/Foo.md\`).` +
|
|
128
|
+
(refused ? ' A path without that prefix is outside the repository and is refused.' : ''),
|
|
129
|
+
);
|
|
130
|
+
|
|
114
131
|
function asText(content: string | Buffer): string {
|
|
115
132
|
return typeof content === 'string' ? content : content.toString('utf8');
|
|
116
133
|
}
|
|
@@ -394,7 +411,7 @@ export function registerWorkspaceTools(
|
|
|
394
411
|
type: 'object',
|
|
395
412
|
properties: {
|
|
396
413
|
branch: BRANCH_INPUT,
|
|
397
|
-
path: str(
|
|
414
|
+
path: str(`Path to read, starting with \`${kbDirName}/\` (e.g. \`${kbDirName}/KnowledgeBase/Foo.md\`), or a \`__tool_chain_spill__/…\` ref from a truncated \`call_tool_chain\`.`),
|
|
398
415
|
offset: int('Start character index (default 0).'),
|
|
399
416
|
limit: int('Max characters to return from `offset`.'),
|
|
400
417
|
sessionId: SESSION_ID_INPUT,
|
|
@@ -453,13 +470,13 @@ export function registerWorkspaceTools(
|
|
|
453
470
|
mount({
|
|
454
471
|
name: 'list_files',
|
|
455
472
|
description:
|
|
456
|
-
|
|
473
|
+
`List a directory. Returns \`{ path, entries: [{ name, type, size? }] }\`. Omit \`path\` for the workspace root, which holds the repository as the \`${kbDirName}/\` folder: every content path starts with it (e.g. \`${kbDirName}/KnowledgeBase\`).` +
|
|
457
474
|
ONTOLOGY_BOUNDARY_NOTE,
|
|
458
475
|
inputs: {
|
|
459
476
|
type: 'object',
|
|
460
477
|
properties: {
|
|
461
478
|
branch: BRANCH_INPUT,
|
|
462
|
-
path: str(
|
|
479
|
+
path: str(`Directory to list, starting with \`${kbDirName}/\` (default: the workspace root, where the repository is the \`${kbDirName}/\` folder).`),
|
|
463
480
|
sessionId: SESSION_ID_INPUT,
|
|
464
481
|
},
|
|
465
482
|
required: ['branch'],
|
|
@@ -501,7 +518,7 @@ export function registerWorkspaceTools(
|
|
|
501
518
|
type: 'object',
|
|
502
519
|
properties: {
|
|
503
520
|
branch: BRANCH_INPUT,
|
|
504
|
-
path:
|
|
521
|
+
path: wsPath(kbDirName, 'Path'),
|
|
505
522
|
sessionId: SESSION_ID_INPUT,
|
|
506
523
|
},
|
|
507
524
|
required: ['branch', 'path'],
|
|
@@ -610,7 +627,7 @@ export function registerWorkspaceTools(
|
|
|
610
627
|
type: 'object',
|
|
611
628
|
properties: {
|
|
612
629
|
branch: BRANCH_INPUT,
|
|
613
|
-
path:
|
|
630
|
+
path: wsPath(kbDirName, 'Path'),
|
|
614
631
|
content: str('Full file content.'),
|
|
615
632
|
sessionId: SESSION_ID_INPUT,
|
|
616
633
|
},
|
|
@@ -657,7 +674,7 @@ export function registerWorkspaceTools(
|
|
|
657
674
|
description: 'Files to write; each created or overwritten.',
|
|
658
675
|
items: {
|
|
659
676
|
type: 'object',
|
|
660
|
-
properties: { path:
|
|
677
|
+
properties: { path: wsPath(kbDirName, 'Path'), content: str('Full file content.') },
|
|
661
678
|
required: ['path', 'content'],
|
|
662
679
|
additionalProperties: false,
|
|
663
680
|
},
|
|
@@ -705,7 +722,7 @@ export function registerWorkspaceTools(
|
|
|
705
722
|
type: 'object',
|
|
706
723
|
properties: {
|
|
707
724
|
branch: BRANCH_INPUT,
|
|
708
|
-
path:
|
|
725
|
+
path: wsPath(kbDirName, 'Path'),
|
|
709
726
|
old_string: str('Exact text to replace (include enough context to be unique).'),
|
|
710
727
|
new_string: str('Replacement text.'),
|
|
711
728
|
replace_all: { type: 'boolean', description: 'Replace every occurrence instead of requiring a unique match.' },
|
|
@@ -750,7 +767,7 @@ export function registerWorkspaceTools(
|
|
|
750
767
|
type: 'object',
|
|
751
768
|
properties: {
|
|
752
769
|
branch: BRANCH_INPUT,
|
|
753
|
-
path:
|
|
770
|
+
path: wsPath(kbDirName, 'Path to the file', false),
|
|
754
771
|
sessionId: SESSION_ID_INPUT,
|
|
755
772
|
},
|
|
756
773
|
required: ['branch', 'path'],
|
|
@@ -781,7 +798,7 @@ export function registerWorkspaceTools(
|
|
|
781
798
|
type: 'object',
|
|
782
799
|
properties: {
|
|
783
800
|
branch: BRANCH_INPUT,
|
|
784
|
-
path:
|
|
801
|
+
path: wsPath(kbDirName, 'Directory to create'),
|
|
785
802
|
sessionId: SESSION_ID_INPUT,
|
|
786
803
|
},
|
|
787
804
|
required: ['branch', 'path'],
|
|
@@ -808,8 +825,8 @@ export function registerWorkspaceTools(
|
|
|
808
825
|
type: 'object',
|
|
809
826
|
properties: {
|
|
810
827
|
branch: BRANCH_INPUT,
|
|
811
|
-
src:
|
|
812
|
-
dest:
|
|
828
|
+
src: wsPath(kbDirName, 'Source path', false),
|
|
829
|
+
dest: wsPath(kbDirName, 'Destination path'),
|
|
813
830
|
sessionId: SESSION_ID_INPUT,
|
|
814
831
|
},
|
|
815
832
|
required: ['branch', 'src', 'dest'],
|
|
@@ -842,8 +859,8 @@ export function registerWorkspaceTools(
|
|
|
842
859
|
type: 'object',
|
|
843
860
|
properties: {
|
|
844
861
|
branch: BRANCH_INPUT,
|
|
845
|
-
src:
|
|
846
|
-
dest:
|
|
862
|
+
src: wsPath(kbDirName, 'Source path', false),
|
|
863
|
+
dest: wsPath(kbDirName, 'Destination path'),
|
|
847
864
|
sessionId: SESSION_ID_INPUT,
|
|
848
865
|
},
|
|
849
866
|
required: ['branch', 'src', 'dest'],
|
|
@@ -877,8 +894,8 @@ export function registerWorkspaceTools(
|
|
|
877
894
|
type: 'object',
|
|
878
895
|
properties: {
|
|
879
896
|
branch: BRANCH_INPUT,
|
|
880
|
-
path:
|
|
881
|
-
destination:
|
|
897
|
+
path: wsPath(kbDirName, 'Path to the .zip archive', false),
|
|
898
|
+
destination: wsPath(kbDirName, "Directory to extract into (default: the zip's parent)"),
|
|
882
899
|
sessionId: SESSION_ID_INPUT,
|
|
883
900
|
},
|
|
884
901
|
required: ['branch', 'path'],
|
|
@@ -916,6 +933,9 @@ export function registerWorkspaceTools(
|
|
|
916
933
|
// extension policy applies per entry too, so a restricted run can't unzip a
|
|
917
934
|
// `.md` into the graph.
|
|
918
935
|
(wsRelPath) => {
|
|
936
|
+
// An entry that would land beside the repository is skipped with the
|
|
937
|
+
// corrected-path reason, like any other refused entry.
|
|
938
|
+
assertInsideRepo(wsRelPath, kbDirName);
|
|
919
939
|
writePolicy.assertPathWritable(ctx.sessionId, wsRelPath);
|
|
920
940
|
return assertOntologyWriteAllowed(sessionOntologyGate, ctx, wsRelPath);
|
|
921
941
|
},
|