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.
Files changed (214) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +135 -37
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +111 -15
  6. package/dist/agents/codex/app-server-client.js +3 -0
  7. package/dist/agents/codex/diagnostics.js +112 -54
  8. package/dist/agents/codex-runner.js +241 -63
  9. package/dist/agents/ecagent-runner.js +78 -33
  10. package/dist/agents/runner-types.js +5 -6
  11. package/dist/aun/aid/identity.js +1 -1
  12. package/dist/aun/group/operations.js +0 -1
  13. package/dist/aun/msg/history.js +1 -0
  14. package/dist/aun/msg/p2p.js +0 -1
  15. package/dist/aun/outbox.js +50 -1
  16. package/dist/channels/aun.js +44 -8
  17. package/dist/channels/daemon.js +261 -3
  18. package/dist/cli/agent-command.js +58 -24
  19. package/dist/cli/agent.js +1 -1
  20. package/dist/cli/aun-commands.js +368 -31
  21. package/dist/cli/contact.js +8 -2
  22. package/dist/cli/ctl-command.js +3 -1
  23. package/dist/cli/daemon-commands.js +197 -27
  24. package/dist/cli/debug-mode.js +154 -0
  25. package/dist/cli/fs-command.js +2 -1
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +98 -67
  28. package/dist/cli/init.js +2 -2
  29. package/dist/cli/integrity-command.js +196 -0
  30. package/dist/cli/managed-command-guard.js +5 -0
  31. package/dist/cli/memory-command.js +166 -146
  32. package/dist/cli/queue-command.js +9 -4
  33. package/dist/cli/session-command.js +194 -0
  34. package/dist/cli/skill-command.js +78 -2
  35. package/dist/cli/task-context.js +1 -0
  36. package/dist/cli/thread-command.js +622 -0
  37. package/dist/cli/trigger-command.js +10 -3
  38. package/dist/config/builtin-roles.js +1 -0
  39. package/dist/config/config-field-policy.js +6 -3
  40. package/dist/config/config-manager.js +26 -2
  41. package/dist/config/contact-book-store.js +5 -0
  42. package/dist/config/resolved-config-op.js +3 -0
  43. package/dist/config/role-service.js +13 -0
  44. package/dist/config/runtime-env.js +16 -0
  45. package/dist/config-store.js +45 -0
  46. package/dist/core/audit/log-integrity.js +3 -1
  47. package/dist/core/auth/agent-delegation.js +133 -36
  48. package/dist/core/auth/debug-context.js +139 -0
  49. package/dist/core/auth/delegation-diagnostics.js +414 -0
  50. package/dist/core/auth/operation-authorizer.js +25 -2
  51. package/dist/core/auth/operation-catalog.js +49 -0
  52. package/dist/core/background-session/session-cleanup.js +212 -0
  53. package/dist/core/background-session/types.js +10 -0
  54. package/dist/core/bootstrap-messages.js +1 -1
  55. package/dist/core/bootstrap-service.js +6 -3
  56. package/dist/core/command/agent-control.js +18 -26
  57. package/dist/core/command/command-handler.js +197 -27
  58. package/dist/core/command/daemon-diagnostics.js +1 -0
  59. package/dist/core/command/memory-slash.js +6 -2
  60. package/dist/core/command/menu-catalog.js +51 -1
  61. package/dist/core/command/menu-handler.js +114 -23
  62. package/dist/core/command/menu-protocol.js +25 -1
  63. package/dist/core/command/skill-slash.js +322 -0
  64. package/dist/core/command/slash-gate.js +6 -4
  65. package/dist/core/command/slash-handler.js +195 -13
  66. package/dist/core/command/topic-slash.js +528 -0
  67. package/dist/core/daemon-bootstrap.js +4 -7
  68. package/dist/core/evolagent-registry.js +20 -5
  69. package/dist/core/file-integrity/read.js +61 -0
  70. package/dist/core/file-integrity/store.js +262 -0
  71. package/dist/core/handoff/runtime.js +5 -11
  72. package/dist/core/handoff/store.js +5 -0
  73. package/dist/core/ipc-registration.js +175 -3
  74. package/dist/core/memory/batch-meta.js +132 -0
  75. package/dist/core/memory/consolidation-context.js +83 -29
  76. package/dist/core/memory/consolidation-ops-records.js +49 -16
  77. package/dist/core/memory/consolidation-queue-format.js +21 -0
  78. package/dist/core/memory/consolidation.js +264 -205
  79. package/dist/core/memory/index-manager.js +19 -28
  80. package/dist/core/memory/memory-admin.js +19 -14
  81. package/dist/core/memory/memory-document.js +53 -9
  82. package/dist/core/memory/memory-writer.js +30 -15
  83. package/dist/core/message/aun-ipc-sender.js +161 -6
  84. package/dist/core/message/im-renderer.js +169 -10
  85. package/dist/core/message/message-bridge.js +15 -4
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/peer-mode.js +6 -1
  88. package/dist/core/message/response-engine.js +929 -110
  89. package/dist/core/message/subtask-tool-projection.js +140 -0
  90. package/dist/core/model/catalog-service.js +17 -12
  91. package/dist/core/permission/ec-command-parser.js +217 -9
  92. package/dist/core/permission/protected-paths.js +5 -1
  93. package/dist/core/permission/readonly-shell-query.js +43 -5
  94. package/dist/core/permission/session-path-grants.js +334 -0
  95. package/dist/core/permission/tool-canonical.js +226 -0
  96. package/dist/core/permission/tool-policy.js +30 -62
  97. package/dist/core/role/runtime-policy.js +21 -10
  98. package/dist/core/session/session-fs-store.js +123 -8
  99. package/dist/core/session/session-idle-monitor.js +249 -22
  100. package/dist/core/session/session-manager.js +129 -14
  101. package/dist/core/skill/command-parser.js +13 -0
  102. package/dist/core/skill/index-generator.js +21 -4
  103. package/dist/core/skill/startup.js +41 -0
  104. package/dist/core/skill/switch-resolver.js +55 -29
  105. package/dist/core/system-session/grants.js +40 -0
  106. package/dist/core/system-session/memory.js +222 -0
  107. package/dist/core/system-session/registry.js +74 -0
  108. package/dist/core/system-session/types.js +14 -0
  109. package/dist/core/thread/command-parser.js +125 -0
  110. package/dist/core/thread/index.js +1 -0
  111. package/dist/core/thread/ipc-operation.js +95 -0
  112. package/dist/core/thread/thread-context-storage.js +131 -0
  113. package/dist/core/thread/thread-operation.js +258 -0
  114. package/dist/core/thread/thread-route.js +33 -0
  115. package/dist/core/thread/thread-service.js +770 -0
  116. package/dist/core/thread/types.js +1 -0
  117. package/dist/eck/kit-renderer.js +7 -2
  118. package/dist/eck/manifest-engine.js +24 -4
  119. package/dist/eck/message-renderer.js +35 -0
  120. package/dist/index.js +19 -7
  121. package/dist/ipc.js +140 -50
  122. package/dist/paths.js +93 -0
  123. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  124. package/dist/stats/billing.js +20 -8
  125. package/dist/trigger/authorization.js +57 -28
  126. package/dist/trigger/feedback.js +13 -2
  127. package/dist/trigger/parser.js +70 -1
  128. package/dist/trigger/system-definitions.js +3 -3
  129. package/dist/trigger/validation.js +14 -0
  130. package/dist/utils/cross-platform.js +11 -2
  131. package/dist/utils/error-dict.json +18 -4
  132. package/dist/utils/error-utils.js +143 -1
  133. package/dist/utils/instance-registry.js +13 -8
  134. package/dist/utils/log-writer.js +12 -0
  135. package/dist/utils/process-introspect.js +11 -0
  136. package/kits/docs/context-assembly.md +1 -1
  137. package/kits/docs/evolcore/INDEX.md +1 -0
  138. package/kits/docs/evolcore/agent-create.md +40 -28
  139. package/kits/docs/evolcore/agent.md +21 -6
  140. package/kits/docs/evolcore/config.md +2 -2
  141. package/kits/docs/evolcore/ctl.md +2 -2
  142. package/kits/docs/evolcore/ec-capabilities.md +5 -1
  143. package/kits/docs/evolcore/fs-architecture.md +4 -5
  144. package/kits/docs/evolcore/fs.md +1 -1
  145. package/kits/docs/evolcore/group-collaboration.md +4 -6
  146. package/kits/docs/evolcore/group.md +11 -12
  147. package/kits/docs/evolcore/memory-architecture.md +4 -5
  148. package/kits/docs/evolcore/memory-consolidation.md +19 -15
  149. package/kits/docs/evolcore/memory-tags.md +3 -1
  150. package/kits/docs/evolcore/memory.md +24 -12
  151. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  152. package/kits/docs/evolcore/menu-protocol.md +7 -0
  153. package/kits/docs/evolcore/model.md +16 -0
  154. package/kits/docs/evolcore/msg.md +8 -11
  155. package/kits/docs/evolcore/thread-chain.md +267 -0
  156. package/kits/docs/prompt-loading-architecture.md +1 -1
  157. package/kits/eck_manifest.json +55 -40
  158. package/kits/eck_manifest.memory.json +0 -12
  159. package/kits/eck_message_manifest.json +169 -10
  160. package/kits/rules/01-overview.md +11 -0
  161. package/kits/rules/03-identity.md +6 -3
  162. package/kits/rules/05-venue.md +1 -1
  163. package/kits/rules/06-channel.md +1 -1
  164. package/kits/rules/07-memory-and-thread.md +199 -0
  165. package/kits/schemas/_meta.json +7 -1
  166. package/kits/schemas/agent-config.schema.13.json +1 -1
  167. package/kits/schemas/daemon.schema.7.json +140 -0
  168. package/kits/schemas/relation-config.schema.9.json +18 -3
  169. package/kits/schemas/role-config.schema.1.json +6 -6
  170. package/kits/schemas/single-session.schema.1.json +6 -6
  171. package/kits/schemas/single-session.schema.2.json +6 -6
  172. package/kits/schemas/single-session.schema.3.json +6 -6
  173. package/kits/skills/.switches.json +12 -1
  174. package/kits/skills/ec-group/SKILL.md +234 -0
  175. package/kits/skills/ec-skill/SKILL.md +10 -1
  176. package/kits/skills/ec-skill/scripts/cli.mjs +16 -2
  177. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  178. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  179. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  180. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  181. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  182. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  183. package/kits/skills/thread-ops/SKILL.md +492 -0
  184. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  185. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  186. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  187. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  188. package/kits/templates/bootstrap-welcome.md +7 -7
  189. package/kits/templates/memory-consolidation/forget.md +7 -2
  190. package/kits/templates/memory-consolidation/generalize.md +9 -4
  191. package/kits/templates/memory-consolidation/notes.md +11 -11
  192. package/kits/templates/memory-consolidation/profile.md +8 -3
  193. package/kits/templates/memory-consolidation/promote.md +8 -3
  194. package/kits/templates/memory-consolidation/workflow.md +6 -6
  195. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  196. package/kits/templates/message-fragments/item.md +2 -1
  197. package/kits/templates/message-fragments/thread-context.md +6 -0
  198. package/kits/templates/message-fragments/trigger-session.md +2 -0
  199. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  200. package/kits/templates/roles/member.json +2 -0
  201. package/kits/templates/roles/visitor.json +2 -0
  202. package/kits/templates/system-fragments/bootstrap.md +67 -72
  203. package/kits/templates/system-fragments/channel.md +3 -6
  204. package/kits/templates/system-fragments/commands.md +1 -0
  205. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  206. package/kits/templates/system-fragments/memory.md +16 -9
  207. package/kits/templates/system-fragments/reply.md +7 -6
  208. package/kits/templates/system-fragments/session.md +5 -14
  209. package/package.json +6 -3
  210. package/dist/core/memory/memory-session-policy.js +0 -41
  211. package/dist/core/memory/memory-signature.js +0 -257
  212. package/kits/docs/evolcore/group-fs.md +0 -17
  213. package/kits/docs/venues/aun-group.md +0 -45
  214. 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 isSameOrDescendant(resolveProtectedCandidate(input.file_path, projectPath), workspace);
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 isSameOrDescendant(resolveProtectedCandidate(scope, projectPath), workspace);
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 (!isSameOrDescendant(base, workspace))
1463
+ if (!withinBoundary(base))
1456
1464
  return false;
