evolcore 0.0.32 → 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 +61 -0
- package/bin/ec.js +3 -2
- package/bin/install-codex-managed-hooks.mjs +135 -37
- package/dist/agents/baseagent-availability.js +20 -36
- package/dist/agents/claude-runner.js +111 -15
- package/dist/agents/codex/app-server-client.js +3 -0
- package/dist/agents/codex/diagnostics.js +112 -54
- package/dist/agents/codex-runner.js +241 -63
- package/dist/agents/ecagent-runner.js +78 -33
- package/dist/agents/runner-types.js +5 -6
- package/dist/aun/aid/identity.js +1 -1
- package/dist/aun/group/operations.js +0 -1
- package/dist/aun/msg/history.js +1 -0
- package/dist/aun/msg/p2p.js +0 -1
- package/dist/aun/outbox.js +50 -1
- package/dist/channels/aun.js +44 -8
- package/dist/channels/daemon.js +261 -3
- package/dist/cli/agent-command.js +58 -24
- package/dist/cli/agent.js +1 -1
- package/dist/cli/aun-commands.js +368 -31
- package/dist/cli/contact.js +8 -2
- package/dist/cli/ctl-command.js +3 -1
- package/dist/cli/daemon-commands.js +197 -27
- package/dist/cli/debug-mode.js +154 -0
- package/dist/cli/fs-command.js +2 -1
- package/dist/cli/handoff-command.js +3 -0
- package/dist/cli/index.js +98 -67
- package/dist/cli/init.js +2 -2
- package/dist/cli/integrity-command.js +196 -0
- package/dist/cli/managed-command-guard.js +5 -0
- package/dist/cli/memory-command.js +166 -146
- package/dist/cli/queue-command.js +9 -4
- package/dist/cli/session-command.js +194 -0
- package/dist/cli/skill-command.js +78 -2
- package/dist/cli/task-context.js +1 -0
- package/dist/cli/thread-command.js +622 -0
- package/dist/cli/trigger-command.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +6 -3
- package/dist/config/config-manager.js +26 -2
- package/dist/config/contact-book-store.js +5 -0
- package/dist/config/resolved-config-op.js +3 -0
- package/dist/config/role-service.js +13 -0
- package/dist/config/runtime-env.js +16 -0
- package/dist/config-store.js +45 -0
- package/dist/core/audit/log-integrity.js +3 -1
- package/dist/core/auth/agent-delegation.js +133 -36
- package/dist/core/auth/debug-context.js +139 -0
- package/dist/core/auth/delegation-diagnostics.js +414 -0
- package/dist/core/auth/operation-authorizer.js +25 -2
- package/dist/core/auth/operation-catalog.js +49 -0
- package/dist/core/background-session/session-cleanup.js +212 -0
- package/dist/core/background-session/types.js +10 -0
- package/dist/core/bootstrap-messages.js +1 -1
- package/dist/core/bootstrap-service.js +6 -3
- package/dist/core/command/agent-control.js +18 -26
- package/dist/core/command/command-handler.js +197 -27
- package/dist/core/command/daemon-diagnostics.js +1 -0
- package/dist/core/command/memory-slash.js +6 -2
- package/dist/core/command/menu-catalog.js +51 -1
- package/dist/core/command/menu-handler.js +114 -23
- package/dist/core/command/menu-protocol.js +25 -1
- package/dist/core/command/skill-slash.js +322 -0
- package/dist/core/command/slash-gate.js +6 -4
- package/dist/core/command/slash-handler.js +195 -13
- package/dist/core/command/topic-slash.js +528 -0
- package/dist/core/daemon-bootstrap.js +4 -7
- package/dist/core/evolagent-registry.js +20 -5
- package/dist/core/file-integrity/read.js +61 -0
- package/dist/core/file-integrity/store.js +262 -0
- package/dist/core/handoff/runtime.js +5 -11
- package/dist/core/handoff/store.js +5 -0
- package/dist/core/ipc-registration.js +175 -3
- package/dist/core/memory/batch-meta.js +132 -0
- package/dist/core/memory/consolidation-context.js +83 -29
- package/dist/core/memory/consolidation-ops-records.js +49 -16
- package/dist/core/memory/consolidation-queue-format.js +21 -0
- package/dist/core/memory/consolidation.js +264 -205
- 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/aun-ipc-sender.js +161 -6
- package/dist/core/message/im-renderer.js +169 -10
- package/dist/core/message/message-bridge.js +15 -4
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/peer-mode.js +6 -1
- package/dist/core/message/response-engine.js +929 -110
- package/dist/core/message/subtask-tool-projection.js +140 -0
- package/dist/core/model/catalog-service.js +17 -12
- package/dist/core/permission/ec-command-parser.js +217 -9
- 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-idle-monitor.js +249 -22
- package/dist/core/session/session-manager.js +129 -14
- package/dist/core/skill/command-parser.js +13 -0
- package/dist/core/skill/index-generator.js +21 -4
- package/dist/core/skill/startup.js +41 -0
- package/dist/core/skill/switch-resolver.js +55 -29
- 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/command-parser.js +125 -0
- package/dist/core/thread/index.js +1 -0
- package/dist/core/thread/ipc-operation.js +95 -0
- package/dist/core/thread/thread-context-storage.js +131 -0
- package/dist/core/thread/thread-operation.js +258 -0
- package/dist/core/thread/thread-route.js +33 -0
- package/dist/core/thread/thread-service.js +770 -0
- package/dist/core/thread/types.js +1 -0
- package/dist/eck/kit-renderer.js +7 -2
- package/dist/eck/manifest-engine.js +24 -4
- package/dist/eck/message-renderer.js +35 -0
- package/dist/index.js +19 -7
- package/dist/ipc.js +140 -50
- package/dist/paths.js +93 -0
- package/dist/response-system/engines/v1/proactive-flow.js +169 -80
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/authorization.js +57 -28
- package/dist/trigger/feedback.js +13 -2
- package/dist/trigger/parser.js +70 -1
- package/dist/trigger/system-definitions.js +3 -3
- package/dist/trigger/validation.js +14 -0
- package/dist/utils/cross-platform.js +11 -2
- package/dist/utils/error-dict.json +18 -4
- package/dist/utils/error-utils.js +143 -1
- 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 +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -0
- package/kits/docs/evolcore/agent-create.md +40 -28
- package/kits/docs/evolcore/agent.md +21 -6
- package/kits/docs/evolcore/config.md +2 -2
- package/kits/docs/evolcore/ctl.md +2 -2
- package/kits/docs/evolcore/ec-capabilities.md +5 -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 +4 -6
- package/kits/docs/evolcore/group.md +11 -12
- package/kits/docs/evolcore/memory-architecture.md +4 -5
- package/kits/docs/evolcore/memory-consolidation.md +19 -15
- package/kits/docs/evolcore/memory-tags.md +3 -1
- package/kits/docs/evolcore/memory.md +24 -12
- package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
- package/kits/docs/evolcore/menu-protocol.md +7 -0
- package/kits/docs/evolcore/model.md +16 -0
- package/kits/docs/evolcore/msg.md +8 -11
- package/kits/docs/evolcore/thread-chain.md +267 -0
- package/kits/docs/prompt-loading-architecture.md +1 -1
- package/kits/eck_manifest.json +55 -40
- package/kits/eck_manifest.memory.json +0 -12
- package/kits/eck_message_manifest.json +169 -10
- package/kits/rules/01-overview.md +11 -0
- package/kits/rules/03-identity.md +6 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/rules/06-channel.md +1 -1
- package/kits/rules/07-memory-and-thread.md +199 -0
- 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/schemas/relation-config.schema.9.json +18 -3
- package/kits/schemas/role-config.schema.1.json +6 -6
- package/kits/schemas/single-session.schema.1.json +6 -6
- package/kits/schemas/single-session.schema.2.json +6 -6
- package/kits/schemas/single-session.schema.3.json +6 -6
- package/kits/skills/.switches.json +12 -1
- package/kits/skills/ec-group/SKILL.md +234 -0
- package/kits/skills/ec-skill/SKILL.md +10 -1
- package/kits/skills/ec-skill/scripts/cli.mjs +16 -2
- package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
- package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
- package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
- package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
- package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
- package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
- package/kits/skills/thread-ops/SKILL.md +492 -0
- package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
- package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
- package/kits/skills/wechat-article-reader/SKILL.md +97 -0
- package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
- package/kits/templates/bootstrap-welcome.md +7 -7
- package/kits/templates/memory-consolidation/forget.md +7 -2
- package/kits/templates/memory-consolidation/generalize.md +9 -4
- package/kits/templates/memory-consolidation/notes.md +11 -11
- package/kits/templates/memory-consolidation/profile.md +8 -3
- package/kits/templates/memory-consolidation/promote.md +8 -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/message-fragments/item.md +2 -1
- package/kits/templates/message-fragments/thread-context.md +6 -0
- package/kits/templates/message-fragments/trigger-session.md +2 -0
- package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
- package/kits/templates/roles/member.json +2 -0
- package/kits/templates/roles/visitor.json +2 -0
- package/kits/templates/system-fragments/bootstrap.md +67 -72
- package/kits/templates/system-fragments/channel.md +3 -6
- package/kits/templates/system-fragments/commands.md +1 -0
- package/kits/templates/system-fragments/managed-runtime.md +15 -0
- package/kits/templates/system-fragments/memory.md +16 -9
- package/kits/templates/system-fragments/reply.md +7 -6
- package/kits/templates/system-fragments/session.md +5 -14
- package/package.json +6 -3
- package/dist/core/memory/memory-session-policy.js +0 -41
- package/dist/core/memory/memory-signature.js +0 -257
- package/kits/docs/evolcore/group-fs.md +0 -17
- package/kits/docs/venues/aun-group.md +0 -45
- package/kits/docs/venues/group.md +0 -43
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-canonical.ts — runner-agnostic canonical tool name mapping
|
|
3
|
+
*
|
|
4
|
+
* Different runners surface the same capability under different names:
|
|
5
|
+
*
|
|
6
|
+
* canonical ← runner aliases
|
|
7
|
+
* ─────────────────────────────────────────────────────────────────
|
|
8
|
+
* Bash ← Shell (Codex), exec_command (Codex legacy)
|
|
9
|
+
* Write ← FileChange (new-file intent, Codex)
|
|
10
|
+
* Edit ← FileChange (patch intent, Codex), apply_patch (Codex)
|
|
11
|
+
* Read ← read_file (Codex legacy)
|
|
12
|
+
*
|
|
13
|
+
* All policy enforcement (system-session modules, evaluateToolPreflight,
|
|
14
|
+
* checkReadonly) should call canonicalToolName() once at the entry point
|
|
15
|
+
* and operate on the result, rather than handling runner-specific aliases
|
|
16
|
+
* scattered throughout.
|
|
17
|
+
*
|
|
18
|
+
* D1-A: FileChange / apply_patch require the tool input to distinguish
|
|
19
|
+
* Write vs Edit intent; the mapping function accepts an optional `input`
|
|
20
|
+
* parameter for that purpose.
|
|
21
|
+
*
|
|
22
|
+
* canonicalToolCall() goes one step further and also normalises the input:
|
|
23
|
+
* every runner's write shape (Claude/ecagent file_path, Codex fileChanges,
|
|
24
|
+
* standalone or shell-wrapped apply_patch) yields the same list of write
|
|
25
|
+
* targets, so policy modules never read runner-specific input fields.
|
|
26
|
+
*/
|
|
27
|
+
import { parseApplyPatchDocument, parseShellApplyPatchInvocation, } from './ec-command-parser.js';
|
|
28
|
+
/**
|
|
29
|
+
* Normalise a runner-specific tool name to its canonical form.
|
|
30
|
+
*
|
|
31
|
+
* This is a projection of canonicalToolCall(): both read the same
|
|
32
|
+
* `resolveToolIntent` result, so a name-only caller can never disagree with a
|
|
33
|
+
* caller that judges operands. Pass `input` when the runner tool name alone is
|
|
34
|
+
* ambiguous (FileChange, apply_patch, and shell-carried apply_patch).
|
|
35
|
+
*/
|
|
36
|
+
export function canonicalToolName(toolName, input) {
|
|
37
|
+
const intent = resolveToolIntent(toolName, input);
|
|
38
|
+
switch (intent.access) {
|
|
39
|
+
case 'write':
|
|
40
|
+
case 'read':
|
|
41
|
+
return intent.name;
|
|
42
|
+
case 'shell':
|
|
43
|
+
return 'Bash';
|
|
44
|
+
default:
|
|
45
|
+
return toolName;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Returns true iff the canonical name represents a shell execution tool.
|
|
50
|
+
* Consolidates the ad-hoc `isShellTool` checks spread across runners.
|
|
51
|
+
*/
|
|
52
|
+
export function isShellToolName(toolName) {
|
|
53
|
+
const canon = canonicalToolName(toolName);
|
|
54
|
+
return canon === 'Bash';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Returns true iff the canonical name represents a file-write tool.
|
|
58
|
+
* Used by system-session modules and readonly checks.
|
|
59
|
+
*/
|
|
60
|
+
export function isWriteToolName(toolName, input) {
|
|
61
|
+
return canonicalToolCall(toolName, input).access === 'write';
|
|
62
|
+
}
|
|
63
|
+
// ── Internal helpers ─────────────────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* Codex FileChange intent: if fileChanges contains an apply-patch style entry
|
|
66
|
+
* (has an `oldContent` / `patch` key) it is Edit; otherwise Write.
|
|
67
|
+
*/
|
|
68
|
+
function resolveFileChangeName(input) {
|
|
69
|
+
if (!input)
|
|
70
|
+
return 'Write'; // conservative fallback
|
|
71
|
+
const changes = input.fileChanges;
|
|
72
|
+
if (!Array.isArray(changes) || changes.length === 0)
|
|
73
|
+
return 'Write';
|
|
74
|
+
const first = changes[0];
|
|
75
|
+
if (first && typeof first === 'object') {
|
|
76
|
+
const obj = first;
|
|
77
|
+
// A patch/edit operation carries 'patch', 'oldContent', or 'type': 'edit'
|
|
78
|
+
if ('patch' in obj || 'oldContent' in obj || obj.type === 'edit')
|
|
79
|
+
return 'Edit';
|
|
80
|
+
}
|
|
81
|
+
return 'Write';
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* apply_patch: if the patch string starts with '--- ' or 'diff ' it is an
|
|
85
|
+
* edit; otherwise (e.g. a full file replacement) treat as Write.
|
|
86
|
+
*/
|
|
87
|
+
function resolveApplyPatchName(input) {
|
|
88
|
+
if (!input)
|
|
89
|
+
return 'Edit'; // apply_patch is most commonly a diff edit
|
|
90
|
+
const patch = typeof input.patch === 'string' ? input.patch : '';
|
|
91
|
+
if (patch.startsWith('--- ') || patch.startsWith('diff '))
|
|
92
|
+
return 'Edit';
|
|
93
|
+
return 'Write';
|
|
94
|
+
}
|
|
95
|
+
const READ_TOOL_NAMES = new Set(['Read', 'Glob', 'Grep']);
|
|
96
|
+
/** Keys a write tool may carry its target in (Claude / ecagent file tools). */
|
|
97
|
+
const FILE_WRITE_PATH_KEYS = ['file_path', 'notebook_path', 'path'];
|
|
98
|
+
/** Keys a standalone apply_patch call may carry its patch document in. */
|
|
99
|
+
const APPLY_PATCH_DOCUMENT_KEYS = ['patch', 'input', 'command'];
|
|
100
|
+
/** Runner tool names that mean "shell". */
|
|
101
|
+
const SHELL_TOOL_NAMES = new Set(['Shell', 'exec_command']);
|
|
102
|
+
/**
|
|
103
|
+
* The single place that decides what a tool call is. Every public helper here
|
|
104
|
+
* projects this result, so no two callers can classify the same call
|
|
105
|
+
* differently. `paths` is only populated for `write`; read scope stays with
|
|
106
|
+
* checkReadonly, which owns the Glob/Grep rules.
|
|
107
|
+
*/
|
|
108
|
+
function resolveToolIntent(toolName, input = {}) {
|
|
109
|
+
if (toolName === 'FileChange') {
|
|
110
|
+
const paths = [];
|
|
111
|
+
if (typeof input.grantRoot === 'string' && input.grantRoot)
|
|
112
|
+
paths.push(input.grantRoot);
|
|
113
|
+
collectFileChangePaths(input.fileChanges, paths);
|
|
114
|
+
return { access: 'write', name: resolveFileChangeName(input), via: 'tool', paths: unique(paths) };
|
|
115
|
+
}
|
|
116
|
+
if (toolName === 'apply_patch') {
|
|
117
|
+
return applyPatchCall(standaloneApplyPatchChanges(input), 'tool');
|
|
118
|
+
}
|
|
119
|
+
if (toolName === 'Bash' || SHELL_TOOL_NAMES.has(toolName)) {
|
|
120
|
+
const parsed = parseShellApplyPatchInvocation(input);
|
|
121
|
+
if (parsed.kind === 'file-change')
|
|
122
|
+
return applyPatchCall(parsed.fileChanges, 'shell');
|
|
123
|
+
// A recognised but malformed apply_patch is still a write attempt.
|
|
124
|
+
if (parsed.kind === 'invalid')
|
|
125
|
+
return { access: 'write', name: 'Edit', via: 'shell', paths: [] };
|
|
126
|
+
return { access: 'shell', name: 'Bash' };
|
|
127
|
+
}
|
|
128
|
+
// read_file is Codex's legacy read alias; Claude / ecagent use the name directly.
|
|
129
|
+
const name = toolName === 'read_file' ? 'Read' : toolName;
|
|
130
|
+
if (READ_TOOL_NAMES.has(name))
|
|
131
|
+
return { access: 'read', name: name };
|
|
132
|
+
if (name === 'Write' || name === 'Edit' || name === 'MultiEdit' || name === 'NotebookEdit') {
|
|
133
|
+
const paths = [];
|
|
134
|
+
for (const key of FILE_WRITE_PATH_KEYS) {
|
|
135
|
+
if (typeof input[key] === 'string' && input[key])
|
|
136
|
+
paths.push(input[key]);
|
|
137
|
+
}
|
|
138
|
+
return { access: 'write', name: name === 'Write' ? 'Write' : 'Edit', via: 'tool', paths: unique(paths) };
|
|
139
|
+
}
|
|
140
|
+
return { access: 'other', name: toolName };
|
|
141
|
+
}
|
|
142
|
+
export function canonicalToolCall(toolName, input = {}) {
|
|
143
|
+
return resolveToolIntent(toolName, input);
|
|
144
|
+
}
|
|
145
|
+
function applyPatchCall(changes, via) {
|
|
146
|
+
if (!changes || changes.length === 0)
|
|
147
|
+
return { access: 'write', name: 'Edit', via, paths: [] };
|
|
148
|
+
const name = changes.every(change => change.kind === 'add') ? 'Write' : 'Edit';
|
|
149
|
+
return { access: 'write', name, via, paths: unique(applyPatchChangePaths(changes)) };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A standalone apply_patch call is only accepted when every key that carries a
|
|
153
|
+
* patch document parses to the same change set; otherwise the paths approved
|
|
154
|
+
* here might not be the ones the runner applies.
|
|
155
|
+
*/
|
|
156
|
+
function standaloneApplyPatchChanges(input) {
|
|
157
|
+
let found;
|
|
158
|
+
let signature;
|
|
159
|
+
for (const key of APPLY_PATCH_DOCUMENT_KEYS) {
|
|
160
|
+
const value = input[key];
|
|
161
|
+
if (typeof value !== 'string' || !value.trim())
|
|
162
|
+
continue;
|
|
163
|
+
const changes = parseApplyPatchDocument(value);
|
|
164
|
+
if (!changes)
|
|
165
|
+
return undefined;
|
|
166
|
+
const next = JSON.stringify(changes);
|
|
167
|
+
if (signature !== undefined && signature !== next)
|
|
168
|
+
return undefined;
|
|
169
|
+
signature = next;
|
|
170
|
+
found = changes;
|
|
171
|
+
}
|
|
172
|
+
return found;
|
|
173
|
+
}
|
|
174
|
+
function unique(paths) {
|
|
175
|
+
return [...new Set(paths)];
|
|
176
|
+
}
|
|
177
|
+
// ── Write-target extraction shared with tool-policy ──────────────────────────
|
|
178
|
+
function collectExplicitFileChangePaths(record, output) {
|
|
179
|
+
const explicitPathKeys = ['path', 'filePath', 'file_path', 'movePath', 'move_path', 'destinationPath', 'targetPath'];
|
|
180
|
+
const hasExplicitPath = explicitPathKeys.some(key => typeof record[key] === 'string');
|
|
181
|
+
for (const key of explicitPathKeys) {
|
|
182
|
+
if (typeof record[key] === 'string' && record[key])
|
|
183
|
+
output.push(record[key]);
|
|
184
|
+
}
|
|
185
|
+
if (record.kind && typeof record.kind === 'object') {
|
|
186
|
+
collectExplicitFileChangePaths(record.kind, output);
|
|
187
|
+
}
|
|
188
|
+
return hasExplicitPath;
|
|
189
|
+
}
|
|
190
|
+
/** Collect every target path from a Codex / Claude-style `fileChanges` payload. */
|
|
191
|
+
export function collectFileChangePaths(value, output) {
|
|
192
|
+
if (Array.isArray(value)) {
|
|
193
|
+
for (const entry of value) {
|
|
194
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
195
|
+
collectExplicitFileChangePaths(entry, output);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (!value || typeof value !== 'object')
|
|
201
|
+
return;
|
|
202
|
+
const record = value;
|
|
203
|
+
const hasExplicitPath = collectExplicitFileChangePaths(record, output);
|
|
204
|
+
// Claude-style inputs are maps keyed by path; Codex v2 uses an array of
|
|
205
|
+
// records with an explicit path field. Only treat keys as paths for the map
|
|
206
|
+
// shape so diff text and metadata never become path candidates.
|
|
207
|
+
if (!hasExplicitPath) {
|
|
208
|
+
for (const [filePath, change] of Object.entries(record)) {
|
|
209
|
+
if (filePath !== 'kind' && filePath !== 'type')
|
|
210
|
+
output.push(filePath);
|
|
211
|
+
if (change && typeof change === 'object' && !Array.isArray(change)) {
|
|
212
|
+
collectExplicitFileChangePaths(change, output);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Every path an apply_patch change set touches, including move destinations. */
|
|
218
|
+
export function applyPatchChangePaths(changes) {
|
|
219
|
+
const paths = [];
|
|
220
|
+
for (const change of changes) {
|
|
221
|
+
paths.push(change.path);
|
|
222
|
+
if (change.movePath)
|
|
223
|
+
paths.push(change.movePath);
|
|
224
|
+
}
|
|
225
|
+
return paths;
|
|
226
|
+
}
|
|
@@ -7,6 +7,7 @@ import { resolveRoot } from '../../paths.js';
|
|
|
7
7
|
import { containsHClassReference, containsAgentScopeReference, containsLClassReference, checkProtectedPathAccess, getExistingHClassMaskTargets, hClassGrantIncludesProtectedPath, isHClassPath, isSameOrDescendant, isLClassPath, lClassGrantIncludesProtectedPath, resolveProtectedCandidate, resolveProtectedCandidateWithoutFinalSymlink, } from './protected-paths.js';
|
|
8
8
|
import { classifyEvolcoreShellCommand, hasCodexCmdCarrierEcIntent, hasPowerShellCredentialAssignment, normalizeEvolcoreCommandArgv, parseCodexToolCommand, parseBoundedOutputShellCommand, parseShellApplyPatchInvocation, parseLiteralShellArgv, resolveCodexShellCarrierArgv, resolveCodexShellCarrierString, } from './ec-command-parser.js';
|
|
9
9
|
import { analyzeReadonlyShellQuery, } from './readonly-shell-query.js';
|
|
10
|
+
import { applyPatchChangePaths, collectFileChangePaths } from './tool-canonical.js';
|
|
10
11
|
/** Resolve the session-owned temporary root supplied by the execution host. */
|
|
11
12
|
export function resolveManagedTempDir(env = process.env) {
|
|
12
13
|
const raw = typeof env.TMPDIR === 'string' ? env.TMPDIR.trim() : '';
|
|
@@ -1438,32 +1439,41 @@ function globReadScope(input, options) {
|
|
|
1438
1439
|
? resolveProtectedCandidate(prefix || path.parse(pattern).root)
|
|
1439
1440
|
: resolveProtectedCandidate(prefix || '.', base);
|
|
1440
1441
|
}
|
|
1441
|
-
function protectedReadToolStaysInWorkspace(toolName, input, projectPath) {
|
|
1442
|
+
function protectedReadToolStaysInWorkspace(toolName, input, projectPath, grantedRoots = [], excludeWorkspace = false) {
|
|
1442
1443
|
const workspace = resolveProtectedCandidate(projectPath);
|
|
1444
|
+
// A granted root is an explicitly authorized read boundary for this session
|
|
1445
|
+
// (see session-path-grants.ts). Bash already honours its own managed temp
|
|
1446
|
+
// root; read tools must honour the same table or a session cannot read
|
|
1447
|
+
// files the policy layer granted it.
|
|
1448
|
+
// excludeWorkspace:系统会话的 cwd 只用于解析相对路径,本身不是可读根。
|
|
1449
|
+
const withinBoundary = (candidate) => (!excludeWorkspace && isSameOrDescendant(candidate, workspace))
|
|
1450
|
+
|| grantedRoots.some(root => isSameOrDescendant(candidate, root));
|
|
1443
1451
|
if (toolName === 'Read') {
|
|
1444
1452
|
if (typeof input.file_path !== 'string' || !input.file_path)
|
|
1445
1453
|
return false;
|
|
1446
|
-
return
|
|
1454
|
+
return withinBoundary(resolveProtectedCandidate(input.file_path, projectPath));
|
|
1447
1455
|
}
|
|
1448
1456
|
if (toolName === 'Grep') {
|
|
1449
1457
|
const scope = typeof input.path === 'string' && input.path ? input.path : '.';
|
|
1450
|
-
return
|
|
1458
|
+
return withinBoundary(resolveProtectedCandidate(scope, projectPath));
|
|
1451
1459
|
}
|
|
1452
1460
|
if (toolName === 'Glob') {
|
|
1453
1461
|
const baseValue = typeof input.path === 'string' && input.path ? input.path : '.';
|
|
1454
1462
|
const base = resolveProtectedCandidate(baseValue, projectPath);
|
|
1455
|
-
if (!
|
|
1463
|
+
if (!withinBoundary(base))
|
|
1456
1464
|
return false;
|
|
1457
1465
|
const scope = globReadScope(input, { cwd: projectPath });
|
|
1458
|
-
return
|
|
1466
|
+
return withinBoundary(scope);
|
|
1459
1467
|
}
|
|
1460
1468
|
return true;
|
|
1461
1469
|
}
|
|
1462
|
-
function readonlyShellStaysInWorkspace(analysis, projectPath,
|
|
1470
|
+
function readonlyShellStaysInWorkspace(analysis, projectPath, readRoots = [], excludeWorkspace = false) {
|
|
1463
1471
|
if (analysis.kind !== 'proven-readonly')
|
|
1464
1472
|
return false;
|
|
1465
1473
|
const workspace = resolveProtectedCandidate(projectPath);
|
|
1466
|
-
const
|
|
1474
|
+
const roots = readRoots
|
|
1475
|
+
.filter(root => typeof root === 'string' && path.isAbsolute(root))
|
|
1476
|
+
.map(root => resolveProtectedCandidate(root));
|
|
1467
1477
|
return analysis.ir.pipelines.flatMap(pipeline => pipeline.flatMap(command => command.pathOperands.map(operand => ({
|
|
1468
1478
|
operand,
|
|
1469
1479
|
accessKind: command.accessKind ?? (operand.access === 'recursive' ? 'enumerate' : 'content'),
|
|
@@ -1471,8 +1481,8 @@ function readonlyShellStaysInWorkspace(analysis, projectPath, managedTempDir) {
|
|
|
1471
1481
|
const candidate = accessKind === 'metadata' && operand.access === 'exact'
|
|
1472
1482
|
? resolveProtectedCandidateWithoutFinalSymlink(operand.value, projectPath)
|
|
1473
1483
|
: resolveProtectedCandidate(operand.value, projectPath);
|
|
1474
|
-
return isSameOrDescendant(candidate, workspace)
|
|
1475
|
-
|| (
|
|
1484
|
+
return (!excludeWorkspace && isSameOrDescendant(candidate, workspace))
|
|
1485
|
+
|| roots.some(root => isSameOrDescendant(candidate, root));
|
|
1476
1486
|
})());
|
|
1477
1487
|
}
|
|
1478
1488
|
const READONLY_SOURCE_ROOTS = new Set([
|
|
@@ -1571,10 +1581,15 @@ export function checkReadonly(toolName, input, projectPath, context) {
|
|
|
1571
1581
|
'Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch',
|
|
1572
1582
|
'TaskList', 'TaskGet', 'ToolSearch',
|
|
1573
1583
|
]);
|
|
1584
|
+
// 会话私有临时根在授权表之外恒为可读:runner 自己的临时产物都落在这里。
|
|
1585
|
+
const grantedRoots = [...new Set([
|
|
1586
|
+
...(context?.grantedReadRoots ?? []),
|
|
1587
|
+
...(context?.managedTempDir ? [context.managedTempDir] : []),
|
|
1588
|
+
])];
|
|
1574
1589
|
if (readOnlyTools.has(toolName)) {
|
|
1575
1590
|
const paths = protectedReadToolPaths(toolName, input);
|
|
1576
1591
|
const pathOptions = { cwd: projectPath, root: context?.root, selfAid: context?.selfAid };
|
|
1577
|
-
if (!protectedReadToolStaysInWorkspace(toolName, input, projectPath)) {
|
|
1592
|
+
if (!protectedReadToolStaysInWorkspace(toolName, input, projectPath, grantedRoots, context?.excludeWorkspace === true)) {
|
|
1578
1593
|
return { behavior: 'deny', message: '🔒 只读模式:文件读取范围不能越出当前项目目录', policyCode: 'readonly_workspace_escape' };
|
|
1579
1594
|
}
|
|
1580
1595
|
const directHPath = paths.find(filePath => containsHClassReference(filePath, pathOptions)
|
|
@@ -1626,7 +1641,7 @@ export function checkReadonly(toolName, input, projectPath, context) {
|
|
|
1626
1641
|
if (protectedOperands.analysis.kind === 'proven-readonly'
|
|
1627
1642
|
&& !protectedOperands.hClass
|
|
1628
1643
|
&& !protectedOperands.lClass
|
|
1629
|
-
&& readonlyShellStaysInWorkspace(protectedOperands.analysis, projectPath,
|
|
1644
|
+
&& readonlyShellStaysInWorkspace(protectedOperands.analysis, projectPath, grantedRoots, context?.excludeWorkspace === true)) {
|
|
1630
1645
|
return { behavior: 'allow' };
|
|
1631
1646
|
}
|
|
1632
1647
|
if (protectedOperands.hClass) {
|
|
@@ -1816,53 +1831,6 @@ function collectFilesystemWriteGrantPaths(value, output, projectRootSubpaths) {
|
|
|
1816
1831
|
}
|
|
1817
1832
|
}
|
|
1818
1833
|
}
|
|
1819
|
-
function collectExplicitFileChangePaths(record, output) {
|
|
1820
|
-
const explicitPathKeys = ['path', 'filePath', 'file_path', 'movePath', 'move_path', 'destinationPath', 'targetPath'];
|
|
1821
|
-
const hasExplicitPath = explicitPathKeys.some(key => typeof record[key] === 'string');
|
|
1822
|
-
for (const key of explicitPathKeys) {
|
|
1823
|
-
if (typeof record[key] === 'string' && record[key])
|
|
1824
|
-
output.push(record[key]);
|
|
1825
|
-
}
|
|
1826
|
-
if (record.kind && typeof record.kind === 'object') {
|
|
1827
|
-
collectExplicitFileChangePaths(record.kind, output);
|
|
1828
|
-
}
|
|
1829
|
-
return hasExplicitPath;
|
|
1830
|
-
}
|
|
1831
|
-
function collectFileChangePaths(value, output) {
|
|
1832
|
-
if (Array.isArray(value)) {
|
|
1833
|
-
for (const entry of value) {
|
|
1834
|
-
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
1835
|
-
collectExplicitFileChangePaths(entry, output);
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
return;
|
|
1839
|
-
}
|
|
1840
|
-
if (!value || typeof value !== 'object')
|
|
1841
|
-
return;
|
|
1842
|
-
const record = value;
|
|
1843
|
-
const hasExplicitPath = collectExplicitFileChangePaths(record, output);
|
|
1844
|
-
// Claude-style inputs are maps keyed by path; Codex v2 uses an array of
|
|
1845
|
-
// records with an explicit path field. Only treat keys as paths for the map
|
|
1846
|
-
// shape so diff text and metadata never become path candidates.
|
|
1847
|
-
if (!hasExplicitPath) {
|
|
1848
|
-
for (const [filePath, change] of Object.entries(record)) {
|
|
1849
|
-
if (filePath !== 'kind' && filePath !== 'type')
|
|
1850
|
-
output.push(filePath);
|
|
1851
|
-
if (change && typeof change === 'object' && !Array.isArray(change)) {
|
|
1852
|
-
collectExplicitFileChangePaths(change, output);
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
}
|
|
1857
|
-
function applyPatchChangePaths(changes) {
|
|
1858
|
-
const paths = [];
|
|
1859
|
-
for (const change of changes) {
|
|
1860
|
-
paths.push(change.path);
|
|
1861
|
-
if (change.movePath)
|
|
1862
|
-
paths.push(change.movePath);
|
|
1863
|
-
}
|
|
1864
|
-
return paths;
|
|
1865
|
-
}
|
|
1866
1834
|
function applyPatchWorkspaceEscape(changes, context) {
|
|
1867
1835
|
const workspace = resolveProtectedCandidate(context.workspacePath ?? context.projectPath);
|
|
1868
1836
|
const managedTempDir = context.managedTempDir
|
|
@@ -2066,7 +2034,7 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
2066
2034
|
}
|
|
2067
2035
|
else if (normalized.includes('/backups/config/') || normalized.includes('/.snapshots/')) {
|
|
2068
2036
|
suggestion = '\n\n💡 快照由系统自动管理,请使用:\n' +
|
|
2069
|
-
|
|
2037
|
+
` • 创建快照:ec config snapshot --desc '<说明>'\n` +
|
|
2070
2038
|
' • 查看历史:ec config history\n' +
|
|
2071
2039
|
' • 恢复快照:ec config restore <version>';
|
|
2072
2040
|
}
|
|
@@ -2602,7 +2570,7 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
2602
2570
|
? '🔒 EC 命令包含无法安全解析的 PowerShell 展开;临时路径仅支持已登记的当前会话 $env:TMPDIR 或 ${env:TMPDIR},也可使用完整路径。纯文字请使用单引号,或用 PowerShell 反引号转义 $ 和正文内的双引号。'
|
|
2603
2571
|
: shellDialect === 'cmd'
|
|
2604
2572
|
? '🔒 EC 命令包含未转义的 cmd.exe 展开;请不要使用 %VAR%、!VAR! 或 ^ 转义。cmd.exe 传给 ec 时必须保持原文字面量'
|
|
2605
|
-
: '🔒 EC
|
|
2573
|
+
: '🔒 EC 自由文本参数(正文、--desc、--name、--keywords 等)一律用单引号包住;双引号内的反引号与 $ 会被 shell 展开。正文里不要再出现半角撇号(ASCII 0x27),需要时用 `ʼ`(U+02BC)或 `'`(U+FF07)';
|
|
2606
2574
|
return {
|
|
2607
2575
|
behavior: 'deny',
|
|
2608
2576
|
input,
|
|
@@ -2612,10 +2580,10 @@ export function evaluateToolPreflight(toolName, input, context) {
|
|
|
2612
2580
|
}
|
|
2613
2581
|
if (ecCommand.issue === 'invalid-quote') {
|
|
2614
2582
|
const message = shellDialect === 'powershell'
|
|
2615
|
-
? '🔒 EC
|
|
2583
|
+
? '🔒 EC 命令引号未闭合或存在错误嵌套。Note: PowerShell 把 U+2018/U+2019/U+201A/U+201B 也当引号定界符——正文里出现这四个字符同样会报此错。正文一律用半角单引号(ASCII 0x27)包住,正文内部的半角撇号用 `ʼ`(U+02BC)或 `'`(U+FF07)代替,或把消息正文写入当前 $TMPDIR 后使用 --text-from-file'
|
|
2616
2584
|
: shellDialect === 'cmd'
|
|
2617
2585
|
? '🔒 EC 命令引号未闭合或存在错误嵌套;cmd.exe carrier 的命令正文请保持一个完整参数,不要用未配对的双引号'
|
|
2618
|
-
: '🔒 EC
|
|
2586
|
+
: '🔒 EC 命令引号未闭合或存在错误嵌套。自由文本参数一律用单引号包住;正文里不要出现半角撇号(ASCII 0x27)本身,需要时用 `ʼ`(U+02BC)或 `'`(U+FF07)';
|
|
2619
2587
|
return {
|
|
2620
2588
|
behavior: 'deny',
|
|
2621
2589
|
input,
|
|
@@ -147,13 +147,17 @@ export function validateRuntimeStringFieldOverride(input) {
|
|
|
147
147
|
* Resolve the Agent-level host execution policy for one authenticated principal.
|
|
148
148
|
* The Agent flag is independent of role permissionMode values.
|
|
149
149
|
*/
|
|
150
|
+
export function isAgentFullAccessOwner(agentAid, principalId) {
|
|
151
|
+
if (!principalId)
|
|
152
|
+
return false;
|
|
153
|
+
const config = read(ConfigTarget.Agent, { self: agentAid }, { cache: false });
|
|
154
|
+
return config?.owners?.includes(principalId) === true
|
|
155
|
+
&& loadDaemonConfig().owners?.includes(principalId) === true;
|
|
156
|
+
}
|
|
150
157
|
export function agentFullAccessStatus(agentAid, principalId) {
|
|
151
158
|
const config = read(ConfigTarget.Agent, { self: agentAid }, { cache: false });
|
|
152
|
-
const daemon = loadDaemonConfig();
|
|
153
159
|
const enabled = config?.fullaccess?.enabled === true;
|
|
154
|
-
const eligible =
|
|
155
|
-
&& !!config?.owners?.includes(principalId)
|
|
156
|
-
&& !!daemon.owners?.includes(principalId);
|
|
160
|
+
const eligible = isAgentFullAccessOwner(agentAid, principalId);
|
|
157
161
|
return { enabled, eligible, effective: enabled && eligible };
|
|
158
162
|
}
|
|
159
163
|
/** Create a trusted per-call claim for a direct private message. */
|
|
@@ -189,13 +193,20 @@ export function assertAgentPolicyAuthorization(authorization) {
|
|
|
189
193
|
throw new Error('Agent fullaccess policy disabled or current owner is no longer eligible');
|
|
190
194
|
}
|
|
191
195
|
}
|
|
192
|
-
/** Only
|
|
193
|
-
export function agentFullAccessWriteError(
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
+
/** Only authenticated ECWeb management or a direct double-Owner Menu request may mutate this policy. */
|
|
197
|
+
export function agentFullAccessWriteError(agentAid, subject) {
|
|
198
|
+
if (subject?.ecwebManagement === true) {
|
|
199
|
+
if (!subject.isDaemonOwner || subject.canApprovePersistentFullAccess === false || subject.chatType !== 'private') {
|
|
200
|
+
return 'Only authenticated ECWeb management may configure Agent fullaccess';
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
196
203
|
}
|
|
197
|
-
if (
|
|
198
|
-
|
|
204
|
+
if (subject?.agentFullAccessManagement !== true
|
|
205
|
+
|| subject.chatType !== 'private'
|
|
206
|
+
|| !subject.isDaemonOwner
|
|
207
|
+
|| subject.processRole !== 'daemon-owner'
|
|
208
|
+
|| !isAgentFullAccessOwner(agentAid, subject.principalId)) {
|
|
209
|
+
return 'Agent fullaccess requires a direct Menu request from both the target Agent Owner and daemon Owner';
|
|
199
210
|
}
|
|
200
211
|
return undefined;
|
|
201
212
|
}
|
|
@@ -178,6 +178,111 @@ export function appendJsonlDurable(filePath, record) {
|
|
|
178
178
|
fs.closeSync(fd);
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* meta_*.jsonl(含 `_threads/`)是快照日志:每行一份完整 Session,只有最后一行是权威状态,
|
|
183
|
+
* 之前的行只供排障回看。超过 256KB 压到最近 50 行。
|
|
184
|
+
*/
|
|
185
|
+
export const SESSION_META_COMPACT_POLICY = {
|
|
186
|
+
maxBytes: 256 * 1024,
|
|
187
|
+
keepLines: 50,
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* 从 [0, endExclusive) 区间由后往前逐行产出(不含换行符,含空行)。
|
|
191
|
+
* 按 64KB 分块倒读,内存只与单行长度相关,与文件大小无关。
|
|
192
|
+
*/
|
|
193
|
+
function* iterateLinesBackward(fd, endExclusive, filePath) {
|
|
194
|
+
const chunkSize = 64 * 1024;
|
|
195
|
+
let cursor = endExclusive;
|
|
196
|
+
let carry = Buffer.alloc(0);
|
|
197
|
+
while (cursor > 0) {
|
|
198
|
+
const start = Math.max(0, cursor - chunkSize);
|
|
199
|
+
const chunk = readExactAt(fd, cursor - start, start, filePath);
|
|
200
|
+
const buf = carry.length ? Buffer.concat([chunk, carry]) : chunk;
|
|
201
|
+
let end = buf.length;
|
|
202
|
+
let newline = buf.lastIndexOf(0x0a, end - 1);
|
|
203
|
+
while (newline >= 0) {
|
|
204
|
+
yield buf.subarray(newline + 1, end);
|
|
205
|
+
end = newline;
|
|
206
|
+
newline = end > 0 ? buf.lastIndexOf(0x0a, end - 1) : -1;
|
|
207
|
+
}
|
|
208
|
+
carry = Buffer.from(buf.subarray(0, end));
|
|
209
|
+
cursor = start;
|
|
210
|
+
}
|
|
211
|
+
if (carry.length)
|
|
212
|
+
yield carry;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* 快照日志的尾部压缩:文件超过 policy.maxBytes 时,只保留最近 keepLines 行
|
|
216
|
+
* (且不超过 maxBytes/2 字节,至少保留最后一行),写 tmp → fsync → rename 覆盖。
|
|
217
|
+
*
|
|
218
|
+
* - 保留行按字节原样拷贝,不解析,坏的末行仍交给读取方报错,不在这里被掩盖。
|
|
219
|
+
* - 末尾未以换行结束(崩溃残留或有人正在写)时不压缩,留给下次 append 的尾部修复。
|
|
220
|
+
* - rename 前复查文件大小,期间有其他进程追加则放弃本次压缩,避免吞掉新写入的行。
|
|
221
|
+
* - 直接 rename 覆盖,不先 unlink,不留目标文件缺失的窗口给并发读者。
|
|
222
|
+
*
|
|
223
|
+
* @returns 是否实际做了压缩。失败时抛错,原文件保持不变。
|
|
224
|
+
*/
|
|
225
|
+
export function compactJsonlTail(filePath, policy) {
|
|
226
|
+
let fd;
|
|
227
|
+
try {
|
|
228
|
+
fd = fs.openSync(filePath, 'r');
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (error.code === 'ENOENT')
|
|
232
|
+
return false;
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
let output;
|
|
236
|
+
let size;
|
|
237
|
+
try {
|
|
238
|
+
size = fs.fstatSync(fd).size;
|
|
239
|
+
if (size <= policy.maxBytes)
|
|
240
|
+
return false;
|
|
241
|
+
if (readExactAt(fd, 1, size - 1, filePath)[0] !== 0x0a)
|
|
242
|
+
return false;
|
|
243
|
+
const budget = Math.floor(policy.maxBytes / 2);
|
|
244
|
+
const kept = [];
|
|
245
|
+
let keptBytes = 0;
|
|
246
|
+
for (const line of iterateLinesBackward(fd, size - 1, filePath)) {
|
|
247
|
+
if (line.toString('utf8').trim() === '')
|
|
248
|
+
continue;
|
|
249
|
+
if (kept.length > 0 && (kept.length >= policy.keepLines || keptBytes + line.length + 1 > budget))
|
|
250
|
+
break;
|
|
251
|
+
kept.push(Buffer.from(line));
|
|
252
|
+
keptBytes += line.length + 1;
|
|
253
|
+
}
|
|
254
|
+
if (keptBytes >= size)
|
|
255
|
+
return false;
|
|
256
|
+
output = Buffer.concat(kept.reverse().flatMap(line => [line, Buffer.from('\n')]));
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
fs.closeSync(fd);
|
|
260
|
+
}
|
|
261
|
+
const tmpPath = filePath + '.compact.tmp';
|
|
262
|
+
const tmpFd = fs.openSync(tmpPath, 'w');
|
|
263
|
+
try {
|
|
264
|
+
writeAllSync(tmpFd, output);
|
|
265
|
+
fs.fsyncSync(tmpFd);
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
fs.closeSync(tmpFd);
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
if (fs.statSync(filePath).size !== size) {
|
|
272
|
+
fs.unlinkSync(tmpPath);
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
fs.renameSync(tmpPath, filePath);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
try {
|
|
279
|
+
fs.unlinkSync(tmpPath);
|
|
280
|
+
}
|
|
281
|
+
catch { /* best effort */ }
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
181
286
|
function writeAllSync(fd, content) {
|
|
182
287
|
let offset = 0;
|
|
183
288
|
while (offset < content.length) {
|
|
@@ -323,12 +428,24 @@ export function readJsonFile(filePath) {
|
|
|
323
428
|
throw e;
|
|
324
429
|
}
|
|
325
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* 宽松读取最后一条可解析记录:从尾部倒读,跳过空行和坏行(含未终止的尾行)。
|
|
433
|
+
* 开销只与末尾几行有关,不随文件大小增长。
|
|
434
|
+
*/
|
|
326
435
|
export function readLastJsonlLine(filePath) {
|
|
436
|
+
let fd;
|
|
327
437
|
try {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
438
|
+
fd = fs.openSync(filePath, 'r');
|
|
439
|
+
}
|
|
440
|
+
catch (e) {
|
|
441
|
+
if (e.code === 'ENOENT')
|
|
442
|
+
return undefined;
|
|
443
|
+
throw e;
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
const size = fs.fstatSync(fd).size;
|
|
447
|
+
for (const raw of iterateLinesBackward(fd, size, filePath)) {
|
|
448
|
+
const line = raw.toString('utf8').trim();
|
|
332
449
|
if (!line)
|
|
333
450
|
continue;
|
|
334
451
|
try {
|
|
@@ -340,10 +457,8 @@ export function readLastJsonlLine(filePath) {
|
|
|
340
457
|
}
|
|
341
458
|
return undefined;
|
|
342
459
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return undefined;
|
|
346
|
-
throw e;
|
|
460
|
+
finally {
|
|
461
|
+
fs.closeSync(fd);
|
|
347
462
|
}
|
|
348
463
|
}
|
|
349
464
|
export function readAllJsonlLines(filePath) {
|