evolcore 0.0.3 → 0.0.5
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 +83 -0
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +1 -31
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +157 -5
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/channels/aun.js +220 -112
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +52 -137
- package/dist/channels/feishu.js +56 -4
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +383 -7
- package/dist/cli/aun-commands.js +11 -11
- package/dist/cli/daemon-commands.js +46 -15
- package/dist/cli/handoff-command.js +2 -1
- package/dist/cli/init-channel.js +185 -67
- package/dist/cli/init.js +49 -27
- package/dist/cli/trigger-command.js +92 -11
- package/dist/config/access-policy-domain.js +18 -0
- package/dist/config/access-policy.js +110 -0
- package/dist/config/builtin-role-templates.js +27 -14
- package/dist/config/builtin-roles.js +25 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +198 -22
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +173 -5
- package/dist/config/contact-book.js +32 -0
- package/dist/config/contact-operation-service.js +9 -3
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +3 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -2
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +6 -0
- package/dist/core/auth/operation-authorizer.js +8 -0
- package/dist/core/auth/operation-catalog.js +51 -3
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +10 -13
- package/dist/core/command/connect-menu.js +229 -26
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +269 -85
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/role-menu.js +16 -0
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +158 -31
- package/dist/core/daemon-file-cache.js +28 -0
- package/dist/core/event-catalog.js +29 -0
- package/dist/core/evolagent-registry.js +4 -39
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +18 -14
- package/dist/core/message/inbound-admission.js +134 -0
- package/dist/core/message/message-bridge.js +616 -74
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +185 -10
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +381 -79
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +69 -24
- package/dist/core/protected-paths.js +36 -11
- package/dist/core/session/session-fs-store.js +19 -4
- package/dist/core/session/session-manager.js +232 -4
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +224 -89
- package/dist/ipc.js +81 -4
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/trigger/parser.js +55 -15
- package/dist/trigger/patch.js +4 -1
- package/dist/trigger/scheduler.js +441 -39
- package/dist/utils/cross-platform.js +8 -2
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/process-introspect.js +19 -3
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +8 -3
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +4 -4
- package/kits/docs/evolcore/msg.md +5 -5
- package/kits/docs/evolcore/trigger.md +25 -8
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +18 -7
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +364 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +46 -0
- package/kits/templates/roles/member.json +4 -0
- package/kits/templates/roles/visitor.json +4 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +4 -2
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/dist/channels/contact-bind-code.js +0 -134
- package/dist/channels/wecom-card.js +0 -101
- package/dist/channels/wecom-onboarding.js +0 -82
- package/dist/channels/wecom-state.js +0 -191
|
@@ -83,9 +83,9 @@ function printHelp() {
|
|
|
83
83
|
list [--agent <aid>] [--all] [--json]
|
|
84
84
|
show [--agent <aid>] <triggerId> [--json]
|
|
85
85
|
history [--agent <aid>] [triggerId] [--limit <n>] [--json]
|
|
86
|
-
create --file <trigger.json|trigger-dir> [--enable] [--json]
|
|
86
|
+
create --exec <script|trigger-session|target-session> --file <trigger.json|trigger-dir> [--enable] [--json]
|
|
87
87
|
OR
|
|
88
|
-
create [--agent <aid>] --cron <expr>|--event <pattern> --prompt <text> [--enable] [其他flag]
|
|
88
|
+
create [--agent <aid>] --exec <script|trigger-session|target-session> --cron <expr>|--event <pattern> (--prompt <text>|--prompt-file <path>) [--enable] [其他flag]
|
|
89
89
|
set ... (create 的兼容别名)
|
|
90
90
|
update [--agent <aid>] <triggerId> <参数...> [--if-revision <sha256:...>] [--json]
|
|
91
91
|
enable [--agent <aid>] <triggerId>
|
|
@@ -97,8 +97,9 @@ function printHelp() {
|
|
|
97
97
|
|
|
98
98
|
Create 参数模式支持:
|
|
99
99
|
--once | --delay <时长> | --at <ISO时间> | --cron <表达式> | --every <时长> | --event <事件模式>
|
|
100
|
-
--exec <script|trigger-session|target-session>
|
|
101
|
-
--prompt <文本>
|
|
100
|
+
--exec <script|trigger-session|target-session> (必填)
|
|
101
|
+
--prompt <文本> | --prompt-file <路径>
|
|
102
|
+
Shell 双引号内的双引号、反引号和变量形式的美元符号都需在前面加反斜杠;trigger 保存的是还原后的原文
|
|
102
103
|
--script-path <路径> --script-runtime <node|python|bash>
|
|
103
104
|
--script-args <JSON> --script-timeout <时长>
|
|
104
105
|
--feedback <target> (origin/silent 仅可通过 --file 导入)
|
|
@@ -117,12 +118,13 @@ Create 参数模式支持:
|
|
|
117
118
|
|
|
118
119
|
Update 仅支持常用字段:
|
|
119
120
|
--once | --delay | --at | --cron | --every | --event [--tz]
|
|
120
|
-
--prompt <文本> --name <名称>
|
|
121
|
+
--prompt <文本> | --prompt-file <路径> --name <名称>
|
|
121
122
|
--model <模型> --effort <强度> --permission <权限模式|inherit>(inherit 清除覆盖)
|
|
122
123
|
--max-runs <次数> --max-duration <时长>
|
|
123
124
|
--concurrency <forbid|replace|allow> --missed-policy <skip|run-once|run-all>
|
|
124
125
|
|
|
125
|
-
省略 --agent 时使用当前 agent AID
|
|
126
|
+
省略 --agent 时使用当前 agent AID;无会话上下文或跨身份操作时需要显式指定。
|
|
127
|
+
任务会话中的跨身份操作仅允许目标 Agent 上具备 owner/admin 权限的调用者执行 list/show/history;创建、修改、启停、删除和运行仍只允许当前 Agent。`);
|
|
126
128
|
}
|
|
127
129
|
async function listTriggers(args, json) {
|
|
128
130
|
const agentAid = resolveAgentAid(args);
|
|
@@ -214,10 +216,20 @@ async function showHistory(args, json) {
|
|
|
214
216
|
}
|
|
215
217
|
}
|
|
216
218
|
async function createTrigger(args, json) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
+
if (!hasOption(args, '--exec')) {
|
|
220
|
+
throw new Error('创建 trigger 必须显式指定 --exec script|trigger-session|target-session');
|
|
221
|
+
}
|
|
222
|
+
const executionType = executionTypeFromFlag(args);
|
|
223
|
+
// --file is reserved for full trigger-definition imports.
|
|
224
|
+
if (hasOption(args, '--file')) {
|
|
225
|
+
if (hasOption(args, '--prompt-file')) {
|
|
226
|
+
throw new Error('--file(完整定义导入)与 --prompt-file(prompt 正文)不能同时指定');
|
|
227
|
+
}
|
|
219
228
|
const file = requireFlag(args, '--file');
|
|
220
229
|
const { definition, files } = loadDefinitionWithFiles(file);
|
|
230
|
+
if (definition.execution.type !== executionType) {
|
|
231
|
+
throw new Error(`--exec ${flagValue(args, '--exec')} 与 trigger.json 的 execution.type=${definition.execution.type} 不一致`);
|
|
232
|
+
}
|
|
221
233
|
const res = await request({
|
|
222
234
|
type: 'trigger.create',
|
|
223
235
|
definition,
|
|
@@ -230,8 +242,12 @@ async function createTrigger(args, json) {
|
|
|
230
242
|
return;
|
|
231
243
|
}
|
|
232
244
|
// Flag mode: use parser
|
|
245
|
+
const usesPromptFile = hasOption(args, '--prompt-file');
|
|
246
|
+
if (usesPromptFile && executionType === 'script') {
|
|
247
|
+
throw new Error('--exec script 不接受 --prompt-file;请使用 --script-path 和 --script-runtime');
|
|
248
|
+
}
|
|
233
249
|
const { parseTriggerSetArgv } = await import('../trigger/parser.js');
|
|
234
|
-
const parseResult = parseTriggerSetArgv(triggerCreateArgs(args));
|
|
250
|
+
const parseResult = parseTriggerSetArgv(withPromptFromFile(triggerCreateArgs(args)), { allowLongPrompt: usesPromptFile });
|
|
235
251
|
if (!parseResult.ok) {
|
|
236
252
|
throw new Error(parseResult.error);
|
|
237
253
|
}
|
|
@@ -331,6 +347,16 @@ function sourceFromParsed(parsed) {
|
|
|
331
347
|
return { type: 'event', eventPattern: parsed.scheduleValue };
|
|
332
348
|
throw new Error(`unsupported scheduleType: ${parsed.scheduleType}`);
|
|
333
349
|
}
|
|
350
|
+
function executionTypeFromFlag(args) {
|
|
351
|
+
const value = requireFlag(args, '--exec');
|
|
352
|
+
if (value === 'script')
|
|
353
|
+
return 'script';
|
|
354
|
+
if (value === 'trigger-session' || value === 'trigger_session')
|
|
355
|
+
return 'trigger_session';
|
|
356
|
+
if (value === 'target-session' || value === 'target_session')
|
|
357
|
+
return 'target_session';
|
|
358
|
+
throw new Error('--exec 只接受 script、trigger-session 或 target-session');
|
|
359
|
+
}
|
|
334
360
|
function limitsFromParsed(parsed) {
|
|
335
361
|
const limits = {};
|
|
336
362
|
if (parsed.maxRuns !== undefined)
|
|
@@ -362,9 +388,10 @@ function shellQuoteArg(value) {
|
|
|
362
388
|
}
|
|
363
389
|
async function updateTrigger(args, json) {
|
|
364
390
|
const agentAid = resolveAgentAid(args);
|
|
365
|
-
const triggerId = positional(args, 0, ['--agent', '--format', '--if-revision']);
|
|
391
|
+
const triggerId = positional(args, 0, ['--agent', '--format', '--if-revision', '--prompt-file']);
|
|
392
|
+
const usesPromptFile = hasOption(args, '--prompt-file');
|
|
366
393
|
const { parseTriggerUpdateArgv } = await import('../trigger/parser.js');
|
|
367
|
-
const parsed = parseTriggerUpdateArgv(triggerId, triggerUpdateArgs(args));
|
|
394
|
+
const parsed = parseTriggerUpdateArgv(triggerId, withPromptFromFile(triggerUpdateArgs(args)), { allowLongPrompt: usesPromptFile });
|
|
368
395
|
if (!parsed.ok)
|
|
369
396
|
throw new Error(parsed.error);
|
|
370
397
|
const expectedRevision = flagValue(args, '--if-revision');
|
|
@@ -373,6 +400,7 @@ async function updateTrigger(args, json) {
|
|
|
373
400
|
agentAid,
|
|
374
401
|
triggerId,
|
|
375
402
|
patch: parsed.value,
|
|
403
|
+
...(usesPromptFile ? { promptFile: true } : {}),
|
|
376
404
|
...(expectedRevision ? { expectedRevision } : {}),
|
|
377
405
|
}, 30_000);
|
|
378
406
|
if (json)
|
|
@@ -400,6 +428,56 @@ function triggerUpdateArgs(args) {
|
|
|
400
428
|
}
|
|
401
429
|
return out;
|
|
402
430
|
}
|
|
431
|
+
function withPromptFromFile(args) {
|
|
432
|
+
const fileIndexes = [];
|
|
433
|
+
const filePaths = [];
|
|
434
|
+
for (let index = 0; index < args.length; index++) {
|
|
435
|
+
const arg = args[index];
|
|
436
|
+
if (arg === '--prompt-file') {
|
|
437
|
+
const value = args[index + 1];
|
|
438
|
+
if (!value || value.startsWith('--'))
|
|
439
|
+
throw new Error('missing --prompt-file');
|
|
440
|
+
fileIndexes.push(index, index + 1);
|
|
441
|
+
filePaths.push(value);
|
|
442
|
+
index += 1;
|
|
443
|
+
}
|
|
444
|
+
else if (arg.startsWith('--prompt-file=')) {
|
|
445
|
+
const value = arg.slice('--prompt-file='.length);
|
|
446
|
+
if (!value)
|
|
447
|
+
throw new Error('missing --prompt-file');
|
|
448
|
+
fileIndexes.push(index);
|
|
449
|
+
filePaths.push(value);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (fileIndexes.length === 0)
|
|
453
|
+
return args;
|
|
454
|
+
if (filePaths.length > 1)
|
|
455
|
+
throw new Error('--prompt-file 只能指定一次');
|
|
456
|
+
if (hasOption(args, '--prompt'))
|
|
457
|
+
throw new Error('--prompt-file 与 --prompt 不能同时指定');
|
|
458
|
+
const filePath = filePaths[0];
|
|
459
|
+
let stat;
|
|
460
|
+
try {
|
|
461
|
+
stat = fs.statSync(filePath);
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
throw new Error(`无法读取 prompt 文件 ${filePath}: ${err?.message || String(err)}`);
|
|
465
|
+
}
|
|
466
|
+
if (!stat.isFile())
|
|
467
|
+
throw new Error(`prompt 文件必须是普通文件: ${filePath}`);
|
|
468
|
+
let prompt;
|
|
469
|
+
try {
|
|
470
|
+
prompt = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
|
|
471
|
+
}
|
|
472
|
+
catch (err) {
|
|
473
|
+
throw new Error(`无法读取 prompt 文件 ${filePath}: ${err?.message || String(err)}`);
|
|
474
|
+
}
|
|
475
|
+
const fileIndexSet = new Set(fileIndexes);
|
|
476
|
+
return [
|
|
477
|
+
...args.filter((_, index) => !fileIndexSet.has(index)),
|
|
478
|
+
'--prompt', prompt,
|
|
479
|
+
];
|
|
480
|
+
}
|
|
403
481
|
async function setEnabled(args, enabled, json) {
|
|
404
482
|
const agentAid = resolveAgentAid(args);
|
|
405
483
|
const triggerId = positional(args, 0, ['--agent', '--format']);
|
|
@@ -522,6 +600,9 @@ function flagValue(args, name) {
|
|
|
522
600
|
function hasFlag(args, name) {
|
|
523
601
|
return args.includes(name);
|
|
524
602
|
}
|
|
603
|
+
function hasOption(args, name) {
|
|
604
|
+
return args.includes(name) || args.some(arg => arg.startsWith(`${name}=`));
|
|
605
|
+
}
|
|
525
606
|
function positional(args, index, flagsWithValue) {
|
|
526
607
|
const value = optionalPositional(args, index, flagsWithValue);
|
|
527
608
|
if (!value)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isValidAid } from '../aun/aid/validation.js';
|
|
2
|
+
export function validAgentOwnerIds(agent) {
|
|
3
|
+
return [...new Set((agent?.owners ?? [])
|
|
4
|
+
.map(value => String(value || '').trim())
|
|
5
|
+
.filter(isValidAid))];
|
|
6
|
+
}
|
|
7
|
+
export function validateAccessPolicyDomain(selfAid, agent) {
|
|
8
|
+
const access = agent.access;
|
|
9
|
+
if (!access)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (access.policyMode !== 'open' && validAgentOwnerIds(agent).length === 0) {
|
|
12
|
+
return {
|
|
13
|
+
code: 'restricted_mode_without_owner',
|
|
14
|
+
message: `Agent ${selfAid} cannot use ${access.policyMode} without an Owner`,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { ConfigError, ConfigTarget, mutateConfig, read, validateConfig, } from './config-manager.js';
|
|
3
|
+
import { validAgentOwnerIds, validateAccessPolicyDomain, } from './access-policy-domain.js';
|
|
4
|
+
export class AccessPolicyError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
data;
|
|
7
|
+
constructor(code, message, data) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.data = data;
|
|
11
|
+
this.name = 'AccessPolicyError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export const DEFAULT_ACCESS_POLICY = Object.freeze({
|
|
15
|
+
policyMode: 'open',
|
|
16
|
+
requestLimit: Object.freeze({ maxTimes: 600, coolDown: 60 * 60 }),
|
|
17
|
+
});
|
|
18
|
+
export function listAgentOwners(selfAid) {
|
|
19
|
+
const agent = read(ConfigTarget.Agent, { self: selfAid }, { cache: true });
|
|
20
|
+
return validAgentOwnerIds(agent);
|
|
21
|
+
}
|
|
22
|
+
/** Missing AgentConfig.access uses the default open policy; malformed Agent config never does. */
|
|
23
|
+
export function readAccessPolicy(selfAid) {
|
|
24
|
+
return readAccessPolicySnapshot(selfAid).policy;
|
|
25
|
+
}
|
|
26
|
+
export function readAccessPolicySnapshot(selfAid) {
|
|
27
|
+
const agent = read(ConfigTarget.Agent, { self: selfAid }, { cache: true });
|
|
28
|
+
return accessPolicySnapshotFromAgent(selfAid, agent);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Atomically updates AgentConfig.access through Config Manager's file-scoped
|
|
32
|
+
* mutation path. The callback always receives the newest persisted policy.
|
|
33
|
+
*/
|
|
34
|
+
export function mutateAccessPolicy(input) {
|
|
35
|
+
try {
|
|
36
|
+
const result = mutateConfig(ConfigTarget.Agent, { self: input.selfAid }, agent => {
|
|
37
|
+
const current = accessPolicySnapshotFromAgent(input.selfAid, agent);
|
|
38
|
+
if (input.expectedAccessRevision !== current.accessRevision) {
|
|
39
|
+
throw new AccessPolicyError('access_policy_revision_conflict', 'Access policy changed since it was read', {
|
|
40
|
+
expectedAccessRevision: input.expectedAccessRevision,
|
|
41
|
+
accessRevision: current.accessRevision,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const next = input.update(clonePolicy(current.policy));
|
|
45
|
+
assertAccessPolicyWrite(input, agent, next);
|
|
46
|
+
agent.access = clonePolicy(next);
|
|
47
|
+
return {
|
|
48
|
+
policy: clonePolicy(next),
|
|
49
|
+
accessRevision: accessPolicyRevision(next),
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
return { ...result.value, changed: result.changed };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof AccessPolicyError)
|
|
56
|
+
throw error;
|
|
57
|
+
if (error instanceof ConfigError && error.code === 'LOCK_TIMEOUT') {
|
|
58
|
+
throw new AccessPolicyError('access_policy_temporarily_unavailable', 'Access policy is temporarily unavailable');
|
|
59
|
+
}
|
|
60
|
+
if (error instanceof ConfigError) {
|
|
61
|
+
throw new AccessPolicyError('invalid_access_policy', error.message);
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function accessPolicySnapshotFromAgent(selfAid, agent) {
|
|
67
|
+
if (!agent) {
|
|
68
|
+
throw new AccessPolicyError('invalid_access_policy', `Agent config does not exist: ${selfAid}`);
|
|
69
|
+
}
|
|
70
|
+
const errors = validateConfig(ConfigTarget.Agent, agent);
|
|
71
|
+
if (errors.length > 0) {
|
|
72
|
+
throw new AccessPolicyError('invalid_access_policy', `Invalid access policy for ${selfAid}: ${errors.join('; ')}`);
|
|
73
|
+
}
|
|
74
|
+
const issue = validateAccessPolicyDomain(selfAid, agent);
|
|
75
|
+
if (issue)
|
|
76
|
+
throw new AccessPolicyError(issue.code, issue.message);
|
|
77
|
+
const access = agent.access ?? DEFAULT_ACCESS_POLICY;
|
|
78
|
+
const policy = clonePolicy(access);
|
|
79
|
+
return { policy, accessRevision: accessPolicyRevision(policy) };
|
|
80
|
+
}
|
|
81
|
+
function assertAccessPolicyWrite(input, agent, policy) {
|
|
82
|
+
const owners = validAgentOwnerIds(agent);
|
|
83
|
+
const normalizedActorAid = String(input.actorAid || '').trim();
|
|
84
|
+
const isOwner = input.actorRole === 'owner' || owners.includes(normalizedActorAid);
|
|
85
|
+
if (!isOwner) {
|
|
86
|
+
throw new AccessPolicyError('access_policy_owner_required', 'Only an Agent Owner can change access policy');
|
|
87
|
+
}
|
|
88
|
+
if (policy.policyMode !== 'open' && owners.length === 0) {
|
|
89
|
+
throw new AccessPolicyError('restricted_mode_without_owner', `Agent ${input.selfAid} cannot use ${policy.policyMode} without an Owner`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function clonePolicy(policy) {
|
|
93
|
+
return {
|
|
94
|
+
policyMode: policy.policyMode,
|
|
95
|
+
requestLimit: { ...policy.requestLimit },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function accessPolicyRevision(policy) {
|
|
99
|
+
return `sha256:${crypto.createHash('sha256').update(stableStringify(policy)).digest('hex')}`;
|
|
100
|
+
}
|
|
101
|
+
function stableStringify(value) {
|
|
102
|
+
return JSON.stringify(sortValue(value));
|
|
103
|
+
}
|
|
104
|
+
function sortValue(value) {
|
|
105
|
+
if (Array.isArray(value))
|
|
106
|
+
return value.map(sortValue);
|
|
107
|
+
if (!value || typeof value !== 'object')
|
|
108
|
+
return value;
|
|
109
|
+
return Object.fromEntries(Object.keys(value).sort().map(key => [key, sortValue(value[key])]));
|
|
110
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
1
|
import path from 'path';
|
|
3
2
|
import { kitsRoleTemplatesDir } from '../paths.js';
|
|
3
|
+
import { createErrorCapturingReader, fileCache } from '../core/daemon-file-cache.js';
|
|
4
4
|
import { validateRoleConfig } from './role-schema.js';
|
|
5
5
|
import { expectedRoleRank } from './role-ranks.js';
|
|
6
6
|
export const BUILTIN_ROLE_IDS = ['owner', 'admin', 'member', 'visitor'];
|
|
@@ -24,19 +24,32 @@ export function validateBuiltinRoleTemplateDefinition(roleId, raw) {
|
|
|
24
24
|
}
|
|
25
25
|
return { definition: definition };
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Load the versioned package template that defines a built-in role policy.
|
|
29
|
+
*
|
|
30
|
+
* 模板随包发布、只靠进程重启或 kit 缓存失效刷新 → fileCache 的 on-reload 策略,
|
|
31
|
+
* 归入 'kits' 组。缓存的是"读盘 + JSON.parse"结果;校验(validateRoleConfig)仍每次现做。
|
|
32
|
+
*/
|
|
28
33
|
export function readBuiltinRoleTemplate(roleId) {
|
|
29
34
|
const file = path.join(kitsRoleTemplatesDir(), `${roleId}.json`);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
// 默认 reader 会把所有读盘异常吞成 null(分不清 ENOENT 与 EACCES),
|
|
36
|
+
// 这里注入带错误捕获的 reader 把真实错误原文带出来,保持与直读时一致的报错。
|
|
37
|
+
const reader = createErrorCapturingReader();
|
|
38
|
+
const parsed = fileCache.get(file, (text) => {
|
|
39
|
+
if (text === null) {
|
|
40
|
+
return { error: `cannot read built-in role template: ${reader.lastError()}` };
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return { raw: JSON.parse(text) };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return { error: `cannot read built-in role template: ${error instanceof Error ? error.message : String(error)}` };
|
|
47
|
+
}
|
|
48
|
+
}, { policy: 'on-reload', group: 'kits', read: reader.read });
|
|
49
|
+
if ('error' in parsed)
|
|
50
|
+
return { definition: null, file, error: parsed.error };
|
|
51
|
+
// 缓存返回的是同一个对象引用,而 validateBuiltinRoleTemplateDefinition 里的解构
|
|
52
|
+
// 只隔离顶层——permissions / commandPermissions / usageLimits 等嵌套对象仍会被
|
|
53
|
+
// 所有调用方共享,任何一处改写都会污染后续读者拿到的授权策略。整份深拷贝后再校验。
|
|
54
|
+
return { ...validateBuiltinRoleTemplateDefinition(roleId, structuredClone(parsed.raw)), file };
|
|
42
55
|
}
|
|
@@ -21,17 +21,23 @@ export function getBuiltinRolesConfig() {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* Management grants are a code-level ceiling
|
|
25
|
-
*
|
|
24
|
+
* Management grants are a code-level ceiling. The built-in admin template
|
|
25
|
+
* declares the default grants; local role files may narrow them but can never
|
|
26
|
+
* add a grant outside this ceiling.
|
|
26
27
|
*/
|
|
27
28
|
export function getManagementCommandPermissions(role) {
|
|
28
29
|
return {
|
|
29
30
|
'contact.read': { allow: true, scopes: ['agent'] },
|
|
30
|
-
'contact.block': { allow: true, scopes: ['agent'] },
|
|
31
|
+
'contact.block': { allow: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
|
|
32
|
+
'connect.write': { allow: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
|
|
33
|
+
'connect.access.write': { allow: true, scopes: ['agent'], constraints: { requireAgentOwner: true } },
|
|
31
34
|
'role.assign': { allow: true, scopes: ['agent'] },
|
|
32
35
|
'role.revoke': { allow: true, scopes: ['agent'] },
|
|
33
36
|
'role.policy.read': { allow: true, scopes: ['agent'] },
|
|
34
37
|
'role.relation.read': { allow: true, scopes: ['agent'] },
|
|
38
|
+
'config.get': { allow: true, scopes: ['agent', 'relation'] },
|
|
39
|
+
'config.set': { allow: true, scopes: ['agent', 'relation'] },
|
|
40
|
+
'config.unset': { allow: true, scopes: ['agent', 'relation'] },
|
|
35
41
|
'role.policy.write': { allow: true, dangerous: true, scopes: ['agent'], constraints: { requireAgentAdmin: true } },
|
|
36
42
|
'*': { allow: true },
|
|
37
43
|
'agent.reload': { allow: true, dangerous: true, scopes: ['agent'], constraints: { targetCurrentAgentOnly: true, requireAgentAdmin: true } },
|
|
@@ -39,6 +45,7 @@ export function getManagementCommandPermissions(role) {
|
|
|
39
45
|
};
|
|
40
46
|
}
|
|
41
47
|
export function applyManagementRoleCeiling(role, configured) {
|
|
48
|
+
const builtin = readBuiltinRoleTemplate(role).definition;
|
|
42
49
|
const fallback = {
|
|
43
50
|
description: role === 'owner' ? 'Agent owner' : 'Agent administrator',
|
|
44
51
|
rank: expectedRoleRank(role),
|
|
@@ -48,12 +55,16 @@ export function applyManagementRoleCeiling(role, configured) {
|
|
|
48
55
|
},
|
|
49
56
|
commandPermissions: getManagementCommandPermissions(role),
|
|
50
57
|
};
|
|
51
|
-
const policy = configured ??
|
|
58
|
+
const policy = configured ?? builtin ?? fallback;
|
|
59
|
+
const configuredCommandPermissions = policy.commandPermissions;
|
|
60
|
+
const defaultCommandPermissions = configuredCommandPermissions
|
|
61
|
+
?? (role === 'admin' && builtin ? builtin.commandPermissions : undefined)
|
|
62
|
+
?? getManagementCommandPermissions(role);
|
|
52
63
|
return {
|
|
53
64
|
...policy,
|
|
54
65
|
rank: expectedRoleRank(role),
|
|
55
|
-
allowAccess: true,
|
|
56
|
-
commandPermissions: narrowManagementCommandPermissions(getManagementCommandPermissions(role),
|
|
66
|
+
allowAccess: role === 'owner' ? true : policy.allowAccess !== false,
|
|
67
|
+
commandPermissions: narrowManagementCommandPermissions(getManagementCommandPermissions(role), defaultCommandPermissions),
|
|
57
68
|
};
|
|
58
69
|
}
|
|
59
70
|
export function getManagementRoleDefinition(role) {
|
|
@@ -78,6 +89,14 @@ function narrowManagementCommandPermissions(ceiling, configured) {
|
|
|
78
89
|
else if (!Array.isArray(permission.scopes) && Array.isArray(override.scopes)) {
|
|
79
90
|
next.scopes = [...override.scopes];
|
|
80
91
|
}
|
|
92
|
+
if (permission.constraints || override.constraints) {
|
|
93
|
+
// Configured constraints may add restrictions, but a role file cannot
|
|
94
|
+
// weaken a code-level constraint (the ceiling wins on collisions).
|
|
95
|
+
next.constraints = {
|
|
96
|
+
...(override.constraints ?? {}),
|
|
97
|
+
...(permission.constraints ?? {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
81
100
|
merged[operation] = next;
|
|
82
101
|
}
|
|
83
102
|
// Exact or namespace denies may narrow grants inherited through the ceiling's
|
|
@@ -8,6 +8,7 @@ const BEHAVIOR_TOP_FIELDS = new Set([
|
|
|
8
8
|
'debounce',
|
|
9
9
|
'show_activities',
|
|
10
10
|
'session_renew',
|
|
11
|
+
'groupRules',
|
|
11
12
|
'group_venue_sync',
|
|
12
13
|
'render',
|
|
13
14
|
'sessionManifests',
|
|
@@ -32,6 +33,7 @@ const SENSITIVE_TOP_FIELDS = new Set([
|
|
|
32
33
|
'lifecycle',
|
|
33
34
|
'initialized',
|
|
34
35
|
'owners',
|
|
36
|
+
'access',
|
|
35
37
|
'admins',
|
|
36
38
|
'channels',
|
|
37
39
|
'aun',
|
|
@@ -44,10 +46,10 @@ const SENSITIVE_TOP_FIELDS = new Set([
|
|
|
44
46
|
'role',
|
|
45
47
|
'extra_backup',
|
|
46
48
|
]);
|
|
47
|
-
const CRITICAL_AGENT_FIELDS = new Set(['owners', 'admins', 'roles']);
|
|
49
|
+
const CRITICAL_AGENT_FIELDS = new Set(['owners', 'admins', 'roles', 'access']);
|
|
48
50
|
const EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
49
|
-
function scalar(rolePermissionKey, valueKind) {
|
|
50
|
-
return { class: 'safe-scalar', rolePermissionKey, valueKind };
|
|
51
|
+
function scalar(rolePermissionKey, valueKind, access = 'role-overridable') {
|
|
52
|
+
return { class: 'safe-scalar', access, rolePermissionKey, valueKind };
|
|
51
53
|
}
|
|
52
54
|
function readonlyObject(rolePermissionKey) {
|
|
53
55
|
return { class: 'safe-readonly-object', rolePermissionKey };
|
|
@@ -133,8 +135,6 @@ export function resolveConfigFieldRule(fieldPath) {
|
|
|
133
135
|
return scalar('session_renew', 'boolean');
|
|
134
136
|
if (parts[1] === 'after_hours')
|
|
135
137
|
return scalar('session_renew', 'positive-number');
|
|
136
|
-
if (parts[1] === 'effort')
|
|
137
|
-
return scalar('session_renew', 'effort');
|
|
138
138
|
if (parts[1] === 'fallback_action')
|
|
139
139
|
return scalar('session_renew', 'session-renew-action');
|
|
140
140
|
return { class: 'unknown' };
|
|
@@ -153,6 +153,14 @@ export function resolveConfigFieldRule(fieldPath) {
|
|
|
153
153
|
return scalar('sessionManifests', 'manifest-file');
|
|
154
154
|
return { class: 'unknown' };
|
|
155
155
|
}
|
|
156
|
+
if (top === 'groupRules') {
|
|
157
|
+
if (parts.length === 1)
|
|
158
|
+
return readonlyObject('groupRules');
|
|
159
|
+
if (field === 'groupRules.mode') {
|
|
160
|
+
return scalar(field, 'group-rules-mode', 'owner-only');
|
|
161
|
+
}
|
|
162
|
+
return { class: 'unknown' };
|
|
163
|
+
}
|
|
156
164
|
if (top === 'group_venue_sync')
|
|
157
165
|
return parts.length === 1
|
|
158
166
|
? readonlyObject('group_venue_sync')
|
|
@@ -257,5 +265,9 @@ export function parseConfigFieldValue(fieldPath, rawValue) {
|
|
|
257
265
|
return raw === 'continue' || raw === 'new'
|
|
258
266
|
? { ok: true, value: raw }
|
|
259
267
|
: { ok: false, reason: `Config field ${fieldPath} must be continue or new` };
|
|
268
|
+
case 'group-rules-mode':
|
|
269
|
+
return raw === 'load' || raw === 'ignore'
|
|
270
|
+
? { ok: true, value: raw }
|
|
271
|
+
: { ok: false, reason: `Config field ${fieldPath} must be load or ignore` };
|
|
260
272
|
}
|
|
261
273
|
}
|