1457
1465
  const scope = globReadScope(input, { cwd: projectPath });
1458
- return isSameOrDescendant(scope, workspace);
1466
+ return withinBoundary(scope);
1459
1467
  }
1460
1468
  return true;
1461
1469
  }
1462
- function readonlyShellStaysInWorkspace(analysis, projectPath, managedTempDir) {
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 managedTemp = managedTempDir ? resolveProtectedCandidate(managedTempDir) : undefined;
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
- || (!!managedTemp && isSameOrDescendant(candidate, managedTemp));
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, managedTempDir)) {
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
- ' • 创建快照:ec config snapshot --desc "说明"\n' +
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 双引号正文包含未转义的 Shell 展开;请将反引号写成 \\`,将 $()、${}、$变量中的 $ 写成 \\$。Shell 传给 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 命令引号未闭合或存在错误嵌套;PowerShell 也会解释中文弯引号。请按 PowerShell 规则转义,或把消息正文写入当前 $TMPDIR 后使用 --text-from-file'
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 = !!principalId
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 the authenticated EC Web Agent settings may mutate this policy. */
193
- export function agentFullAccessWriteError(input) {
194
- if (!input.ecwebManagement) {
195
- return 'Agent fullaccess can only be configured from ECWeb';
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 (!input.daemonOwner || input.delegated) {
198
- return 'Only authenticated ECWeb management may configure Agent fullaccess';
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
- const content = fs.readFileSync(filePath, 'utf-8');
329
- const lines = content.trimEnd().split('\n');
330
- for (let i = lines.length - 1; i >= 0; i--) {
331
- const line = lines[i].trim();
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
- catch (e) {
344
- if (e.code === 'ENOENT')
345
- return undefined;
346
- throw e;
460
+ finally {
461
+ fs.closeSync(fd);
347
462
  }
348
463
  }
349
464
  export function readAllJsonlLines(filePath) {