evolcore 0.0.33 → 0.0.34
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/CHANGELOG.md +37 -0
- package/bin/ec.js +3 -2
- package/bin/install-codex-managed-hooks.mjs +12 -1
- package/dist/agents/baseagent-availability.js +20 -36
- package/dist/agents/claude-runner.js +82 -13
- package/dist/agents/codex/diagnostics.js +37 -11
- package/dist/agents/codex-runner.js +218 -44
- package/dist/agents/ecagent-runner.js +73 -20
- package/dist/agents/runner-types.js +5 -6
- package/dist/aun/aid/identity.js +1 -1
- package/dist/channels/aun.js +16 -6
- package/dist/channels/daemon.js +6 -3
- package/dist/cli/aun-commands.js +3 -2
- package/dist/cli/ctl-command.js +3 -1
- package/dist/cli/daemon-commands.js +120 -19
- package/dist/cli/fs-command.js +2 -1
- package/dist/cli/index.js +20 -6
- package/dist/cli/integrity-command.js +196 -0
- package/dist/cli/memory-command.js +158 -145
- package/dist/cli/thread-command.js +5 -3
- package/dist/cli/trigger-command.js +6 -1
- package/dist/config/config-field-policy.js +4 -0
- package/dist/config/config-manager.js +19 -0
- package/dist/config/contact-book-store.js +5 -0
- package/dist/config/role-service.js +13 -0
- package/dist/config/runtime-env.js +16 -0
- package/dist/config-store.js +30 -0
- package/dist/core/audit/log-integrity.js +3 -1
- package/dist/core/auth/agent-delegation.js +108 -35
- package/dist/core/auth/delegation-diagnostics.js +414 -0
- package/dist/core/auth/operation-authorizer.js +1 -1
- package/dist/core/auth/operation-catalog.js +16 -0
- package/dist/core/command/agent-control.js +18 -26
- package/dist/core/command/command-handler.js +138 -4
- package/dist/core/command/daemon-diagnostics.js +1 -0
- package/dist/core/command/menu-catalog.js +3 -0
- package/dist/core/command/menu-handler.js +22 -7
- package/dist/core/command/menu-protocol.js +1 -0
- package/dist/core/command/slash-handler.js +2 -0
- package/dist/core/daemon-bootstrap.js +4 -7
- package/dist/core/file-integrity/read.js +61 -0
- package/dist/core/file-integrity/store.js +262 -0
- package/dist/core/ipc-registration.js +36 -1
- package/dist/core/memory/batch-meta.js +132 -0
- package/dist/core/memory/consolidation-context.js +35 -14
- package/dist/core/memory/consolidation-ops-records.js +14 -16
- package/dist/core/memory/consolidation.js +91 -26
- package/dist/core/memory/index-manager.js +19 -28
- package/dist/core/memory/memory-admin.js +19 -14
- package/dist/core/memory/memory-document.js +53 -9
- package/dist/core/memory/memory-writer.js +30 -15
- package/dist/core/message/im-renderer.js +20 -4
- package/dist/core/message/message-bridge.js +2 -1
- package/dist/core/message/response-engine.js +584 -47
- package/dist/core/permission/ec-command-parser.js +93 -4
- package/dist/core/permission/protected-paths.js +5 -1
- package/dist/core/permission/readonly-shell-query.js +43 -5
- package/dist/core/permission/session-path-grants.js +334 -0
- package/dist/core/permission/tool-canonical.js +226 -0
- package/dist/core/permission/tool-policy.js +30 -62
- package/dist/core/role/runtime-policy.js +21 -10
- package/dist/core/session/session-fs-store.js +123 -8
- package/dist/core/session/session-manager.js +75 -5
- package/dist/core/skill/index-generator.js +17 -3
- package/dist/core/system-session/grants.js +40 -0
- package/dist/core/system-session/memory.js +222 -0
- package/dist/core/system-session/registry.js +74 -0
- package/dist/core/system-session/types.js +14 -0
- package/dist/core/thread/thread-service.js +13 -20
- package/dist/eck/kit-renderer.js +7 -2
- package/dist/eck/manifest-engine.js +24 -4
- package/dist/index.js +11 -2
- package/dist/ipc.js +51 -40
- package/dist/paths.js +70 -0
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/feedback.js +6 -1
- package/dist/trigger/parser.js +8 -1
- package/dist/trigger/system-definitions.js +3 -3
- package/dist/utils/cross-platform.js +11 -2
- package/dist/utils/error-dict.json +18 -4
- package/dist/utils/error-utils.js +121 -0
- package/dist/utils/instance-registry.js +13 -8
- package/dist/utils/log-writer.js +12 -0
- package/dist/utils/process-introspect.js +11 -0
- package/kits/docs/context-assembly.md +0 -1
- package/kits/docs/evolcore/agent-create.md +5 -4
- package/kits/docs/evolcore/agent.md +2 -2
- package/kits/docs/evolcore/config.md +2 -2
- package/kits/docs/evolcore/ctl.md +2 -2
- package/kits/docs/evolcore/ec-capabilities.md +1 -1
- package/kits/docs/evolcore/fs-architecture.md +4 -5
- package/kits/docs/evolcore/fs.md +1 -1
- package/kits/docs/evolcore/group-collaboration.md +2 -2
- package/kits/docs/evolcore/group.md +10 -10
- package/kits/docs/evolcore/memory-architecture.md +4 -5
- package/kits/docs/evolcore/memory-consolidation.md +18 -14
- package/kits/docs/evolcore/memory-tags.md +3 -1
- package/kits/docs/evolcore/memory.md +24 -12
- package/kits/docs/evolcore/model.md +16 -0
- package/kits/docs/evolcore/msg.md +7 -7
- package/kits/docs/evolcore/thread-chain.md +5 -5
- package/kits/docs/prompt-loading-architecture.md +0 -1
- package/kits/eck_manifest.json +0 -20
- package/kits/eck_manifest.memory.json +0 -12
- package/kits/rules/01-overview.md +11 -0
- package/kits/rules/03-identity.md +6 -3
- package/kits/rules/06-channel.md +1 -1
- package/kits/rules/07-memory-and-thread.md +12 -15
- package/kits/schemas/_meta.json +7 -1
- package/kits/schemas/agent-config.schema.13.json +1 -1
- package/kits/schemas/daemon.schema.7.json +140 -0
- package/kits/skills/ec-group/SKILL.md +4 -4
- package/kits/skills/ec-skill/scripts/cli.mjs +1 -1
- package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
- package/kits/templates/memory-consolidation/forget.md +2 -2
- package/kits/templates/memory-consolidation/generalize.md +4 -4
- package/kits/templates/memory-consolidation/notes.md +11 -11
- package/kits/templates/memory-consolidation/profile.md +3 -3
- package/kits/templates/memory-consolidation/promote.md +3 -3
- package/kits/templates/memory-consolidation/workflow.md +6 -6
- package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
- package/kits/templates/system-fragments/bootstrap.md +4 -3
- package/kits/templates/system-fragments/channel.md +2 -2
- package/kits/templates/system-fragments/memory.md +16 -9
- package/kits/templates/system-fragments/reply.md +6 -6
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +4 -3
- package/dist/core/memory/memory-session-policy.js +0 -41
- package/dist/core/memory/memory-signature.js +0 -257
|
@@ -714,6 +714,77 @@ function decodePositionalText(argv, start, optionFlags) {
|
|
|
714
714
|
argv[index] = decodeEvolcoreInlineText(value);
|
|
715
715
|
}
|
|
716
716
|
}
|
|
717
|
+
// 取值为「逗号分隔、单项不含空格」的列表参数。aid、标签值、角色名里都不会有
|
|
718
|
+
// 空格,所以按空格拆分是安全的。
|
|
719
|
+
const MEMORY_LIST_FLAGS = new Set(['--tags', '--speakers']);
|
|
720
|
+
const SEND_LIST_FLAGS = new Set(['--mention']);
|
|
721
|
+
const FS_LIST_FLAGS = new Set(['--allow-roles']);
|
|
722
|
+
// 取值可能含空格的列表参数,只能按逗号拆:关键词是自由文本,记忆路径是 config
|
|
723
|
+
// 相对路径,两者都可能有空格。
|
|
724
|
+
const MEMORY_COMMA_ONLY_FLAGS = new Set(['--keywords', '--replaces']);
|
|
725
|
+
/** 逗号分隔项:去空、去首尾空白,重新用逗号连接。 */
|
|
726
|
+
function joinCommaList(value) {
|
|
727
|
+
return value.split(',').map(item => item.trim()).filter(Boolean).join(',');
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* 把列表参数统一成逗号分隔。PowerShell 把未加引号的 `a,b` 当数组,经 npm 的
|
|
731
|
+
* ec.ps1 用 `$args` 转发给 node 时逗号消失:`a,b` 变成 `a b`,`a, b` 和
|
|
732
|
+
* `a,b, c` 也变成 `a b` / `a b c`(实测 PS 5.1)。审批侧按命令串解析时看到的
|
|
733
|
+
* 是 `a,` 和 `b` 两个 token。两边在这里收敛到同一表示,委托哈希才能对上。
|
|
734
|
+
*
|
|
735
|
+
* 值以逗号结尾说明逗号数组还没结束:把后续 token 并进同一个值里再拆。值里本来
|
|
736
|
+
* 带逗号却没有空格的写法(bash 下 `--tags "a, b"`)也会被拆成两个标签,这是刻
|
|
737
|
+
* 意取舍——标签都是不含空格的单词,而 ec.ps1 已经把 `a, b` 和 `a,b` 归成了一
|
|
738
|
+
* 个值,无法再区分。
|
|
739
|
+
*/
|
|
740
|
+
function canonicalizeListFlags(argv, flags) {
|
|
741
|
+
for (let index = 0; index < argv.length; index++) {
|
|
742
|
+
const value = argv[index];
|
|
743
|
+
if (value === '--')
|
|
744
|
+
break;
|
|
745
|
+
if (!flags.has(value))
|
|
746
|
+
continue;
|
|
747
|
+
if (argv[index + 1] === undefined || argv[index + 1].startsWith('--'))
|
|
748
|
+
continue;
|
|
749
|
+
// `--tags a, b` 在 PowerShell 里是同一个值,审批侧却解析成 `a,` 加 `b`,
|
|
750
|
+
// 这里按逗号是否收尾把后续 token 续接回来
|
|
751
|
+
let end = index + 1;
|
|
752
|
+
while (argv[end].endsWith(',') && argv[end + 1] !== undefined
|
|
753
|
+
&& !argv[end + 1].startsWith('--')) {
|
|
754
|
+
end++;
|
|
755
|
+
}
|
|
756
|
+
argv[index + 1] = argv.slice(index + 1, end + 1)
|
|
757
|
+
.join(' ')
|
|
758
|
+
.split(/[,\s]+/u)
|
|
759
|
+
.filter(Boolean)
|
|
760
|
+
.join(',');
|
|
761
|
+
if (end > index + 1)
|
|
762
|
+
argv.splice(index + 2, end - index - 1);
|
|
763
|
+
index++;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* 只按逗号归一化,不动值里的空格。
|
|
768
|
+
*
|
|
769
|
+
* 与 canonicalizeListFlags 的区别:`--keywords 'a b'` 是一个含空格的关键词,
|
|
770
|
+
* 不能被拆成两个。所以只在值本身带逗号时整理(顺带去掉逗号周围的空白),
|
|
771
|
+
* `--keywords a,b` 这种未加引号的写法在 PowerShell 下会先被折成 `a b`,此处
|
|
772
|
+
* 认不出来——那属于被拒绝的写法,靠规范要求单引号避免。
|
|
773
|
+
*/
|
|
774
|
+
function canonicalizeCommaOnlyFlags(argv, flags) {
|
|
775
|
+
for (let index = 0; index < argv.length; index++) {
|
|
776
|
+
const value = argv[index];
|
|
777
|
+
if (value === '--')
|
|
778
|
+
break;
|
|
779
|
+
if (!flags.has(value))
|
|
780
|
+
continue;
|
|
781
|
+
const next = argv[index + 1];
|
|
782
|
+
if (next === undefined || next.startsWith('--') || !next.includes(','))
|
|
783
|
+
continue;
|
|
784
|
+
argv[index + 1] = joinCommaList(next);
|
|
785
|
+
index++;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
717
788
|
function canonicalizeHandoffText(argv) {
|
|
718
789
|
const action = argv[2];
|
|
719
790
|
if (action !== 'return' && action !== 'recover-return')
|
|
@@ -739,7 +810,8 @@ function canonicalizeHandoffText(argv) {
|
|
|
739
810
|
* Convert an EC invocation to the one argv representation shared by approval,
|
|
740
811
|
* delegation hashing, IPC, and command execution. Shell syntax is already
|
|
741
812
|
* resolved before this function is called; this function only normalizes the
|
|
742
|
-
* EC entrypoint
|
|
813
|
+
* EC entrypoint, applies the command-specific inline text contract, and joins
|
|
814
|
+
* memory list flags with commas.
|
|
743
815
|
*/
|
|
744
816
|
export function canonicalizeEvolcoreCommandArgv(argv) {
|
|
745
817
|
const canonical = normalizeEvolcoreCommandArgv(argv);
|
|
@@ -759,11 +831,13 @@ export function canonicalizeEvolcoreCommandArgv(argv) {
|
|
|
759
831
|
else if (command === 'msg' && subcommand === 'send') {
|
|
760
832
|
decodeInlineFlags(result, INLINE_TEXT_FLAGS.msgSend);
|
|
761
833
|
decodePositionalText(result, 5, MESSAGE_POSITIONAL_FLAGS);
|
|
834
|
+
canonicalizeListFlags(result, SEND_LIST_FLAGS);
|
|
762
835
|
}
|
|
763
836
|
else if (command === 'group') {
|
|
764
837
|
if (subcommand === 'send') {
|
|
765
838
|
decodeInlineFlags(result, INLINE_TEXT_FLAGS.groupSend);
|
|
766
839
|
decodePositionalText(result, 5, MESSAGE_POSITIONAL_FLAGS);
|
|
840
|
+
canonicalizeListFlags(result, SEND_LIST_FLAGS);
|
|
767
841
|
}
|
|
768
842
|
else if (subcommand === 'create')
|
|
769
843
|
decodeInlineFlags(result, INLINE_TEXT_FLAGS.groupCreate);
|
|
@@ -785,6 +859,13 @@ export function canonicalizeEvolcoreCommandArgv(argv) {
|
|
|
785
859
|
else if (command === 'handoff') {
|
|
786
860
|
canonicalizeHandoffText(result);
|
|
787
861
|
}
|
|
862
|
+
else if (command === 'memory') {
|
|
863
|
+
canonicalizeListFlags(result, MEMORY_LIST_FLAGS);
|
|
864
|
+
canonicalizeCommaOnlyFlags(result, MEMORY_COMMA_ONLY_FLAGS);
|
|
865
|
+
}
|
|
866
|
+
else if (command === 'fs' && subcommand === 'chmod') {
|
|
867
|
+
canonicalizeListFlags(result, FS_LIST_FLAGS);
|
|
868
|
+
}
|
|
788
869
|
return result;
|
|
789
870
|
}
|
|
790
871
|
const POWERSHELL_CREDENTIAL_ENV_RE = /^(EVOLCORE_CODEX_SESSION_TOKEN|EVOLCORE_DELEGATION_TOKEN)$/i;
|
|
@@ -1496,6 +1577,8 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
1496
1577
|
let token = '';
|
|
1497
1578
|
let tokenStarted = false;
|
|
1498
1579
|
let quote = null;
|
|
1580
|
+
// EC 命令的自由文本有额外的写法约束(见双引号分支的反引号/$ 转义处理)。
|
|
1581
|
+
const isEcCommand = hasEcCommandPrefix(input);
|
|
1499
1582
|
const finishToken = () => {
|
|
1500
1583
|
if (!tokenStarted)
|
|
1501
1584
|
return;
|
|
@@ -1527,6 +1610,12 @@ export function parseLiteralShellCommand(command, options = {}) {
|
|
|
1527
1610
|
return parseFailure(issue, input, index, argv.length, 'posix');
|
|
1528
1611
|
if (char === '\\') {
|
|
1529
1612
|
const next = input[++index];
|
|
1613
|
+
// EC 自由文本只接受单引号一种写法。双引号里用反斜杠转义 ` 或 $ 虽然
|
|
1614
|
+
// 能得到字面量,但同一条规则下要记两套写法,所以对 ec 命令显式拒绝,
|
|
1615
|
+
// 把正文一律导向单引号。非 ec 命令保持原行为。
|
|
1616
|
+
if (isEcCommand && (next === '`' || next === '$')) {
|
|
1617
|
+
return parseFailure('unsupported-escape', input, index, argv.length, 'posix');
|
|
1618
|
+
}
|
|
1530
1619
|
token += ['"', '\\', '$', '`'].includes(next) ? next : `\\${next}`;
|
|
1531
1620
|
continue;
|
|
1532
1621
|
}
|
|
@@ -1715,7 +1804,7 @@ function normalizeApplyPatchPath(value) {
|
|
|
1715
1804
|
return undefined;
|
|
1716
1805
|
return normalized;
|
|
1717
1806
|
}
|
|
1718
|
-
function parseApplyPatchDocument(source) {
|
|
1807
|
+
export function parseApplyPatchDocument(source) {
|
|
1719
1808
|
const lines = source.replace(/\r\n?/gu, '\n').split('\n');
|
|
1720
1809
|
while (lines.at(-1) === '')
|
|
1721
1810
|
lines.pop();
|
|
@@ -3022,7 +3111,7 @@ export function describeProactiveSendCommandFailure(toolName, input, session) {
|
|
|
3022
3111
|
const from = argv[3];
|
|
3023
3112
|
const to = argv[4];
|
|
3024
3113
|
const fullFormHint = `${prefix}缺少发送者与目标。完整形式为 ${expectedForm};`
|
|
3025
|
-
+
|
|
3114
|
+
+ `省略 from/to 仅在带 --thread 的跨话题发送中允许,普通回复也可用 ec ctl send '<正文>' 继承当前会话。`;
|
|
3026
3115
|
if (!from || from.startsWith('--')) {
|
|
3027
3116
|
return fullFormHint;
|
|
3028
3117
|
}
|
|
@@ -3030,7 +3119,7 @@ export function describeProactiveSendCommandFailure(toolName, input, session) {
|
|
|
3030
3119
|
return looksLikeAid(from)
|
|
3031
3120
|
? fullFormHint
|
|
3032
3121
|
: `${prefix}缺少发送者与目标("${from}" 是正文而非 AID)。完整形式为 ${expectedForm};`
|
|
3033
|
-
+
|
|
3122
|
+
+ `省略 from/to 仅在带 --thread 的跨话题发送中允许,普通回复也可用 ec ctl send '<正文>' 继承当前会话。`;
|
|
3034
3123
|
}
|
|
3035
3124
|
if (!looksLikeAid(from) || !looksLikeAid(to)) {
|
|
3036
3125
|
return `${prefix}发送者/目标不是合法 AID(收到 from="${from}" to="${to}")。完整形式为 ${expectedForm}。`;
|
|
@@ -938,7 +938,11 @@ export function lClassGrantIncludesProtectedPath(value, options = {}) {
|
|
|
938
938
|
// A grant covering an Agent root also covers its L-class configuration.
|
|
939
939
|
if (segments.length <= 2)
|
|
940
940
|
return true;
|
|
941
|
-
|
|
941
|
+
// relations/ 下的 L 类只有 relations/<peer>/config.json:只有范围是 relations/ 或
|
|
942
|
+
// relations/<peer> 本身时才覆盖它,更深的路径(如 relations/<peer>/memory/…)不会。
|
|
943
|
+
if (segments[2] === 'relations')
|
|
944
|
+
return segments.length <= 4;
|
|
945
|
+
return ['config.json', 'contact.json', 'contact-operations.jsonl', 'roles', 'triggers']
|
|
942
946
|
.includes(segments[2]);
|
|
943
947
|
}
|
|
944
948
|
return false;
|
|
@@ -3,7 +3,7 @@ import { parseLiteralShellArgv } from './ec-command-parser.js';
|
|
|
3
3
|
const MAX_QUERY_LISTS = 8;
|
|
4
4
|
const MAX_PIPELINE_STAGES = 3;
|
|
5
5
|
const SUPPORTED_EXECUTABLES = new Set([
|
|
6
|
-
'echo', 'pwd', 'ls', 'cat', 'grep', 'rg', 'find', 'sed', 'head', 'sort', 'git', 'wc', 'stat', 'lstat', 'awk',
|
|
6
|
+
'echo', 'pwd', 'ls', 'cat', 'grep', 'rg', 'find', 'sed', 'head', 'tail', 'sort', 'git', 'wc', 'stat', 'lstat', 'awk',
|
|
7
7
|
'bash', 'sh',
|
|
8
8
|
]);
|
|
9
9
|
const FIND_MUTATING_PRIMARY_RE = /^-(?:delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/;
|
|
@@ -447,6 +447,13 @@ function analyzeHead(args) {
|
|
|
447
447
|
const parsed = parseOptionsAndPaths(args, new Set(['-c', '-n', '--bytes', '--lines']));
|
|
448
448
|
return parsed?.paths.map(exact) ?? null;
|
|
449
449
|
}
|
|
450
|
+
function analyzeTail(args) {
|
|
451
|
+
// -f/--follow opens an indefinite watch loop — not a bounded read query.
|
|
452
|
+
if (args.some(a => a === '-f' || a === '--follow' || /^-[^-]*f/.test(a)))
|
|
453
|
+
return null;
|
|
454
|
+
const parsed = parseOptionsAndPaths(args, new Set(['-c', '-n', '--bytes', '--lines']));
|
|
455
|
+
return parsed?.paths.map(exact) ?? null;
|
|
456
|
+
}
|
|
450
457
|
function analyzeSort(args) {
|
|
451
458
|
if (args.some(argument => /^(?:-o|-T)(?:.+|$)|^(?:--output|--compress-program|--temporary-directory|--files0-from)(?:=|$)/.test(argument))) {
|
|
452
459
|
return null;
|
|
@@ -828,68 +835,95 @@ function analyzeCommand(argv) {
|
|
|
828
835
|
const args = argv.slice(1);
|
|
829
836
|
let pathOperands = null;
|
|
830
837
|
let accessKind = 'content';
|
|
838
|
+
let purpose = 'unknown';
|
|
831
839
|
switch (executable) {
|
|
832
840
|
case 'echo':
|
|
833
841
|
pathOperands = [];
|
|
842
|
+
purpose = 'passthrough';
|
|
834
843
|
break;
|
|
835
844
|
case 'pwd':
|
|
836
845
|
pathOperands = args.every(argument => ['-L', '-P', '--help', '--version'].includes(argument)) ? [] : null;
|
|
846
|
+
purpose = 'passthrough';
|
|
837
847
|
break;
|
|
838
848
|
case 'ls':
|
|
839
849
|
pathOperands = analyzeLs(args);
|
|
850
|
+
purpose = 'enumerate';
|
|
840
851
|
break;
|
|
841
852
|
case 'cat':
|
|
842
853
|
pathOperands = analyzeCat(args);
|
|
854
|
+
purpose = 'content-read';
|
|
843
855
|
break;
|
|
844
856
|
case 'grep':
|
|
845
857
|
pathOperands = analyzeGrep(args);
|
|
858
|
+
purpose = 'search';
|
|
846
859
|
break;
|
|
847
860
|
case 'rg':
|
|
848
861
|
pathOperands = analyzeRg(args);
|
|
862
|
+
purpose = 'search';
|
|
849
863
|
break;
|
|
850
864
|
case 'find':
|
|
851
865
|
pathOperands = analyzeFind(args);
|
|
866
|
+
purpose = 'enumerate';
|
|
852
867
|
break;
|
|
853
868
|
case 'sed':
|
|
854
869
|
pathOperands = analyzeSed(args);
|
|
870
|
+
purpose = 'content-read';
|
|
855
871
|
break;
|
|
856
872
|
case 'head':
|
|
857
873
|
pathOperands = analyzeHead(args);
|
|
874
|
+
purpose = 'content-read';
|
|
875
|
+
break;
|
|
876
|
+
case 'tail':
|
|
877
|
+
pathOperands = analyzeTail(args);
|
|
878
|
+
purpose = 'content-read';
|
|
858
879
|
break;
|
|
859
880
|
case 'sort':
|
|
860
881
|
pathOperands = analyzeSort(args);
|
|
882
|
+
// sort 有路径参数时是文件读取,无路径参数时是纯 stdin 排序
|
|
883
|
+
purpose = 'stdin-transform';
|
|
861
884
|
break;
|
|
862
885
|
case 'git':
|
|
863
886
|
pathOperands = analyzeGit(args);
|
|
887
|
+
purpose = 'vcs-query';
|
|
864
888
|
break;
|
|
865
889
|
case 'wc':
|
|
866
890
|
pathOperands = analyzeWc(args);
|
|
891
|
+
// wc 有路径参数时是元数据读取,无路径参数时是纯 stdin 统计
|
|
892
|
+
purpose = 'metadata-read';
|
|
867
893
|
break;
|
|
868
894
|
case 'stat':
|
|
869
895
|
case 'lstat':
|
|
870
896
|
pathOperands = analyzeStat(args);
|
|
871
897
|
accessKind = 'metadata';
|
|
898
|
+
purpose = 'metadata-read';
|
|
872
899
|
break;
|
|
873
900
|
case 'awk':
|
|
874
901
|
pathOperands = analyzeAwk(args);
|
|
902
|
+
purpose = 'content-read';
|
|
875
903
|
break;
|
|
876
904
|
case 'bash':
|
|
877
905
|
case 'sh':
|
|
878
906
|
pathOperands = analyzeShellSyntaxCheck(args);
|
|
907
|
+
purpose = 'syntax-check';
|
|
879
908
|
break;
|
|
880
909
|
}
|
|
881
910
|
if (!pathOperands)
|
|
882
911
|
return null;
|
|
883
912
|
if (['ls', 'find'].includes(executable))
|
|
884
913
|
accessKind = 'enumerate';
|
|
914
|
+
// 无路径操作数的命令是纯 stdin→stdout 变换,可安全出现在管道任意位置。
|
|
915
|
+
// 对于 sort/wc/awk/grep/head/tail/sed:有路径参数时读文件,无路径参数时为 stdin 变换。
|
|
916
|
+
const stdinTransform = pathOperands.length === 0
|
|
917
|
+
&& ['sort', 'wc', 'awk', 'grep', 'rg', 'head', 'tail', 'sed'].includes(executable);
|
|
918
|
+
// 修正 purpose:stdin 变换时统一标记,避免 'metadata-read'/'content-read' 歧义
|
|
919
|
+
const resolvedPurpose = stdinTransform ? 'stdin-transform' : purpose;
|
|
885
920
|
return {
|
|
886
921
|
command: {
|
|
887
922
|
executable,
|
|
888
923
|
argv,
|
|
889
924
|
pathOperands,
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
// derived from their operands by policy consumers.
|
|
925
|
+
stdinTransform,
|
|
926
|
+
purpose: resolvedPurpose,
|
|
893
927
|
...(accessKind === 'metadata' ? { accessKind } : {}),
|
|
894
928
|
},
|
|
895
929
|
};
|
|
@@ -910,7 +944,11 @@ export function analyzeReadonlyShellQuery(command, options = {}) {
|
|
|
910
944
|
if (!analyzed)
|
|
911
945
|
return { kind: 'unproven', reason: 'unsupported-command-or-option' };
|
|
912
946
|
if (index > 0) {
|
|
913
|
-
|
|
947
|
+
// 管道后续段必须是纯 stdin→stdout 变换(stdinTransform=true 由 analyzeCommand 打标,
|
|
948
|
+
// 表示命令无 FS 路径操作数、只处理 stdin 流)。
|
|
949
|
+
// 黑名单规则已在 analyzeCommand 内部拦截(如 sort -o、tail -f 等),到这里的都是
|
|
950
|
+
// 白名单规则捞回后的安全命令,用标签判断而非硬编码可执行文件列表。
|
|
951
|
+
if (!analyzed.command.stdinTransform) {
|
|
914
952
|
return { kind: 'unproven', reason: 'unsupported-pipeline-filter' };
|
|
915
953
|
}
|
|
916
954
|
if (analyzed.command.pathOperands.length > 0) {
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-path-grants.ts — 用户会话路径授权表
|
|
3
|
+
*
|
|
4
|
+
* 形状:路径模板 → [{ identities, access }],identities 为角色名(owner/admin/member/visitor)。
|
|
5
|
+
*
|
|
6
|
+
* 只服务用户会话。系统会话(记忆整理等)与角色体系无关,路径边界由
|
|
7
|
+
* src/core/system-session/ 下的模块在代码里决定,不走本表、不并运行时授予。
|
|
8
|
+
*
|
|
9
|
+
* 判定规则(两条,别加第三条):
|
|
10
|
+
* 1. 同会话多条命中 → 取最大权限(none 显式覆盖通配)
|
|
11
|
+
* 2. 与权限模式取交集:readonly 下 write 降级为 read
|
|
12
|
+
*
|
|
13
|
+
* 多表合并(mergeSessionPathGrantTables)同路径追加而非替换。
|
|
14
|
+
* 运行时动态授予(ec ctl path grant)走 grantSessionPath(),按 sessionId 持久化。
|
|
15
|
+
*
|
|
16
|
+
* 详见 docs/memory/system-session-plugin-design.md
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { resolveRoot } from '../../paths.js';
|
|
21
|
+
import { resolveProtectedCandidate, isSameOrDescendant } from './protected-paths.js';
|
|
22
|
+
import { logger } from '../../utils/logger.js';
|
|
23
|
+
import { registerContent, unregisterFile } from '../file-integrity/store.js';
|
|
24
|
+
const ACCESS_RANK = { none: -1, read: 1, write: 2 };
|
|
25
|
+
/**
|
|
26
|
+
* 内置默认授权表。当前为空:用户会话缺省只能读工作目录,
|
|
27
|
+
* 额外路径由 owner/admin 经 `ec ctl path grant` 按会话授予。
|
|
28
|
+
*/
|
|
29
|
+
export const BUILTIN_SESSION_PATH_GRANTS = {};
|
|
30
|
+
/**
|
|
31
|
+
* 合并多张授权表:同路径模板的条目**追加**,不替换。
|
|
32
|
+
*
|
|
33
|
+
* 对象展开 `{ ...a, ...b }` 在同键时会整组丢弃前者——会话表里一旦出现同名路径,
|
|
34
|
+
* 前一张表的授予就静默消失。追加后判定仍走「同路径取最大、none 锁定」,
|
|
35
|
+
* 所以想收窄只能显式写 none,不会因为键名巧合丢权限。
|
|
36
|
+
*/
|
|
37
|
+
export function mergeSessionPathGrantTables(...tables) {
|
|
38
|
+
const merged = {};
|
|
39
|
+
for (const table of tables) {
|
|
40
|
+
if (!table)
|
|
41
|
+
continue;
|
|
42
|
+
for (const [template, entries] of Object.entries(table)) {
|
|
43
|
+
if (!Array.isArray(entries))
|
|
44
|
+
continue;
|
|
45
|
+
merged[template] = [...(merged[template] ?? []), ...entries];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return merged;
|
|
49
|
+
}
|
|
50
|
+
/** 判断一份授权表里的条目是否合法。 */
|
|
51
|
+
export function isValidGrantEntry(entry) {
|
|
52
|
+
return Array.isArray(entry.identities)
|
|
53
|
+
&& entry.identities.length > 0
|
|
54
|
+
&& entry.identities.every(value => typeof value === 'string' && value.trim().length > 0)
|
|
55
|
+
&& (entry.access === 'none' || entry.access === 'read' || entry.access === 'write');
|
|
56
|
+
}
|
|
57
|
+
/** 替换路径模板里的变量。未知变量保持原样,由调用方按「不存在」处理。 */
|
|
58
|
+
export function substitutePathTemplate(template, ctx, vars) {
|
|
59
|
+
return template.replace(/\$([A-Z_]+)/g, (match, name) => vars[name] ?? match);
|
|
60
|
+
}
|
|
61
|
+
function buildVars(ctx) {
|
|
62
|
+
return {
|
|
63
|
+
HOME: resolveRoot(),
|
|
64
|
+
SELF_AID: ctx.selfAid,
|
|
65
|
+
PEER: ctx.peerKey,
|
|
66
|
+
OWNER: ctx.ownerAid,
|
|
67
|
+
PROJECT: ctx.projectPath,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** 模板里出现未解析的变量时视为不可用条目。展开后的路径必须是绝对路径。 */
|
|
71
|
+
function resolveTemplate(template, ctx) {
|
|
72
|
+
const resolved = substitutePathTemplate(template, ctx, buildVars(ctx));
|
|
73
|
+
const unresolved = resolved.match(/\$[A-Z_]+/g);
|
|
74
|
+
if (unresolved) {
|
|
75
|
+
// 未知变量(拼错)与已知变量缺值(如私聊会话没有 $OWNER)都会走到这里;
|
|
76
|
+
// 两者都让整条授予失效,留一行日志,否则下游只剩一个原因不明的读拒绝。
|
|
77
|
+
logger.warn(`[path-grants] 授权模板含未解析变量,条目跳过: template=${template} unresolved=${[...new Set(unresolved)].join(',')} identity=${ctx.identity ?? ''}`);
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (!path.isAbsolute(resolved)) {
|
|
81
|
+
logger.warn(`[path-grants] 授权模板展开后不是绝对路径,条目跳过: template=${template} resolved=${resolved}`);
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return resolveProtectedCandidate(resolved);
|
|
85
|
+
}
|
|
86
|
+
/** 当前会话是否命中某条授予的身份。 */
|
|
87
|
+
function identityMatches(entry, identity) {
|
|
88
|
+
const normalized = identity.trim().toLowerCase();
|
|
89
|
+
return entry.identities.some(value => value.trim().toLowerCase() === normalized);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 权限模式交集。系统会话不传 permissionMode,按授予原样生效。
|
|
93
|
+
* readonly 下 write 降级为 read;none 永不升级。
|
|
94
|
+
*/
|
|
95
|
+
function applyPermissionModeCeiling(access, permissionMode) {
|
|
96
|
+
if (access === 'none' || !permissionMode)
|
|
97
|
+
return access;
|
|
98
|
+
return permissionMode === 'readonly' ? 'read' : access;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 计算当前会话在给定授权表下的**有效路径集合**。
|
|
102
|
+
*
|
|
103
|
+
* 返回 path → access 的映射,只含 access 非 none 的条目。同路径多条命中取最大权限
|
|
104
|
+
* (none 除外——任一命中 none 即整体拒绝)。
|
|
105
|
+
*/
|
|
106
|
+
export function resolveEffectivePathGrants(table, ctx) {
|
|
107
|
+
const effective = new Map();
|
|
108
|
+
const identity = (ctx.identity ?? '').trim();
|
|
109
|
+
if (!identity)
|
|
110
|
+
return effective;
|
|
111
|
+
for (const [template, entries] of Object.entries(table)) {
|
|
112
|
+
if (!Array.isArray(entries))
|
|
113
|
+
continue;
|
|
114
|
+
const hits = entries.filter(entry => isValidGrantEntry(entry) && identityMatches(entry, identity));
|
|
115
|
+
if (hits.length === 0)
|
|
116
|
+
continue;
|
|
117
|
+
const resolved = resolveTemplate(template, ctx);
|
|
118
|
+
if (!resolved)
|
|
119
|
+
continue;
|
|
120
|
+
const previous = effective.get(resolved);
|
|
121
|
+
// none 是显式拒绝,一旦出现就锁定,不被同路径的其它授予放宽
|
|
122
|
+
if (previous === 'none')
|
|
123
|
+
continue;
|
|
124
|
+
let next = 'none';
|
|
125
|
+
for (const hit of hits) {
|
|
126
|
+
const grant = applyPermissionModeCeiling(hit.access, ctx.permissionMode);
|
|
127
|
+
if (grant === 'none') {
|
|
128
|
+
next = 'none';
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
if (ACCESS_RANK[grant] > ACCESS_RANK[next])
|
|
132
|
+
next = grant;
|
|
133
|
+
}
|
|
134
|
+
if (next === 'none')
|
|
135
|
+
effective.set(resolved, 'none');
|
|
136
|
+
else
|
|
137
|
+
effective.set(resolved, next);
|
|
138
|
+
}
|
|
139
|
+
return effective;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 有效路径授权表的可读根目录列表(含只读授予)。
|
|
143
|
+
*
|
|
144
|
+
* 判定语义是「本路径落在任一可读根之下即为可读」,因此这里连同 write 授予一起返回——
|
|
145
|
+
* write 蕴含 read。
|
|
146
|
+
*/
|
|
147
|
+
export function resolveGrantedReadRoots(table, ctx) {
|
|
148
|
+
const roots = [];
|
|
149
|
+
for (const [value, access] of resolveEffectivePathGrants(table, ctx)) {
|
|
150
|
+
if (access === 'read' || access === 'write')
|
|
151
|
+
roots.push(value);
|
|
152
|
+
}
|
|
153
|
+
return roots;
|
|
154
|
+
}
|
|
155
|
+
/** 候选路径是否落在任一授权根之下(含根自身)。 */
|
|
156
|
+
export function isPathGranted(candidate, roots) {
|
|
157
|
+
const resolved = resolveProtectedCandidate(candidate);
|
|
158
|
+
return roots.some(root => isSameOrDescendant(resolved, root));
|
|
159
|
+
}
|
|
160
|
+
const runtimeGrants = new Map();
|
|
161
|
+
/**
|
|
162
|
+
* 持久化位置:$HOME/data/path/grants/<sessionId>.json —— **每会话一个文件**。
|
|
163
|
+
*
|
|
164
|
+
* 不用单个共用大文件:那样一份损坏或半写会让**所有**会话的授予一起丢,
|
|
165
|
+
* 而且清理某个会话要重写整文件。一会话一文件让隔离和回收都变成本地操作。
|
|
166
|
+
*
|
|
167
|
+
* 代价是首次加载要扫目录。授予是低频操作,目录规模等于活跃会话数,可接受。
|
|
168
|
+
*/
|
|
169
|
+
function runtimeGrantDir() {
|
|
170
|
+
return path.join(resolveRoot(), 'data', 'path', 'grants');
|
|
171
|
+
}
|
|
172
|
+
/** sessionId 形如 meta_YYYYMMDD_<epochMs>,字符集安全;仍做一次防御性收敛。 */
|
|
173
|
+
function runtimeGrantFilePath(sessionId) {
|
|
174
|
+
const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
175
|
+
return path.join(runtimeGrantDir(), `${safe}.json`);
|
|
176
|
+
}
|
|
177
|
+
/** 原子写:同目录临时文件 + rename,避免读到半写的 JSON。 */
|
|
178
|
+
function persistSessionGrants(sessionId) {
|
|
179
|
+
const file = runtimeGrantFilePath(sessionId);
|
|
180
|
+
try {
|
|
181
|
+
const list = runtimeGrants.get(sessionId);
|
|
182
|
+
if (!list || list.length === 0) {
|
|
183
|
+
fs.rmSync(file, { force: true });
|
|
184
|
+
try {
|
|
185
|
+
unregisterFile(file);
|
|
186
|
+
}
|
|
187
|
+
catch { /* best-effort */ }
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
191
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
192
|
+
const content = `${JSON.stringify(list, null, 2)}\n`;
|
|
193
|
+
// 先登记,再写盘:避免 CLI 进程读到"新内容 + 旧台账"的窗口
|
|
194
|
+
try {
|
|
195
|
+
registerContent(file, content);
|
|
196
|
+
}
|
|
197
|
+
catch { /* best-effort */ }
|
|
198
|
+
fs.writeFileSync(tmp, content, 'utf-8');
|
|
199
|
+
fs.renameSync(tmp, file);
|
|
200
|
+
}
|
|
201
|
+
catch { /* 持久化失败不阻断本次会话:内存授予仍然生效 */ }
|
|
202
|
+
}
|
|
203
|
+
let runtimeGrantsLoaded = false;
|
|
204
|
+
/** 首次使用时从磁盘加载一次。单个文件损坏只影响它自己那个会话。 */
|
|
205
|
+
function ensureRuntimeGrantsLoaded() {
|
|
206
|
+
if (runtimeGrantsLoaded)
|
|
207
|
+
return;
|
|
208
|
+
runtimeGrantsLoaded = true;
|
|
209
|
+
let files;
|
|
210
|
+
try {
|
|
211
|
+
files = fs.readdirSync(runtimeGrantDir()).filter(name => name.endsWith('.json'));
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return; // 首次运行:目录还不存在
|
|
215
|
+
}
|
|
216
|
+
for (const name of files) {
|
|
217
|
+
const sessionId = name.slice(0, -'.json'.length);
|
|
218
|
+
try {
|
|
219
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(runtimeGrantDir(), name), 'utf-8'));
|
|
220
|
+
if (Array.isArray(parsed) && parsed.length > 0)
|
|
221
|
+
runtimeGrants.set(sessionId, parsed);
|
|
222
|
+
}
|
|
223
|
+
catch { /* 该会话的授权文件损坏:跳过它,不影响其它会话 */ }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** 供测试与重载清空(含持久化,避免下个用例读到上个用例的残留)。 */
|
|
227
|
+
export function clearRuntimePathGrants(sessionId) {
|
|
228
|
+
ensureRuntimeGrantsLoaded();
|
|
229
|
+
if (sessionId) {
|
|
230
|
+
runtimeGrants.delete(sessionId);
|
|
231
|
+
persistSessionGrants(sessionId);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
for (const id of [...runtimeGrants.keys()]) {
|
|
235
|
+
runtimeGrants.delete(id);
|
|
236
|
+
persistSessionGrants(id);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** 登记一条运行时授予。授权校验由 IPC 层负责(仅 owner/admin)。 */
|
|
240
|
+
export function grantSessionPath(record) {
|
|
241
|
+
ensureRuntimeGrantsLoaded();
|
|
242
|
+
const list = runtimeGrants.get(record.sessionId) ?? [];
|
|
243
|
+
// 同路径同身份重复登记按覆盖处理,避免列表无限增长
|
|
244
|
+
const kept = list.filter(existing => !(existing.path === record.path && existing.identity === record.identity));
|
|
245
|
+
kept.push(record);
|
|
246
|
+
runtimeGrants.set(record.sessionId, kept);
|
|
247
|
+
persistSessionGrants(record.sessionId);
|
|
248
|
+
}
|
|
249
|
+
/** 撤销一条运行时授予,返回是否命中。 */
|
|
250
|
+
export function revokeSessionPath(sessionId, template, identity) {
|
|
251
|
+
ensureRuntimeGrantsLoaded();
|
|
252
|
+
const list = runtimeGrants.get(sessionId);
|
|
253
|
+
if (!list)
|
|
254
|
+
return false;
|
|
255
|
+
const before = list.length;
|
|
256
|
+
const kept = list.filter(record => !(record.path === template && (identity === undefined || record.identity === identity)));
|
|
257
|
+
if (kept.length === before)
|
|
258
|
+
return false;
|
|
259
|
+
if (kept.length === 0)
|
|
260
|
+
runtimeGrants.delete(sessionId);
|
|
261
|
+
else
|
|
262
|
+
runtimeGrants.set(sessionId, kept);
|
|
263
|
+
persistSessionGrants(sessionId);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
/** 列出某会话的运行时授予。 */
|
|
267
|
+
export function listRuntimePathGrants(sessionId) {
|
|
268
|
+
ensureRuntimeGrantsLoaded();
|
|
269
|
+
return runtimeGrants.get(sessionId) ?? [];
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* 会话结束时的清理入口。
|
|
273
|
+
*
|
|
274
|
+
* 只清**该会话自己的**运行时授予:`ec ctl path grant` 是以当前会话为归属登记的,
|
|
275
|
+
* 会话结束后不再有会话身份去消费它。
|
|
276
|
+
*/
|
|
277
|
+
export function releaseSessionPathGrants(sessionId) {
|
|
278
|
+
ensureRuntimeGrantsLoaded();
|
|
279
|
+
if (!runtimeGrants.delete(sessionId))
|
|
280
|
+
return;
|
|
281
|
+
persistSessionGrants(sessionId);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* 把某会话的运行时授予并入授权表。
|
|
285
|
+
*
|
|
286
|
+
* 运行时授予以**会话身份**登记:identity 缺省时表示「该会话自身」,
|
|
287
|
+
* 亦即用调用方的 identity 匹配。
|
|
288
|
+
*/
|
|
289
|
+
export function mergeRuntimeGrantsForSession(table, sessionId, identity) {
|
|
290
|
+
if (!sessionId)
|
|
291
|
+
return table;
|
|
292
|
+
ensureRuntimeGrantsLoaded();
|
|
293
|
+
const list = runtimeGrants.get(sessionId);
|
|
294
|
+
if (!list || list.length === 0)
|
|
295
|
+
return table;
|
|
296
|
+
const merged = { ...table };
|
|
297
|
+
for (const record of list) {
|
|
298
|
+
if (!path.isAbsolute(record.path))
|
|
299
|
+
continue;
|
|
300
|
+
if (record.identity && record.identity.trim().toLowerCase() !== identity.trim().toLowerCase())
|
|
301
|
+
continue;
|
|
302
|
+
const existing = merged[record.path];
|
|
303
|
+
merged[record.path] = [...(existing ?? []), { identities: [identity], access: record.access }];
|
|
304
|
+
}
|
|
305
|
+
return merged;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* 会话运行目录本身恒为可读根:它是本会话私有的临时根,runner 自己的临时产物
|
|
309
|
+
* 都落在这里(见 task-context.ts 的 ensureSessionRuntimeDir)。
|
|
310
|
+
*
|
|
311
|
+
* TMPDIR 写能力另由 managedTempDir 负责,不在这里表达。
|
|
312
|
+
*/
|
|
313
|
+
export function sessionOwnTempReadRoot(sessionRuntimeDir) {
|
|
314
|
+
if (!sessionRuntimeDir || !path.isAbsolute(sessionRuntimeDir))
|
|
315
|
+
return [];
|
|
316
|
+
return [resolveProtectedCandidate(sessionRuntimeDir)];
|
|
317
|
+
}
|
|
318
|
+
/** 缺省的文件系统根:会话工作目录。 */
|
|
319
|
+
export function defaultProjectRoot(projectPath) {
|
|
320
|
+
if (!projectPath || !path.isAbsolute(projectPath))
|
|
321
|
+
return [];
|
|
322
|
+
return [resolveProtectedCandidate(projectPath)];
|
|
323
|
+
}
|
|
324
|
+
/** 供诊断输出:当前会话的有效路径集合(含运行时授予)。 */
|
|
325
|
+
export function describeEffectivePaths(table, ctx, sessionRuntimeDir) {
|
|
326
|
+
const result = [];
|
|
327
|
+
for (const [value, access] of resolveEffectivePathGrants(table, ctx)) {
|
|
328
|
+
result.push({ path: value, access });
|
|
329
|
+
}
|
|
330
|
+
for (const value of sessionOwnTempReadRoot(sessionRuntimeDir)) {
|
|
331
|
+
result.push({ path: value, access: 'read' });
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
|
+
}
|