evolcore 0.0.28 → 0.0.29
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 +12 -0
- package/bin/ec-safe-output.js +270 -54
- package/dist/agents/baseagent-availability.js +37 -1
- package/dist/agents/baseagent.js +19 -9
- package/dist/agents/claude-runner.js +171 -13
- package/dist/agents/codex/app-server-client.js +6 -2
- package/dist/agents/codex/diagnostics.js +4 -3
- package/dist/agents/codex-runner.js +396 -95
- package/dist/agents/gemini-runner.js +35 -17
- package/dist/aun/group/history.js +130 -90
- package/dist/aun/group/operations.js +1 -3
- package/dist/aun/menu/transport.js +5 -3
- package/dist/aun/msg/managed-operation.js +70 -0
- package/dist/aun/msg/p2p.js +1 -3
- package/dist/aun/msg/upload.js +2 -2
- package/dist/aun/outbox.js +1 -37
- package/dist/channels/aun.js +249 -89
- package/dist/channels/daemon.js +18 -1
- package/dist/channels/feishu.js +1 -2
- package/dist/cli/agent-command.js +23 -3
- package/dist/cli/agent-create.js +68 -2
- package/dist/cli/agent.js +12 -2
- package/dist/cli/aun-commands.js +87 -13
- package/dist/cli/cli-argv.js +26 -0
- package/dist/cli/daemon-commands.js +40 -10
- package/dist/cli/data-command.js +4 -4
- package/dist/cli/handoff-command.js +2 -0
- package/dist/cli/index.js +18 -1
- package/dist/cli/managed-command-guard.js +10 -0
- package/dist/cli/memory-command.js +441 -0
- package/dist/cli/restart-monitor.js +62 -6
- package/dist/cli/skill-command.js +41 -0
- package/dist/cli/task-context.js +2 -23
- package/dist/cli/trigger-command.js +9 -1
- package/dist/cli/watch-msg-diag.js +209 -0
- package/dist/config/config-field-policy.js +52 -3
- package/dist/config/config-manager.js +17 -4
- package/dist/config/contact-book-store.js +2 -2
- package/dist/config/mention-mode.js +7 -2
- package/dist/config/merge.js +28 -9
- package/dist/config/resolved-config-op.js +1 -1
- package/dist/config/runtime-env.js +137 -0
- package/dist/config/schema-registry.js +11 -1
- package/dist/config-store.js +10 -10
- package/dist/core/audit/event-key.js +3 -1
- package/dist/core/audit/log-integrity.js +4 -0
- package/dist/core/auth/agent-delegation.js +2 -1
- package/dist/core/auth/auth-gateway.js +13 -6
- package/dist/core/auth/authorization-audit.js +54 -19
- package/dist/core/auth/operation-authorizer.js +17 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/causation/context.js +5 -1
- package/dist/core/command/agent-control.js +1 -2
- package/dist/core/command/command-handler.js +51 -23
- package/dist/core/command/memory-slash.js +270 -0
- package/dist/core/command/menu-catalog.js +1 -0
- package/dist/core/command/menu-handler.js +315 -60
- package/dist/core/command/menu-protocol.js +42 -4
- package/dist/core/command/slash-gate.js +10 -10
- package/dist/core/command/slash-handler.js +206 -64
- package/dist/core/data-migration.js +282 -505
- package/dist/core/event-catalog.js +26 -0
- package/dist/core/evolagent-registry.js +6 -0
- package/dist/core/evolagent.js +0 -10
- package/dist/core/handoff/store.js +1 -20
- package/dist/core/inference/text-inference.js +57 -1
- package/dist/core/memory/consolidation-context.js +505 -0
- package/dist/core/memory/consolidation-ops-records.js +124 -0
- package/dist/core/memory/consolidation-trigger.js +394 -0
- package/dist/core/memory/index-manager.js +308 -0
- package/dist/core/memory/launch-consolidation.js +395 -0
- package/dist/core/memory/memory-admin.js +234 -0
- package/dist/core/memory/memory-document.js +100 -0
- package/dist/core/memory/memory-session-policy.js +41 -0
- package/dist/core/memory/memory-signature.js +257 -0
- package/dist/core/memory/memory-writer.js +432 -0
- package/dist/core/memory/staleness.js +117 -0
- package/dist/core/message/auto-mention-gate.js +454 -0
- package/dist/core/message/im-renderer.js +69 -16
- package/dist/core/message/message-bridge.js +287 -21
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +114 -27
- package/dist/core/message/pending-hints.js +329 -161
- package/dist/core/message/response-engine.js +567 -80
- package/dist/core/permission/ec-command-parser.js +1029 -80
- package/dist/core/permission/protected-paths.js +376 -126
- package/dist/core/permission/sandbox-runtime.js +40 -12
- package/dist/core/permission/tool-error-code.js +96 -13
- package/dist/core/permission/tool-policy.js +276 -23
- package/dist/core/role/runtime-policy.js +3 -7
- package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
- package/dist/core/session/session-fs-store.js +0 -76
- package/dist/core/session/session-idle-monitor.js +355 -0
- package/dist/core/session/session-manager.js +478 -17
- package/dist/core/session/session-mapper.js +33 -16
- package/dist/core/session/session-renew.js +217 -155
- package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
- package/dist/core/skill/global-watcher.js +282 -0
- package/dist/core/skill/index-generator.js +84 -0
- package/dist/core/skill/startup.js +73 -0
- package/dist/core/skill/switch-resolver.js +115 -0
- package/dist/core/task/task-activity-store.js +219 -0
- package/dist/eck/kit-renderer.js +30 -3
- package/dist/eck/manifest-engine.js +26 -4
- package/dist/index.js +708 -18
- package/dist/ipc.js +564 -2
- package/dist/paths.js +30 -9
- package/dist/response-system/engines/v1/proactive-flow.js +116 -15
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/feedback.js +1 -0
- package/dist/trigger/manager.js +0 -10
- package/dist/trigger/scheduler.js +11 -0
- package/dist/trigger/validation.js +22 -0
- package/dist/utils/avatar-upload.js +59 -18
- package/dist/utils/error-dict.json +6 -3
- package/dist/utils/error-utils.js +27 -0
- package/dist/utils/logger.js +32 -10
- package/dist/utils/media-cache.js +84 -0
- package/dist/utils/msg-diag.js +97 -0
- package/dist/utils/welcome.js +37 -8
- package/kits/docs/AUTHORING.md +314 -0
- package/kits/docs/GUIDE.md +2 -0
- package/kits/docs/INDEX.md +2 -0
- package/kits/docs/aun/CHEATSHEET.md +4 -0
- package/kits/docs/aun/SDK.md +60 -0
- package/kits/docs/context-assembly.md +10 -6
- package/kits/docs/evolcore/INDEX.md +17 -3
- package/kits/docs/evolcore/agent-create.md +24 -0
- package/kits/docs/evolcore/agent.md +5 -2
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/ec-capabilities.md +364 -0
- package/kits/docs/evolcore/group.md +34 -18
- package/kits/docs/evolcore/memory-architecture.md +78 -0
- package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
- package/kits/docs/evolcore/memory-consolidation.md +263 -0
- package/kits/docs/evolcore/memory-tags.md +63 -0
- package/kits/docs/evolcore/memory.md +110 -0
- package/kits/docs/evolcore/menu-client.md +27 -11
- package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
- package/kits/docs/evolcore/menu-protocol.md +213 -0
- package/kits/docs/evolcore/msg.md +2 -0
- package/kits/docs/evolcore/trigger.md +7 -5
- package/kits/docs/path-registry.md +6 -8
- package/kits/docs/prompt-loading-architecture.md +5 -3
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +294 -88
- package/kits/eck_manifest.memory.json +257 -0
- package/kits/rules/03-identity.md +8 -8
- package/kits/rules/05-venue.md +4 -2
- package/kits/rules/06-channel.md +1 -1
- package/kits/schemas/_meta.json +276 -47
- package/kits/schemas/agent-config.schema.12.json +38 -2
- package/kits/schemas/agent-config.schema.13.json +591 -0
- package/kits/schemas/daemon.schema.6.json +9 -9
- package/kits/schemas/defaults.schema.5.json +29 -0
- package/kits/schemas/relation-config.schema.8.json +14 -1
- package/kits/schemas/relation-config.schema.9.json +129 -0
- package/kits/schemas/role-config.schema.1.json +3 -3
- package/kits/skills/.switches.json +3 -0
- package/kits/skills/ec-skill/SKILL.md +217 -0
- package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
- package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
- package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
- package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
- package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
- package/kits/templates/decision/auto-mention.md +25 -0
- package/kits/templates/decision/session-renew.md +22 -0
- package/kits/templates/memory-consolidation/forget.md +104 -0
- package/kits/templates/memory-consolidation/generalize.md +76 -0
- package/kits/templates/memory-consolidation/notes.md +133 -0
- package/kits/templates/memory-consolidation/profile.md +58 -0
- package/kits/templates/memory-consolidation/promote.md +72 -0
- package/kits/templates/memory-consolidation/workflow.md +75 -0
- package/kits/templates/system-fragments/bootstrap.md +14 -5
- package/kits/templates/system-fragments/commands.md +1 -0
- package/kits/templates/system-fragments/group-history.md +6 -0
- package/kits/templates/system-fragments/identity.md +0 -3
- package/kits/templates/system-fragments/memory.md +79 -0
- package/kits/templates/system-fragments/reply.md +1 -0
- package/kits/templates/system-fragments/venue.md +1 -1
- package/package.json +4 -6
- package/dist/trigger/legacy-session-history.js +0 -19
- package/kits/templates/system-fragments/group-unread-history.md +0 -7
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared representation for memory Markdown documents.
|
|
3
|
+
*
|
|
4
|
+
* Memory files have two mutable blocks: YAML-like frontmatter and Markdown
|
|
5
|
+
* body. AUN signature blocks are stripped while parsing and are never exposed
|
|
6
|
+
* as a document block; consolidated writers sign the serialized result again.
|
|
7
|
+
*/
|
|
8
|
+
import { parseSignatureBlock } from './memory-signature.js';
|
|
9
|
+
function parseArray(raw) {
|
|
10
|
+
const value = raw.match(/^\[([^\]]*)\]$/)?.[1] ?? raw;
|
|
11
|
+
return value
|
|
12
|
+
.split(',')
|
|
13
|
+
.map(item => item.trim().replace(/^['"]|['"]$/g, ''))
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
function formatValue(value) {
|
|
17
|
+
return Array.isArray(value) ? `[${value.join(', ')}]` : value;
|
|
18
|
+
}
|
|
19
|
+
function splitDocument(content) {
|
|
20
|
+
const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
21
|
+
if (!normalized.startsWith('---\n'))
|
|
22
|
+
return { frontmatter: [], body: normalized, hasFrontmatter: false };
|
|
23
|
+
const end = normalized.indexOf('\n---\n', 4);
|
|
24
|
+
if (end < 0)
|
|
25
|
+
return { frontmatter: [], body: normalized, hasFrontmatter: false };
|
|
26
|
+
return {
|
|
27
|
+
frontmatter: normalized.slice(4, end).split('\n'),
|
|
28
|
+
body: normalized.slice(end + 5).replace(/^\n/, ''),
|
|
29
|
+
hasFrontmatter: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Parse frontmatter and body; an existing AUN signature is ignored. */
|
|
33
|
+
export function parseMemoryDocument(content) {
|
|
34
|
+
const signature = parseSignatureBlock(content);
|
|
35
|
+
// A malformed marker may be ordinary body text. Only strip a signature when
|
|
36
|
+
// the signature parser accepted the block; otherwise preserve every byte.
|
|
37
|
+
const unsigned = signature.fields ? signature.payload : content;
|
|
38
|
+
const parts = splitDocument(unsigned);
|
|
39
|
+
const fields = {};
|
|
40
|
+
const fieldOrder = [];
|
|
41
|
+
const extraFrontmatterLines = [];
|
|
42
|
+
for (const line of parts.frontmatter) {
|
|
43
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
44
|
+
if (!match) {
|
|
45
|
+
extraFrontmatterLines.push(line);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const [, key, raw] = match;
|
|
49
|
+
if (!(key in fields))
|
|
50
|
+
fieldOrder.push(key);
|
|
51
|
+
fields[key] = raw.startsWith('[') ? parseArray(raw) : raw.trim();
|
|
52
|
+
}
|
|
53
|
+
return { fields, fieldOrder, extraFrontmatterLines, body: parts.body, hasFrontmatter: parts.hasFrontmatter };
|
|
54
|
+
}
|
|
55
|
+
export function getMemoryField(document, key) {
|
|
56
|
+
return document.fields[key];
|
|
57
|
+
}
|
|
58
|
+
export function getMemoryString(document, key) {
|
|
59
|
+
const value = getMemoryField(document, key);
|
|
60
|
+
return typeof value === 'string' ? value : undefined;
|
|
61
|
+
}
|
|
62
|
+
export function getMemoryArray(document, key) {
|
|
63
|
+
const value = getMemoryField(document, key);
|
|
64
|
+
return Array.isArray(value) ? [...value] : value ? parseArray(value) : [];
|
|
65
|
+
}
|
|
66
|
+
export function replaceMemoryBlock(document, block, value) {
|
|
67
|
+
if (block === 'body') {
|
|
68
|
+
document.body = value;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const parsed = parseMemoryDocument(`---\n${value}\n---\n\n${document.body}`);
|
|
72
|
+
document.fields = parsed.fields;
|
|
73
|
+
document.fieldOrder = parsed.fieldOrder;
|
|
74
|
+
document.extraFrontmatterLines = parsed.extraFrontmatterLines;
|
|
75
|
+
document.hasFrontmatter = true;
|
|
76
|
+
}
|
|
77
|
+
export function getMemoryBlock(document, block) {
|
|
78
|
+
if (block === 'body')
|
|
79
|
+
return document.body;
|
|
80
|
+
return document.fieldOrder
|
|
81
|
+
.filter(key => key in document.fields)
|
|
82
|
+
.map(key => `${key}: ${formatValue(document.fields[key])}`)
|
|
83
|
+
.concat(document.extraFrontmatterLines)
|
|
84
|
+
.join('\n');
|
|
85
|
+
}
|
|
86
|
+
export function setMemoryField(document, key, value) {
|
|
87
|
+
if (value === undefined) {
|
|
88
|
+
delete document.fields[key];
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!(key in document.fields))
|
|
92
|
+
document.fieldOrder.push(key);
|
|
93
|
+
document.fields[key] = value;
|
|
94
|
+
}
|
|
95
|
+
export function serializeMemoryDocument(document) {
|
|
96
|
+
const frontmatter = getMemoryBlock(document, 'frontmatter');
|
|
97
|
+
if (!document.hasFrontmatter && !frontmatter)
|
|
98
|
+
return `${document.body.trimEnd()}\n`;
|
|
99
|
+
return `---\n${frontmatter}\n---\n\n${document.body.trimEnd()}\n`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-session-policy.ts — 整理会话(sessionType=memory)的工具策略
|
|
3
|
+
*
|
|
4
|
+
* 整理会话的产出只能经 `ec memory create|update|archive` 走 daemon IPC 落盘
|
|
5
|
+
* (签名、索引、ops 流水都在那一侧)。所以:
|
|
6
|
+
* - 直接写文件的工具一律拦(Claude 的 Write/Edit/MultiEdit/NotebookEdit,Codex 的 FileChange)
|
|
7
|
+
* - 子代理、PowerShell 这类能绕开本策略的入口也拦
|
|
8
|
+
* - Bash 只放行 `ec memory create|update|archive`;解析不出单条字面命令(含 ; && | $() 等)一律拦
|
|
9
|
+
* - Read / Glob / Grep 等只读工具不在这里管
|
|
10
|
+
*
|
|
11
|
+
* 作为 policyHook 由 response-engine 在整理会话里挂上,三个 runner 共用。
|
|
12
|
+
*/
|
|
13
|
+
import { parseCodexToolCommand } from '../permission/ec-command-parser.js';
|
|
14
|
+
const BLOCKED_TOOLS = new Set([
|
|
15
|
+
'Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'FileChange',
|
|
16
|
+
'Agent', 'Task', 'PowerShell',
|
|
17
|
+
]);
|
|
18
|
+
const ALLOWED_MEMORY_SUBCOMMANDS = new Set(['create', 'update', 'archive', 'feedback']);
|
|
19
|
+
export const MEMORY_SESSION_DENY_MESSAGE = '整理会话不允许执行此命令';
|
|
20
|
+
export function memorySessionPolicyHook(toolName, toolInput) {
|
|
21
|
+
if (BLOCKED_TOOLS.has(toolName)) {
|
|
22
|
+
return {
|
|
23
|
+
block: true,
|
|
24
|
+
policyCode: 'memory_session_tool_forbidden',
|
|
25
|
+
reason: `${MEMORY_SESSION_DENY_MESSAGE}:整理会话不能使用 ${toolName},写记忆只能用 \`ec memory create|update|archive\`,反馈用 \`ec memory feedback\``,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (toolName !== 'Bash')
|
|
29
|
+
return undefined;
|
|
30
|
+
const parsed = parseCodexToolCommand(toolInput);
|
|
31
|
+
const argv = parsed.ok ? parsed.argv : undefined;
|
|
32
|
+
if (argv && argv[0] === 'ec' && argv[1] === 'memory' && ALLOWED_MEMORY_SUBCOMMANDS.has(argv[2] ?? '')) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
block: true,
|
|
37
|
+
policyCode: 'memory_session_command_forbidden',
|
|
38
|
+
reason: `${MEMORY_SESSION_DENY_MESSAGE}:Bash 只允许单条 \`ec memory create|update|archive|feedback\`` +
|
|
39
|
+
'(不能拼接其他命令;正文含特殊字符时用单引号包住)。读文件请用 Read / Glob / Grep。',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-signature.ts — 记忆文件签名与验签
|
|
3
|
+
*
|
|
4
|
+
* **不重用 agent.md 的签名链路。** `signAgentMd` / `verifyAgentMd` 会走
|
|
5
|
+
* agent-md-schema 的校验(强制 frontmatter 有 aid/name/type/version、payload ≤ 4096B),
|
|
6
|
+
* 套在记忆文件上必然失败——而且 `signAgentMd` 签名时**不做校验**,
|
|
7
|
+
* 签出来的文件自己验不回来,属于静默产废品。
|
|
8
|
+
*
|
|
9
|
+
* 这里改用 fastaun 的底层字节级接口:
|
|
10
|
+
* - `AID.sign(payload)` → `{ signature: base64 }`,纯字节签名,无大小上限
|
|
11
|
+
* - `AID.verify(payload, sig)` → `{ valid: boolean }`,纯字节验签
|
|
12
|
+
*
|
|
13
|
+
* 签名块格式沿用 AUN 既有的 `<!-- AUN-SIGNATURE ... -->`,便于工具链复用。
|
|
14
|
+
* 但拼装 / 解析 / 证书绑定校验三个工具函数在 fastaun 里**没有导出**
|
|
15
|
+
* (包 exports 只映射了 "."),所以在本模块内镜像实现。
|
|
16
|
+
*
|
|
17
|
+
* 签名身份:记忆所属 agent 自身的 AID 证书私钥。
|
|
18
|
+
* 验签:只加载本地证书,无需网络。
|
|
19
|
+
*/
|
|
20
|
+
import { X509Certificate, createHash } from 'node:crypto';
|
|
21
|
+
import { getAidStore, loadAid, SLOT } from '../../aun/aid/store.js';
|
|
22
|
+
import { aunPath } from '../../paths.js';
|
|
23
|
+
/**
|
|
24
|
+
* 记忆文件大小上限(字节)。超过即拒绝签名。
|
|
25
|
+
*
|
|
26
|
+
* 定这个限制的理由不是技术限制(`AID.sign` 本身无上限,实测 200KB 通过),
|
|
27
|
+
* 而是记忆本身的性质:一条记忆应是**一句话能说清的结论**,不是文档。
|
|
28
|
+
* 超过 10KB 通常意味着应该拆成多条,或者根本不该进记忆系统。
|
|
29
|
+
*/
|
|
30
|
+
export const MAX_MEMORY_BYTES = 10 * 1024;
|
|
31
|
+
// ── 镜像自 fastaun/dist/cert-utils.js ─────────────────────────────────────
|
|
32
|
+
const SIGNATURE_MARKER = '<!-- AUN-SIGNATURE';
|
|
33
|
+
const SIGNATURE_RE = /^<!-- AUN-SIGNATURE(?:\r\n|\n|\r)(?<body>[\s\S]*?)(?:\r\n|\n|\r)-->\s*$/;
|
|
34
|
+
const FINGERPRINT_HEX_RE = /^[0-9a-f]+$/;
|
|
35
|
+
const REQUIRED_FIELDS = ['cert_fingerprint', 'timestamp', 'signature'];
|
|
36
|
+
const ALLOWED_FIELDS = [...REQUIRED_FIELDS, 'public_key_fingerprint'];
|
|
37
|
+
/** 严格 base64 校验(长度是 4 的倍数、字符集合法、可往返)。 */
|
|
38
|
+
function isStrictBase64(value) {
|
|
39
|
+
const text = String(value ?? '');
|
|
40
|
+
if (!text || text.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(text))
|
|
41
|
+
return false;
|
|
42
|
+
return Buffer.from(text, 'base64').toString('base64') === text;
|
|
43
|
+
}
|
|
44
|
+
/** 归一化指纹,去掉 `sha256:` 前缀和冒号分隔,返回小写 hex。 */
|
|
45
|
+
function normalizeFingerprintHex(value) {
|
|
46
|
+
let text = String(value ?? '').trim().toLowerCase();
|
|
47
|
+
if (text.startsWith('sha256:'))
|
|
48
|
+
text = text.slice('sha256:'.length);
|
|
49
|
+
text = text.replace(/:/g, '');
|
|
50
|
+
if (![16, 64].includes(text.length) || !FINGERPRINT_HEX_RE.test(text))
|
|
51
|
+
return '';
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
/** 证书指纹比对(`sha256:<hex>`)。 */
|
|
55
|
+
function certMatchesFingerprint(certPem, fingerprint) {
|
|
56
|
+
const expected = normalizeFingerprintHex(fingerprint);
|
|
57
|
+
if (!expected)
|
|
58
|
+
return false;
|
|
59
|
+
const derHex = new X509Certificate(certPem).fingerprint256.replace(/:/g, '').toLowerCase();
|
|
60
|
+
if (expected.length === 16)
|
|
61
|
+
return derHex.slice(0, 16) === expected;
|
|
62
|
+
return derHex === expected;
|
|
63
|
+
}
|
|
64
|
+
/** 公钥指纹(SPKI DER 的 SHA-256)。 */
|
|
65
|
+
function publicKeyFingerprint(certPem) {
|
|
66
|
+
const cert = new X509Certificate(certPem);
|
|
67
|
+
const spkiDer = cert.publicKey.export({ type: 'spki', format: 'der' });
|
|
68
|
+
return `sha256:${createHash('sha256').update(spkiDer).digest('hex')}`;
|
|
69
|
+
}
|
|
70
|
+
/** 构造签名块。`public_key_fingerprint` 可选,省略即可。 */
|
|
71
|
+
export function buildSignatureBlock(certFingerprint, timestamp, signatureB64, publicKeyFp = '') {
|
|
72
|
+
const lines = ['<!-- AUN-SIGNATURE', `cert_fingerprint: ${certFingerprint}`];
|
|
73
|
+
if (publicKeyFp)
|
|
74
|
+
lines.push(`public_key_fingerprint: ${publicKeyFp}`);
|
|
75
|
+
lines.push(`timestamp: ${Math.trunc(timestamp)}`, `signature: ${signatureB64}`, '-->');
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 解析文件尾部的签名块。
|
|
80
|
+
* `payload` 是**原始字节子串**(`content.slice(0, idx)`),验签必须用它而不是原 content。
|
|
81
|
+
*/
|
|
82
|
+
export function parseSignatureBlock(content) {
|
|
83
|
+
const idx = content.lastIndexOf(SIGNATURE_MARKER);
|
|
84
|
+
if (idx < 0)
|
|
85
|
+
return { payload: content, fields: null };
|
|
86
|
+
if (content.indexOf(SIGNATURE_MARKER) !== idx) {
|
|
87
|
+
return { payload: content.slice(0, idx), fields: null, parseError: 'must contain exactly one signature block' };
|
|
88
|
+
}
|
|
89
|
+
if (idx > 0 && content[idx - 1] !== '\n' && content[idx - 1] !== '\r') {
|
|
90
|
+
return { payload: content.slice(0, idx), fields: null, parseError: 'signature block must start on a new line' };
|
|
91
|
+
}
|
|
92
|
+
const tail = content.slice(idx);
|
|
93
|
+
const match = tail.match(SIGNATURE_RE);
|
|
94
|
+
if (!match)
|
|
95
|
+
return { payload: content.slice(0, idx), fields: null, parseError: 'malformed signature block' };
|
|
96
|
+
const fields = {};
|
|
97
|
+
for (const rawLine of (match.groups?.body ?? '').split(/\r\n|\n|\r/)) {
|
|
98
|
+
const line = rawLine.trim();
|
|
99
|
+
if (!line)
|
|
100
|
+
continue;
|
|
101
|
+
const colon = line.indexOf(':');
|
|
102
|
+
if (colon < 0)
|
|
103
|
+
return { payload: content.slice(0, idx), fields: null, parseError: `malformed signature field: ${line}` };
|
|
104
|
+
const key = line.slice(0, colon).trim().toLowerCase();
|
|
105
|
+
if (Object.prototype.hasOwnProperty.call(fields, key)) {
|
|
106
|
+
return { payload: content.slice(0, idx), fields: null, parseError: `duplicate signature field: ${key}` };
|
|
107
|
+
}
|
|
108
|
+
fields[key] = line.slice(colon + 1).trim();
|
|
109
|
+
}
|
|
110
|
+
for (const req of REQUIRED_FIELDS) {
|
|
111
|
+
if (!fields[req])
|
|
112
|
+
return { payload: content.slice(0, idx), fields: null, parseError: `signature block missing ${req}` };
|
|
113
|
+
}
|
|
114
|
+
if (Object.keys(fields).some(k => !ALLOWED_FIELDS.includes(k))) {
|
|
115
|
+
return { payload: content.slice(0, idx), fields: null, parseError: 'signature block contains unknown fields' };
|
|
116
|
+
}
|
|
117
|
+
if (!/^sha256:[0-9a-f]{64}$/i.test(fields.cert_fingerprint)) {
|
|
118
|
+
return { payload: content.slice(0, idx), fields: null, parseError: 'invalid cert_fingerprint' };
|
|
119
|
+
}
|
|
120
|
+
if (fields.public_key_fingerprint && !/^sha256:[0-9a-f]{64}$/i.test(fields.public_key_fingerprint)) {
|
|
121
|
+
return { payload: content.slice(0, idx), fields: null, parseError: 'invalid public_key_fingerprint' };
|
|
122
|
+
}
|
|
123
|
+
if (!/^\d+$/.test(fields.timestamp)) {
|
|
124
|
+
return { payload: content.slice(0, idx), fields: null, parseError: 'invalid timestamp' };
|
|
125
|
+
}
|
|
126
|
+
if (!isStrictBase64(fields.signature)) {
|
|
127
|
+
return { payload: content.slice(0, idx), fields: null, parseError: 'invalid signature base64' };
|
|
128
|
+
}
|
|
129
|
+
return { payload: content.slice(0, idx), fields: fields };
|
|
130
|
+
}
|
|
131
|
+
// ── 证书 ↔ AID 绑定校验(镜像 fastaun/dist/agent-md-schema.js:102) ────────
|
|
132
|
+
/** 取证书 Subject 的 CN。 */
|
|
133
|
+
function certCommonName(certPem, issuer = false) {
|
|
134
|
+
const cert = new X509Certificate(certPem);
|
|
135
|
+
const target = issuer ? cert.issuer : cert.subject;
|
|
136
|
+
for (const line of target.split('\n')) {
|
|
137
|
+
const m = line.match(/^CN=(.*)$/);
|
|
138
|
+
if (m)
|
|
139
|
+
return m[1].trim();
|
|
140
|
+
}
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 校验证书本身与 AID 的绑定。
|
|
145
|
+
*
|
|
146
|
+
* `requireCurrentValidity` 默认 **false** —— 记忆文件是历史记录,证书到期后
|
|
147
|
+
* 旧记忆不该集体失效。要做时效策略,由调用方比对 `timestamp`。
|
|
148
|
+
*/
|
|
149
|
+
export function validateMemoryCertificate(certPem, expectedAid, timestamp, opts = {}) {
|
|
150
|
+
let cert;
|
|
151
|
+
try {
|
|
152
|
+
cert = new X509Certificate(certPem);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
return { ok: false, reason: `certificate unreadable: ${e?.message ?? e}` };
|
|
156
|
+
}
|
|
157
|
+
if (cert.publicKey.asymmetricKeyType !== 'ec'
|
|
158
|
+
|| cert.publicKey.asymmetricKeyDetails?.namedCurve !== 'prime256v1') {
|
|
159
|
+
return { ok: false, reason: 'signing certificate must use P-256' };
|
|
160
|
+
}
|
|
161
|
+
const notBefore = Math.trunc(new Date(cert.validFrom).getTime() / 1000);
|
|
162
|
+
const notAfter = Math.trunc(new Date(cert.validTo).getTime() / 1000);
|
|
163
|
+
const current = Math.trunc(opts.now ?? Date.now() / 1000);
|
|
164
|
+
if (![notBefore, notAfter, current, timestamp].every(Number.isFinite)) {
|
|
165
|
+
return { ok: false, reason: 'signing certificate is invalid' };
|
|
166
|
+
}
|
|
167
|
+
if ((opts.requireCurrentValidity ?? false) && (current < notBefore || current > notAfter)) {
|
|
168
|
+
return { ok: false, reason: 'signing certificate is expired or not yet valid' };
|
|
169
|
+
}
|
|
170
|
+
if (certCommonName(certPem).toLowerCase() !== expectedAid.trim().toLowerCase()) {
|
|
171
|
+
return { ok: false, reason: 'signing certificate subject does not match AID' };
|
|
172
|
+
}
|
|
173
|
+
if (timestamp < notBefore || timestamp > notAfter) {
|
|
174
|
+
return { ok: false, reason: 'signature timestamp is outside certificate validity' };
|
|
175
|
+
}
|
|
176
|
+
return { ok: true };
|
|
177
|
+
}
|
|
178
|
+
// ── 对外接口 ────────────────────────────────────────────────────────────────
|
|
179
|
+
/**
|
|
180
|
+
* 对记忆文件内容签名。
|
|
181
|
+
* 失败时抛出错误,调用方负责处理(不静默丢弃)。
|
|
182
|
+
*/
|
|
183
|
+
export async function signMemoryFile(content, selfAid) {
|
|
184
|
+
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
185
|
+
if (bytes > MAX_MEMORY_BYTES) {
|
|
186
|
+
throw new Error(`MEMORY_TOO_LARGE: 记忆文件 ${bytes} 字节,超过上限 ${MAX_MEMORY_BYTES} 字节(10KB)。` +
|
|
187
|
+
`一条记忆应当是一句话能说清的结论。内容确实这么多时,请拆成多条分别 create,` +
|
|
188
|
+
`或把细节留在归档的文件里、记忆只记结论。`);
|
|
189
|
+
}
|
|
190
|
+
const store = await getAidStore({ slotId: SLOT.daemon, aunPath: aunPath() });
|
|
191
|
+
try {
|
|
192
|
+
const aid = loadAid(store, selfAid);
|
|
193
|
+
// 保证 payload 以换行结尾,签名块独占一行(解析时的硬要求)
|
|
194
|
+
const payload = content.endsWith('\n') ? content : `${content}\n`;
|
|
195
|
+
const r = aid.sign(payload);
|
|
196
|
+
if (!r.ok) {
|
|
197
|
+
throw new Error(`sign 失败(${selfAid}): ${r.error?.message ?? String(r.error)}`);
|
|
198
|
+
}
|
|
199
|
+
const block = buildSignatureBlock(aid.certFingerprint, Math.trunc(Date.now() / 1000), r.data.signature);
|
|
200
|
+
return { ok: true, signed: payload + block };
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
try {
|
|
204
|
+
store.close();
|
|
205
|
+
}
|
|
206
|
+
catch { /* ignore */ }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 验签记忆文件内容。
|
|
211
|
+
*
|
|
212
|
+
* 返回 `unsigned` 表示文件没有签名块;`invalid` 表示签名块损坏、指纹不符、
|
|
213
|
+
* 证书与 AID 不匹配、或字节验签失败。
|
|
214
|
+
*
|
|
215
|
+
* 只加载本地证书,无需网络。
|
|
216
|
+
*/
|
|
217
|
+
export async function verifyMemoryFile(content, selfAid) {
|
|
218
|
+
const parsed = parseSignatureBlock(content);
|
|
219
|
+
if (parsed.parseError)
|
|
220
|
+
return { status: 'invalid', reason: parsed.parseError };
|
|
221
|
+
if (!parsed.fields)
|
|
222
|
+
return { status: 'unsigned' };
|
|
223
|
+
const store = await getAidStore({ slotId: SLOT.daemon, aunPath: aunPath() });
|
|
224
|
+
try {
|
|
225
|
+
const aid = loadAid(store, selfAid);
|
|
226
|
+
// ① 证书指纹必须匹配本地证书
|
|
227
|
+
if (!certMatchesFingerprint(aid.certPem, parsed.fields.cert_fingerprint)) {
|
|
228
|
+
return { status: 'invalid', reason: 'certificate fingerprint mismatch' };
|
|
229
|
+
}
|
|
230
|
+
// ② 公钥指纹(若有)也必须匹配
|
|
231
|
+
if (parsed.fields.public_key_fingerprint
|
|
232
|
+
&& parsed.fields.public_key_fingerprint.toLowerCase() !== publicKeyFingerprint(aid.certPem).toLowerCase()) {
|
|
233
|
+
return { status: 'invalid', reason: 'public key fingerprint mismatch' };
|
|
234
|
+
}
|
|
235
|
+
// ③ 证书 ↔ AID 绑定 + 签名时间落在证书有效期
|
|
236
|
+
const certCheck = validateMemoryCertificate(aid.certPem, selfAid, Number(parsed.fields.timestamp));
|
|
237
|
+
if (!certCheck.ok)
|
|
238
|
+
return { status: 'invalid', reason: certCheck.reason };
|
|
239
|
+
// ④ 字节验签(用解析出的原始 payload,不是原 content)
|
|
240
|
+
const v = aid.verify(parsed.payload, parsed.fields.signature);
|
|
241
|
+
if (!v.ok) {
|
|
242
|
+
return { status: 'invalid', reason: v.error?.message ?? String(v.error) };
|
|
243
|
+
}
|
|
244
|
+
if (!v.data.valid)
|
|
245
|
+
return { status: 'invalid', reason: 'signature verification failed' };
|
|
246
|
+
return { status: 'verified' };
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
return { status: 'invalid', reason: e?.message ?? String(e) };
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
try {
|
|
253
|
+
store.close();
|
|
254
|
+
}
|
|
255
|
+
catch { /* ignore */ }
|
|
256
|
+
}
|
|
257
|
+
}
|