@bevel-software/platform-core-backend 0.28.0 → 0.29.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 +15 -0
- package/agent-guide/skills.md +8 -0
- package/dist/core/catalog-revision.d.ts.map +1 -1
- package/dist/core/catalog-revision.js +4 -2
- package/dist/core/catalog-revision.js.map +1 -1
- package/dist/core/compose-embed.d.ts +35 -0
- package/dist/core/compose-embed.d.ts.map +1 -0
- package/dist/core/compose-embed.js +14 -0
- package/dist/core/compose-embed.js.map +1 -0
- package/dist/core/core-ports.d.ts +10 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +7 -1
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +1 -1
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +25 -14
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +11 -6
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +13 -6
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +5 -2
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +36 -14
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +208 -85
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/auth/index.d.ts +15 -0
- package/dist/modules/auth/index.d.ts.map +1 -0
- package/dist/modules/auth/index.js +20 -0
- package/dist/modules/auth/index.js.map +1 -0
- package/dist/modules/embed/embed.interface.d.ts +22 -0
- package/dist/modules/embed/embed.interface.d.ts.map +1 -1
- package/dist/modules/embed/embed.service.d.ts +28 -14
- package/dist/modules/embed/embed.service.d.ts.map +1 -1
- package/dist/modules/embed/embed.service.js +38 -17
- package/dist/modules/embed/embed.service.js.map +1 -1
- package/dist/modules/embed/embed.tools.d.ts.map +1 -1
- package/dist/modules/embed/embed.tools.js +10 -2
- package/dist/modules/embed/embed.tools.js.map +1 -1
- package/dist/modules/kb-fs/index.d.ts +7 -0
- package/dist/modules/kb-fs/index.d.ts.map +1 -0
- package/dist/modules/kb-fs/index.js +2 -0
- package/dist/modules/kb-fs/index.js.map +1 -0
- package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -1
- package/dist/modules/plugins/join-request-jobs.service.js +43 -8
- package/dist/modules/plugins/join-request-jobs.service.js.map +1 -1
- package/dist/modules/skills/skills.contract.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts +8 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +115 -11
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-helpers/default-branch-write.d.ts +69 -0
- package/dist/modules/tool-helpers/default-branch-write.d.ts.map +1 -0
- package/dist/modules/tool-helpers/default-branch-write.js +63 -0
- package/dist/modules/tool-helpers/default-branch-write.js.map +1 -0
- package/dist/modules/tool-helpers/index.d.ts +1 -0
- package/dist/modules/tool-helpers/index.d.ts.map +1 -1
- package/dist/modules/tool-helpers/index.js +1 -0
- package/dist/modules/tool-helpers/index.js.map +1 -1
- package/dist/modules/workflow/file-lock.service.d.ts +27 -5
- package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
- package/dist/modules/workflow/file-lock.service.js +70 -18
- package/dist/modules/workflow/file-lock.service.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +13 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +24 -0
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/index.d.ts +8 -0
- package/dist/modules/workflow/index.d.ts.map +1 -0
- package/dist/modules/workflow/index.js +8 -0
- package/dist/modules/workflow/index.js.map +1 -0
- package/dist/modules/workflow/pending-commits.service.d.ts +2 -17
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +4 -24
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +20 -1
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +5 -3
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/index.d.ts +10 -0
- package/dist/modules/workspace/index.d.ts.map +1 -0
- package/dist/modules/workspace/index.js +10 -0
- package/dist/modules/workspace/index.js.map +1 -0
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +106 -20
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/workspace-id.d.ts +17 -0
- package/dist/shared/workspace-id.d.ts.map +1 -1
- package/dist/shared/workspace-id.js +24 -0
- package/dist/shared/workspace-id.js.map +1 -1
- package/kb-template/access.md +2 -1
- package/mcp-app/page.html +142 -8
- package/package.json +3 -3
- package/src/core/__tests__/compose-embed.test.ts +65 -0
- package/src/core/__tests__/lifecycle.test.ts +29 -26
- package/src/core/catalog-revision.ts +4 -2
- package/src/core/compose-embed.ts +50 -0
- package/src/core/core-ports.ts +10 -0
- package/src/core/create-core-server.ts +7 -1
- package/src/core/create-core-services.ts +27 -21
- package/src/core/lifecycle.ts +24 -12
- package/src/modules/access/__tests__/access-control.admin-root-floor.test.ts +23 -3
- package/src/modules/access/__tests__/access-control.atref-cache.test.ts +303 -25
- package/src/modules/access/__tests__/access-control.download-read.test.ts +3 -1
- package/src/modules/access/__tests__/access-control.service.test.ts +77 -16
- package/src/modules/access/__tests__/change-read-gate.test.ts +36 -5
- package/src/modules/access/access-control.interface.ts +5 -2
- package/src/modules/access/access-control.service.ts +236 -95
- package/src/modules/auth/index.ts +29 -0
- package/src/modules/embed/__tests__/embed.service.test.ts +76 -3
- package/src/modules/embed/__tests__/embed.tools.test.ts +29 -0
- package/src/modules/embed/embed.interface.ts +24 -0
- package/src/modules/embed/embed.service.ts +44 -19
- package/src/modules/embed/embed.tools.ts +10 -2
- package/src/modules/kb-fs/index.ts +6 -0
- package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +73 -6
- package/src/modules/plugins/join-request-jobs.service.ts +45 -8
- package/src/modules/skills/__tests__/branch-skills.tools.test.ts +1 -1
- package/src/modules/skills/skills.contract.ts +4 -0
- package/src/modules/skills/skills.tools.ts +120 -10
- package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +581 -0
- package/src/modules/tool-helpers/default-branch-write.ts +90 -0
- package/src/modules/tool-helpers/index.ts +5 -0
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +36 -2
- package/src/modules/workflow/__tests__/fake-file-lock-db.ts +13 -2
- package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +168 -1
- package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +102 -6
- package/src/modules/workflow/file-lock.service.ts +79 -18
- package/src/modules/workflow/git/__tests__/git.service.readFileAtRef.test.ts +42 -0
- package/src/modules/workflow/git/git.service.ts +23 -0
- package/src/modules/workflow/index.ts +7 -0
- package/src/modules/workflow/pending-commits.service.ts +4 -23
- package/src/modules/workflow/workflow.routes.ts +24 -1
- package/src/modules/workflow/workflow.service.ts +3 -2
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +21 -11
- package/src/modules/workspace/index.ts +14 -0
- package/src/modules/workspace/workspace.tools.ts +120 -20
- package/src/shared/workspace-id.ts +24 -0
|
@@ -5,6 +5,7 @@ import AdmZip from 'adm-zip';
|
|
|
5
5
|
import { hasHttpStatus, ToolError } from '../tool-helpers/tool.contract.js';
|
|
6
6
|
import { BRANCH_INPUT, toolDef } from '../tool-helpers/tool-def.js';
|
|
7
7
|
import { notifyAgentRead, assertAgentWriteAllowed, SESSION_ID_INPUT, } from './agent-access.gate.js';
|
|
8
|
+
import { CAN_WRITE_CLAUSE, defaultBranchWriteVerdicts } from '../tool-helpers/index.js';
|
|
8
9
|
import { requireInternalSource, requireExternalSource } from '../tool-auth/tool-auth.middleware.js';
|
|
9
10
|
import { workspaceIdForBranch } from '../../shared/workspace-id.js';
|
|
10
11
|
import { GitInternalsError, WorkflowValidationError } from '../../shared/domain-errors.js';
|
|
@@ -705,6 +706,23 @@ disk) {
|
|
|
705
706
|
});
|
|
706
707
|
/** A repo-relative path in the workspace-relative form every tool speaks. */
|
|
707
708
|
const toWs = (rel) => (rel ? `${kbDirName}/${rel}` : kbDirName);
|
|
709
|
+
/**
|
|
710
|
+
* `canWrite` for workspace paths, as `list_files` and `read_file` report it:
|
|
711
|
+
* whether the caller may commit a change there directly on the DEFAULT
|
|
712
|
+
* branch, whichever branch the call read — one batch per call (see
|
|
713
|
+
* `defaultBranchWriteVerdicts`). The repository folder itself is judged as
|
|
714
|
+
* the repository root; a path outside the repository never lands on the
|
|
715
|
+
* default branch, so it is `false`.
|
|
716
|
+
*/
|
|
717
|
+
const canWriteAt = async (ctx, wsPaths) => {
|
|
718
|
+
const relOf = (p) => {
|
|
719
|
+
const norm = p.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
720
|
+
return norm === kbDirName ? '' : toKbRelative(norm, kbDirName);
|
|
721
|
+
};
|
|
722
|
+
const rels = wsPaths.map(relOf);
|
|
723
|
+
const verdicts = await defaultBranchWriteVerdicts({ accessControl, defaultWorkspaceId: () => kb.defaultWorkspaceId() }, ctx, rels.filter((r) => r !== null));
|
|
724
|
+
return new Map(wsPaths.map((p, i) => [p, rels[i] !== null && verdicts.get(rels[i]) === true]));
|
|
725
|
+
};
|
|
708
726
|
const sourceToWs = (s) => (s ? { ...s, path: toWs(s.path) } : null);
|
|
709
727
|
/**
|
|
710
728
|
* What `file_stat` adds to its `access` verdicts when asked to explain them:
|
|
@@ -1467,7 +1485,14 @@ disk) {
|
|
|
1467
1485
|
* hook, the extraction and the not-found message all have to match, and a
|
|
1468
1486
|
* copy drifts on the first change to any of them.
|
|
1469
1487
|
*/
|
|
1470
|
-
const readForTool = async (branch, p, ctx) =>
|
|
1488
|
+
const readForTool = async (branch, p, ctx) => (await readServed(branch, p, ctx)).result;
|
|
1489
|
+
/**
|
|
1490
|
+
* {@link readForTool}, also saying, at the guide's name, whether a file of
|
|
1491
|
+
* the knowledge base's own answered first on THIS branch (`guide`: `own`)
|
|
1492
|
+
* or the platform's guide answered alone (`alone`); `null` for any other
|
|
1493
|
+
* path. `read_file` needs it to judge `canWrite` there (see its handler).
|
|
1494
|
+
*/
|
|
1495
|
+
const readServed = async (branch, p, ctx) => {
|
|
1471
1496
|
// The guide's name at the repository root answers with the platform's
|
|
1472
1497
|
// guide, which is text the code owns and every agent may read: no gate
|
|
1473
1498
|
// and no read hook for it. A file the knowledge base keeps under that
|
|
@@ -1476,7 +1501,8 @@ disk) {
|
|
|
1476
1501
|
// guide an earlier release wrote to disk (still on a draft, say) is
|
|
1477
1502
|
// recognised by its header and not served a second time.
|
|
1478
1503
|
if (agentGuide && isAgentGuidePath(toKbRelative(p, kbDirName) ?? '')) {
|
|
1479
|
-
|
|
1504
|
+
const { text, own } = await guideServed(branch, ctx, p);
|
|
1505
|
+
return { result: { kind: 'text', text }, guide: own ? 'own' : 'alone' };
|
|
1480
1506
|
}
|
|
1481
1507
|
await notifyAgentRead(agentAccessGate, ctx, branch, p);
|
|
1482
1508
|
await assertCanRead(readGateFor(branch, ctx), p);
|
|
@@ -1485,18 +1511,38 @@ disk) {
|
|
|
1485
1511
|
// the access gate and the read hook above — a document read is still a
|
|
1486
1512
|
// KB read. ONE registry dispatch picks the reader by extension.
|
|
1487
1513
|
const bytes = await orNotFound(p, async () => asBytes(await fs.readFile(p)));
|
|
1488
|
-
return readers.readerFor(p).read(bytes, p);
|
|
1514
|
+
return { result: await readers.readerFor(p).read(bytes, p), guide: null };
|
|
1515
|
+
};
|
|
1516
|
+
/**
|
|
1517
|
+
* Whether the DEFAULT branch has a file of the knowledge base's own at the
|
|
1518
|
+
* guide's name that this caller may read — not a copy of the platform's
|
|
1519
|
+
* guide an earlier release wrote. Telling the two apart reads the file, so
|
|
1520
|
+
* that branch's read hook is asked first, as {@link ownGuideFile} does —
|
|
1521
|
+
* after the gate, before the bytes. A refusal, like any failure, is
|
|
1522
|
+
* `false`, so `canWrite` fails closed.
|
|
1523
|
+
*/
|
|
1524
|
+
const ownGuideOnDefault = async (ctx, p) => {
|
|
1525
|
+
try {
|
|
1526
|
+
const fs = await ctx.getFilesystem(kb.defaultBranch);
|
|
1527
|
+
if (!(await ownGuideReadable(fs, kb.defaultBranch, ctx, p)))
|
|
1528
|
+
return false;
|
|
1529
|
+
await notifyAgentRead(agentAccessGate, ctx, kb.defaultBranch, p);
|
|
1530
|
+
return await isOwnEntryStill(fs, p);
|
|
1531
|
+
}
|
|
1532
|
+
catch {
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1489
1535
|
};
|
|
1490
1536
|
mount({
|
|
1491
1537
|
name: 'read_file',
|
|
1492
1538
|
gated: true,
|
|
1493
|
-
description: 'Read a workspace file as text. Returns `{ path, content }`.
|
|
1494
|
-
'
|
|
1495
|
-
'
|
|
1496
|
-
'
|
|
1497
|
-
'for
|
|
1498
|
-
'
|
|
1499
|
-
|
|
1539
|
+
description: 'Read a workspace file as text. Returns `{ path, content, canWrite }`. Per the shared content rule: text as ' +
|
|
1540
|
+
'text, documents and email as extracted text, an image as the picture, anything else as a one-line ' +
|
|
1541
|
+
'description. Also reads a `__tool_chain_spill__/…` ref back from a truncated `call_tool_chain`: it ' +
|
|
1542
|
+
'belongs to no workspace, so `branch` is ignored and no `canWrite` comes back. `offset`/`limit` slice the ' +
|
|
1543
|
+
'content (characters; bytes for a spill ref; ignored for an image): page through large files and ' +
|
|
1544
|
+
'spills, never read multi-MB whole. `canWrite`: may you change this file directly on the default branch. ' +
|
|
1545
|
+
CAN_WRITE_CLAUSE,
|
|
1500
1546
|
inputs: {
|
|
1501
1547
|
type: 'object',
|
|
1502
1548
|
properties: {
|
|
@@ -1511,7 +1557,15 @@ disk) {
|
|
|
1511
1557
|
},
|
|
1512
1558
|
outputs: {
|
|
1513
1559
|
type: 'object',
|
|
1514
|
-
properties: {
|
|
1560
|
+
properties: {
|
|
1561
|
+
path: str('The path that was read (echoes the input).'),
|
|
1562
|
+
content: str('File (or spill) content, sliced if offset/limit were given.'),
|
|
1563
|
+
canWrite: {
|
|
1564
|
+
type: 'boolean',
|
|
1565
|
+
description: 'Whether you may commit a change to this file directly on the default branch, under that branch\'s access ' +
|
|
1566
|
+
'rules — the same answer whichever branch you read. Absent for a spill ref.',
|
|
1567
|
+
},
|
|
1568
|
+
},
|
|
1515
1569
|
required: ['path', 'content'],
|
|
1516
1570
|
},
|
|
1517
1571
|
write: false,
|
|
@@ -1528,7 +1582,18 @@ disk) {
|
|
|
1528
1582
|
? content.slice(start, limit !== undefined ? start + limit : undefined)
|
|
1529
1583
|
: content;
|
|
1530
1584
|
};
|
|
1531
|
-
const
|
|
1585
|
+
const branch = a.branch;
|
|
1586
|
+
const { result, guide } = await readServed(branch, p, ctx);
|
|
1587
|
+
// Judged after the read, so a path the caller may not read is refused
|
|
1588
|
+
// before anything is said about writing it. At the guide's name this
|
|
1589
|
+
// answers for the knowledge base's own file there, as `file_stat` does:
|
|
1590
|
+
// where the platform's guide stands alone there is nothing to change —
|
|
1591
|
+
// `false`, whether nothing is there or a file the caller may not read.
|
|
1592
|
+
// Like every verdict here, that is decided on the DEFAULT branch, not
|
|
1593
|
+
// the one read: a draft that deleted or added the file must not move
|
|
1594
|
+
// the answer about what can land directly.
|
|
1595
|
+
const ownOnDefault = guide === null || (branch === kb.defaultBranch ? guide === 'own' : await ownGuideOnDefault(ctx, p));
|
|
1596
|
+
const canWrite = ownOnDefault && (await canWriteAt(ctx, [p])).get(p) === true;
|
|
1532
1597
|
// Images return the picture itself as an MCP image content block, so a
|
|
1533
1598
|
// multimodal model SEES it. The handler returns the `McpImageResult`
|
|
1534
1599
|
// sentinel; the MCP result shaping (`toCallToolResult` in
|
|
@@ -1540,14 +1605,17 @@ disk) {
|
|
|
1540
1605
|
// content blocks before any client could try to validate it, so the
|
|
1541
1606
|
// schema keeps describing the text path it has always described.
|
|
1542
1607
|
// `offset`/`limit` are meaningless on a picture and are ignored.
|
|
1608
|
+
// The picture's answer has no fields beside it, so its `canWrite` rides
|
|
1609
|
+
// in the text note that accompanies it.
|
|
1543
1610
|
if (result.kind === 'image') {
|
|
1544
|
-
|
|
1611
|
+
const verdict = `canWrite: ${canWrite}`;
|
|
1612
|
+
return mcpImageResult(result.data, result.mimeType, result.note ? `${result.note} (${verdict})` : verdict);
|
|
1545
1613
|
}
|
|
1546
1614
|
// Text and refusals alike land in `content` — a refusal (corrupt
|
|
1547
1615
|
// document, unreadable binary, oversized image) IS the file's honest
|
|
1548
1616
|
// textual answer, sliced like any other content.
|
|
1549
1617
|
const content = result.kind === 'text' ? result.text : result.message;
|
|
1550
|
-
return { path: p, content: slice(content) };
|
|
1618
|
+
return { path: p, content: slice(content), canWrite };
|
|
1551
1619
|
},
|
|
1552
1620
|
});
|
|
1553
1621
|
/**
|
|
@@ -1566,10 +1634,12 @@ disk) {
|
|
|
1566
1634
|
* node is indistinguishable from an absent one on every other read.
|
|
1567
1635
|
*/
|
|
1568
1636
|
/** What a read of the guide's path answers: the guide, after the knowledge base's own readable file when it has one. */
|
|
1569
|
-
const guideAt = async (branch, ctx, p) =>
|
|
1637
|
+
const guideAt = async (branch, ctx, p) => (await guideServed(branch, ctx, p)).text;
|
|
1638
|
+
/** {@link guideAt}, also saying whether a file of the knowledge base's own came first. */
|
|
1639
|
+
const guideServed = async (branch, ctx, p) => {
|
|
1570
1640
|
const guide = await agentGuide();
|
|
1571
1641
|
const own = await ownGuideFile(branch, ctx, p);
|
|
1572
|
-
return own === null ? guide : withPlatformGuideAppended(own, guide);
|
|
1642
|
+
return own === null ? { text: guide, own: false } : { text: withPlatformGuideAppended(own, guide), own: true };
|
|
1573
1643
|
};
|
|
1574
1644
|
const ownGuideFile = async (branch, ctx, p) => {
|
|
1575
1645
|
const fs = await ctx.getFilesystem(branch);
|
|
@@ -1646,7 +1716,8 @@ disk) {
|
|
|
1646
1716
|
mount({
|
|
1647
1717
|
name: 'list_files',
|
|
1648
1718
|
gated: true,
|
|
1649
|
-
description: `List a directory. Returns \`{ path, entries: [{ name, type, size
|
|
1719
|
+
description: `List a directory. Returns \`{ path, entries: [{ name, type, size?, canWrite }] }\`. Omit \`path\` for the workspace root, which holds the repository as the \`${kbDirName}/\` folder: every content path is under it (e.g. \`${kbDirName}/KnowledgeBase\`), and a path given without that prefix is placed under it. ` +
|
|
1720
|
+
`\`canWrite\`: may you change that file, or add a file directly inside that folder, directly on the default branch. ${CAN_WRITE_CLAUSE}`,
|
|
1650
1721
|
inputs: {
|
|
1651
1722
|
type: 'object',
|
|
1652
1723
|
properties: {
|
|
@@ -1666,8 +1737,18 @@ disk) {
|
|
|
1666
1737
|
description: 'Directory entries.',
|
|
1667
1738
|
items: {
|
|
1668
1739
|
type: 'object',
|
|
1669
|
-
properties: {
|
|
1670
|
-
|
|
1740
|
+
properties: {
|
|
1741
|
+
name: str('Entry name.'),
|
|
1742
|
+
type: str('`file` or `directory`.'),
|
|
1743
|
+
size: int('Size in bytes (files only).'),
|
|
1744
|
+
canWrite: {
|
|
1745
|
+
type: 'boolean',
|
|
1746
|
+
description: 'Whether you may commit directly on the default branch, under that branch\'s access rules: for a ' +
|
|
1747
|
+
'file, a change to it; for a folder, a new file directly inside it, by that folder\'s own rules. ' +
|
|
1748
|
+
'The same answer whichever branch you listed.',
|
|
1749
|
+
},
|
|
1750
|
+
},
|
|
1751
|
+
required: ['name', 'type', 'canWrite'],
|
|
1671
1752
|
},
|
|
1672
1753
|
},
|
|
1673
1754
|
},
|
|
@@ -1680,7 +1761,12 @@ disk) {
|
|
|
1680
1761
|
const fs = await ctx.getFilesystem(a.branch);
|
|
1681
1762
|
const entries = withoutPlaceholder((await fs.readdir(dir || '.')));
|
|
1682
1763
|
const filtered = await filterReadableEntries(readGateFor(a.branch, ctx), dir, entries);
|
|
1683
|
-
|
|
1764
|
+
// One batch for the listing. A folder is judged at its own path, whose
|
|
1765
|
+
// rules (its `access.md` and those above it) are what a file added
|
|
1766
|
+
// directly inside it would get.
|
|
1767
|
+
const wsPathOf = (e) => (dir ? `${dir}/${e.name}` : e.name);
|
|
1768
|
+
const verdicts = await canWriteAt(ctx, filtered.map(wsPathOf));
|
|
1769
|
+
return { path: a.path ?? '', entries: filtered.map((e) => ({ ...e, canWrite: verdicts.get(wsPathOf(e)) === true })) };
|
|
1684
1770
|
},
|
|
1685
1771
|
});
|
|
1686
1772
|
mount({
|