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.
Files changed (148) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +1 -31
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +157 -5
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/channels/aun.js +220 -112
  14. package/dist/channels/daemon.js +18 -4
  15. package/dist/channels/dingtalk.js +52 -137
  16. package/dist/channels/feishu.js +56 -4
  17. package/dist/channels/qqbot.js +23 -1
  18. package/dist/channels/wechat.js +303 -155
  19. package/dist/channels/wecom.js +383 -7
  20. package/dist/cli/aun-commands.js +11 -11
  21. package/dist/cli/daemon-commands.js +46 -15
  22. package/dist/cli/handoff-command.js +2 -1
  23. package/dist/cli/init-channel.js +185 -67
  24. package/dist/cli/init.js +49 -27
  25. package/dist/cli/trigger-command.js +92 -11
  26. package/dist/config/access-policy-domain.js +18 -0
  27. package/dist/config/access-policy.js +110 -0
  28. package/dist/config/builtin-role-templates.js +27 -14
  29. package/dist/config/builtin-roles.js +25 -6
  30. package/dist/config/config-field-policy.js +17 -5
  31. package/dist/config/config-manager.js +198 -22
  32. package/dist/config/config-operation-service.js +35 -38
  33. package/dist/config/contact-bind-code.js +274 -0
  34. package/dist/config/contact-book-store.js +173 -5
  35. package/dist/config/contact-book.js +32 -0
  36. package/dist/config/contact-operation-service.js +9 -3
  37. package/dist/config/contact-request-service.js +331 -0
  38. package/dist/config/peer-role-resolver.js +3 -1
  39. package/dist/config/schema-registry.js +49 -24
  40. package/dist/config-store.js +14 -2
  41. package/dist/core/auth/agent-delegation.js +105 -11
  42. package/dist/core/auth/authorization-audit.js +6 -0
  43. package/dist/core/auth/operation-authorizer.js +8 -0
  44. package/dist/core/auth/operation-catalog.js +51 -3
  45. package/dist/core/auth/trigger-authorization.js +15 -0
  46. package/dist/core/bootstrap-service.js +2 -9
  47. package/dist/core/channel-loader.js +6 -2
  48. package/dist/core/command/command-handler.js +10 -13
  49. package/dist/core/command/connect-menu.js +229 -26
  50. package/dist/core/command/evol-menu-version-gate.js +38 -0
  51. package/dist/core/command/group-menu.js +421 -0
  52. package/dist/core/command/menu-handler.js +269 -85
  53. package/dist/core/command/menu-protocol.js +8 -2
  54. package/dist/core/command/menu-token-store.js +102 -0
  55. package/dist/core/command/role-menu.js +16 -0
  56. package/dist/core/command/slash-gate.js +1 -1
  57. package/dist/core/command/slash-handler.js +158 -31
  58. package/dist/core/daemon-file-cache.js +28 -0
  59. package/dist/core/event-catalog.js +29 -0
  60. package/dist/core/evolagent-registry.js +4 -39
  61. package/dist/core/handoff/runtime.js +162 -37
  62. package/dist/core/handoff/store.js +46 -2
  63. package/dist/core/handoff/types.js +1 -0
  64. package/dist/core/inference/text-inference.js +16 -74
  65. package/dist/core/message/file-markers.js +7 -0
  66. package/dist/core/message/im-renderer.js +18 -14
  67. package/dist/core/message/inbound-admission.js +134 -0
  68. package/dist/core/message/message-bridge.js +616 -74
  69. package/dist/core/message/message-log.js +1 -0
  70. package/dist/core/message/message-queue.js +185 -10
  71. package/dist/core/message/peer-mode.js +7 -8
  72. package/dist/core/message/response-engine.js +381 -79
  73. package/dist/core/permission/approval-gateway.js +17 -10
  74. package/dist/core/permission/ec-command-parser.js +101 -46
  75. package/dist/core/permission/tool-policy.js +69 -24
  76. package/dist/core/protected-paths.js +36 -11
  77. package/dist/core/session/session-fs-store.js +19 -4
  78. package/dist/core/session/session-manager.js +232 -4
  79. package/dist/core/session/session-mapper.js +2 -0
  80. package/dist/core/session/session-renew.js +125 -69
  81. package/dist/core/session/session-turn-coordinator.js +5 -1
  82. package/dist/eck/kit-renderer.js +12 -1
  83. package/dist/eck/message-renderer.js +79 -1
  84. package/dist/index.js +224 -89
  85. package/dist/ipc.js +81 -4
  86. package/dist/paths.js +3 -0
  87. package/dist/response-system/config-resolver.js +29 -0
  88. package/dist/response-system/coordinator.js +8 -32
  89. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  90. package/dist/response-system/index.js +1 -0
  91. package/dist/response-system/modes/single-session/index.js +7 -4
  92. package/dist/trigger/parser.js +55 -15
  93. package/dist/trigger/patch.js +4 -1
  94. package/dist/trigger/scheduler.js +441 -39
  95. package/dist/utils/cross-platform.js +8 -2
  96. package/dist/utils/error-dict.json +7 -0
  97. package/dist/utils/evolcore-version.js +21 -0
  98. package/dist/utils/logger.js +2 -2
  99. package/dist/utils/process-introspect.js +19 -3
  100. package/dist/utils/stable-semver.js +21 -0
  101. package/dist/utils/stats.js +33 -9
  102. package/kits/docs/channels/aun.md +4 -13
  103. package/kits/docs/evolcore/INDEX.md +1 -1
  104. package/kits/docs/evolcore/config.md +47 -2
  105. package/kits/docs/evolcore/contact.md +8 -3
  106. package/kits/docs/evolcore/group-rules.md +46 -4
  107. package/kits/docs/evolcore/group.md +4 -4
  108. package/kits/docs/evolcore/msg.md +5 -5
  109. package/kits/docs/evolcore/trigger.md +25 -8
  110. package/kits/docs/path-registry.md +36 -17
  111. package/kits/eck_message_manifest.json +12 -1
  112. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  113. package/kits/rules/01-overview.md +17 -7
  114. package/kits/rules/02-navigation.md +38 -18
  115. package/kits/rules/03-identity.md +28 -24
  116. package/kits/rules/04-relation.md +44 -28
  117. package/kits/rules/05-venue.md +31 -15
  118. package/kits/rules/06-channel.md +11 -7
  119. package/kits/schemas/_meta.json +18 -7
  120. package/kits/schemas/agent-config.schema.7.json +303 -0
  121. package/kits/schemas/agent-config.schema.8.json +304 -0
  122. package/kits/schemas/agent-config.schema.9.json +364 -0
  123. package/kits/schemas/contact-book.schema.3.json +67 -0
  124. package/kits/schemas/daemon.schema.1.json +3 -2
  125. package/kits/schemas/daemon.schema.2.json +101 -0
  126. package/kits/schemas/daemon.schema.3.json +123 -0
  127. package/kits/schemas/defaults.schema.2.json +85 -0
  128. package/kits/schemas/defaults.schema.3.json +73 -0
  129. package/kits/schemas/relation-config.schema.6.json +46 -0
  130. package/kits/schemas/relation-config.schema.7.json +59 -0
  131. package/kits/schemas/single-session.schema.2.json +57 -0
  132. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  133. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  134. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  135. package/kits/templates/roles/admin.json +46 -0
  136. package/kits/templates/roles/member.json +4 -0
  137. package/kits/templates/roles/visitor.json +4 -0
  138. package/kits/templates/system-fragments/channel.md +12 -2
  139. package/kits/templates/system-fragments/identity.md +3 -1
  140. package/kits/templates/system-fragments/relation.md +1 -1
  141. package/kits/templates/system-fragments/session.md +6 -2
  142. package/package.json +4 -2
  143. package/MIGRATION-0.5.0.md +0 -378
  144. package/ROLE_ACCESS_CONTROL.md +0 -174
  145. package/dist/channels/contact-bind-code.js +0 -134
  146. package/dist/channels/wecom-card.js +0 -101
  147. package/dist/channels/wecom-onboarding.js +0 -82
  148. 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
- // Support two modes: --file (full JSON) or flags (parser-based)
218
- if (hasFlag(args, '--file')) {
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
- /** Load the versioned package template that defines a built-in role policy. */
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
- let raw;
31
- try {
32
- raw = JSON.parse(fs.readFileSync(file, 'utf8'));
33
- }
34
- catch (error) {
35
- return {
36
- definition: null,
37
- file,
38
- error: `cannot read built-in role template: ${error instanceof Error ? error.message : String(error)}`,
39
- };
40
- }
41
- return { ...validateBuiltinRoleTemplateDefinition(roleId, raw), file };
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, not configurable role policy.
25
- * A role file may narrow these rules but can never add a management grant.
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 ?? readBuiltinRoleTemplate(role).definition ?? fallback;
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), policy.commandPermissions || {}),
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
  }