@visual-z/xeno 0.8.4 → 0.9.0

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 (350) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2071
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1818
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3108
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/branch-panel.ts +0 -47
  312. package/src/tui/ui/canvas.ts +0 -96
  313. package/src/tui/ui/colors.ts +0 -70
  314. package/src/tui/ui/composer.ts +0 -199
  315. package/src/tui/ui/control-panel.ts +0 -25
  316. package/src/tui/ui/control-sidebar.ts +0 -49
  317. package/src/tui/ui/dialog.ts +0 -398
  318. package/src/tui/ui/draft.ts +0 -23
  319. package/src/tui/ui/duration.ts +0 -16
  320. package/src/tui/ui/edge.ts +0 -186
  321. package/src/tui/ui/image-size.ts +0 -44
  322. package/src/tui/ui/ink.ts +0 -210
  323. package/src/tui/ui/items.ts +0 -42
  324. package/src/tui/ui/keys.ts +0 -40
  325. package/src/tui/ui/layout.ts +0 -74
  326. package/src/tui/ui/list-model.ts +0 -86
  327. package/src/tui/ui/list-view.ts +0 -172
  328. package/src/tui/ui/mascot.ts +0 -67
  329. package/src/tui/ui/messages.ts +0 -518
  330. package/src/tui/ui/motion.ts +0 -28
  331. package/src/tui/ui/paths.ts +0 -10
  332. package/src/tui/ui/pictures.ts +0 -108
  333. package/src/tui/ui/prompt-history.ts +0 -60
  334. package/src/tui/ui/screen.ts +0 -579
  335. package/src/tui/ui/status.ts +0 -403
  336. package/src/tui/ui/styled.ts +0 -85
  337. package/src/tui/ui/tool-card.ts +0 -150
  338. package/src/tui/width.ts +0 -567
  339. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  340. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  341. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  342. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  343. package/src/vendor/pi-rich/html.ts +0 -51
  344. package/src/vendor/pi-rich/image-convert.ts +0 -49
  345. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  346. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  347. package/src/vendor/pi-rich/image-resize.ts +0 -123
  348. package/src/vendor/pi-rich/mime.ts +0 -116
  349. package/src/vendor/pi-rich/photon.ts +0 -139
  350. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,370 +0,0 @@
1
- /** Pi's factory/registration protocol, attached to xeno's session rather than another AgentSession. */
2
- import { loadExtensionFactory } from "./extension-loader.ts";
3
- import { AsyncLocalStorage } from "node:async_hooks";
4
- import type { ResourceIndex } from "../store/resource-index.ts";
5
- import type { ExtensionEvent, LogEvent } from "../core/events.ts";
6
- import { isJsonValue, type JsonValue } from "../core/tool-contract.ts";
7
- import type { ToolContext, ToolDefinition } from "./tools.ts";
8
- import { nativeCommandResult } from "./tools.ts";
9
- import { attachImageBytes } from "./images.ts";
10
- import type { ExtensionComponent, ExtensionUI, ExtensionTheme } from "./extension-ui.ts";
11
- import { DISCOVERY_TOOL, DISCOVERY_TOOL_NAME } from "./tool-discovery.ts";
12
- import { LazyIntegration, type ExtensionIntegration } from "./integrations.ts";
13
-
14
- export interface ExtensionContext {
15
- ui: ExtensionUI; cwd: string; mode: "tui" | "json" | "print"; hasUI: boolean;
16
- model: unknown; thinkingLevel: string; signal: AbortSignal | undefined;
17
- isIdle(): boolean; isProjectTrusted(): boolean; hasPendingMessages(): boolean;
18
- abort(): void; shutdown(): void; getSystemPrompt(): string;
19
- sessionManager: {
20
- getEntries(): readonly LogEvent[]; getBranch(): readonly LogEvent[];
21
- getSessionId(): string; getSessionFile(): string; getCwd(): string;
22
- };
23
- [key: string]: unknown;
24
- }
25
- export type ExtensionHandler = (event: Record<string, unknown>, ctx: ExtensionContext) => unknown | Promise<unknown>;
26
- export interface ExtensionCommand {
27
- description?: string;
28
- handler(args: string, ctx: ExtensionContext): void | Promise<void>;
29
- getArgumentCompletions?(prefix: string): { value: string; label: string; description?: string }[] | null | Promise<{ value: string; label: string; description?: string }[] | null>;
30
- }
31
- export interface ExtensionToolResult {
32
- content: ({ type: "text"; text: string } | { type: "image"; data: string; mimeType: string })[];
33
- details?: JsonValue; isError?: boolean;
34
- }
35
- export interface ExtensionToolRenderContext {
36
- args: unknown; toolCallId: string; invalidate(): void;
37
- lastComponent: ExtensionComponent | undefined; state: Record<string, unknown>; cwd: string;
38
- executionStarted: boolean; argsComplete: boolean; isPartial: boolean; expanded: boolean; showImages: boolean; isError: boolean;
39
- }
40
- export interface ExtensionTool {
41
- name: string; label?: string; description: string; parameters: unknown;
42
- /** R11: omitted remains direct; deferred schemas are activated at a Session checkpoint. */
43
- exposure?: "direct" | "deferred";
44
- promptSnippet?: string; promptGuidelines?: string[]; executionMode?: "parallel" | "sequential";
45
- prepareArguments?(input: unknown): unknown;
46
- validateInput?(input: unknown): void;
47
- execute(id: string, args: Record<string, unknown>, signal: AbortSignal | undefined,
48
- onUpdate: (result: ExtensionToolResult) => void, ctx: ExtensionContext): Promise<ExtensionToolResult>;
49
- renderCall?(args: unknown, theme: ExtensionTheme, context: ExtensionToolRenderContext): ExtensionComponent;
50
- renderResult?(result: ExtensionToolResult, options: { expanded: boolean; isPartial: boolean }, theme: ExtensionTheme, context: ExtensionToolRenderContext): ExtensionComponent;
51
- }
52
- export type ExtensionRenderer = (message: Record<string, unknown>, options: { expanded: boolean; outputPad: number }, theme: ExtensionTheme) => ExtensionComponent | undefined;
53
- type Flag = { type: "string" | "boolean"; description?: string; default?: string | boolean };
54
- export interface ExtensionActions {
55
- context(): ExtensionContext;
56
- append(entry: Omit<ExtensionEvent, "t" | "id" | "at">, options?: { triggerTurn?: boolean; deliverAs?: string }): Promise<void>;
57
- user(text: string, options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean }): Promise<void>;
58
- setName(name: string): Promise<void>;
59
- setLabel(id: string, label: string | undefined): Promise<void>;
60
- setTools(names: string[]): Promise<void>;
61
- getTools(): { name: string; description: string; parameters: unknown }[];
62
- getAllTools?(): { name: string; description: string; parameters: unknown }[];
63
- setModel(model: unknown): Promise<boolean>;
64
- setThinking(level: string): Promise<void>;
65
- exec(command: string, args: string[], options?: { cwd?: string; timeout?: number; signal?: AbortSignal }): Promise<{ stdout: string; stderr: string; code: number; killed: boolean }>;
66
- }
67
- export interface ExtensionAPI {
68
- on(event: string, handler: ExtensionHandler): () => void;
69
- registerCommand(name: string, command: ExtensionCommand): void;
70
- registerTool(tool: ExtensionTool): void;
71
- registerIntegration(integration: ExtensionIntegration): void;
72
- registerShortcut(key: string, shortcut: { description?: string; handler(ctx: ExtensionContext): void | Promise<void> }): void;
73
- registerFlag(name: string, options: Flag): void;
74
- getFlag(name: string): string | boolean | undefined;
75
- registerMessageRenderer(name: string, renderer: ExtensionRenderer): void;
76
- registerEntryRenderer(name: string, renderer: ExtensionRenderer): void;
77
- registerMarkdownTransformer(transform: (text: string, context: Record<string, unknown>) => string | undefined): void;
78
- sendMessage(message: { customType: string; content: string | { type: "text"; text: string }[]; display?: boolean; details?: JsonValue }, options?: { triggerTurn?: boolean; deliverAs?: string }): void;
79
- sendUserMessage(text: string, options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean }): void;
80
- appendEntry(name: string, data?: JsonValue): void;
81
- setSessionName(name: string): void;
82
- setLabel(id: string, label: string | undefined): void;
83
- getActiveTools(): string[];
84
- getAllTools(): { name: string; description: string; parameters: unknown }[];
85
- setActiveTools(names: string[]): void;
86
- setModel(model: unknown): Promise<boolean>;
87
- getThinkingLevel(): string;
88
- setThinkingLevel(level: string): void;
89
- getCommands(): { name: string; description?: string }[];
90
- exec: ExtensionActions["exec"];
91
- events: { on(name: string, handler: (data: unknown) => void): () => void; emit(name: string, data?: unknown): void };
92
- }
93
-
94
- const prohibited = new Set(["context", "context_with_system", "session_before_compact", "session_compact", "session_compact_failed"]);
95
- let generation = 0;
96
-
97
- interface ExtensionRegistryOptions {
98
- reserved?: readonly string[]; reservedFlags?: readonly string[];
99
- flags?: Readonly<Record<string, string | boolean>>;
100
- flagValue?: (name: string, definition: Flag) => string | boolean | undefined;
101
- report?: (message: string) => void;
102
- }
103
-
104
- /** Registrations are staged before activation, and disposed as one generation on reload. */
105
- export class ExtensionRegistry {
106
- /** Harness-owned tools keep capability metadata; arbitrary extensions cannot claim it. */
107
- readonly builtinTools = new Map<string, ToolDefinition>();
108
- private readonly ownedDisposers: (() => Promise<void>)[] = [];
109
- installBuiltin(tool: ToolDefinition): void {
110
- if (this.tools.has(tool.name) || this.builtinTools.has(tool.name)) throw Error(`duplicate harness tool ${tool.name}`);
111
- this.builtinTools.set(tool.name, tool);
112
- }
113
- own(dispose: () => Promise<void>): void { this.ownedDisposers.push(dispose); }
114
- readonly commands = new Map<string, ExtensionCommand>();
115
- readonly tools = new Map<string, ExtensionTool>();
116
- readonly shortcuts = new Map<string, { description?: string; handler(ctx: ExtensionContext): void | Promise<void> }>();
117
- readonly flags = new Map<string, Flag>();
118
- readonly renderers = new Map<string, ExtensionRenderer>();
119
- readonly entryRenderers = new Map<string, ExtensionRenderer>();
120
- readonly markdown: ((text: string, context: Record<string, unknown>) => string | undefined)[] = [];
121
- readonly diagnostics: string[] = [];
122
- readonly loaded: string[] = [];
123
- private handlers = new Map<string, Set<ExtensionHandler>>();
124
- private bus = new Map<string, Set<(data: unknown) => void>>();
125
- private pending = new Set<Promise<void>>();
126
- /** Pi A / D8: void session mutations still have a durable invocation order. */
127
- private actionTail: Promise<void> = Promise.resolve();
128
- private disposers: (() => void)[] = [];
129
- private detachListeners = new Set<() => void>();
130
- private toolUpdateListeners = new Set<(name: string, id: string, input: unknown, result: ExtensionToolResult) => void>();
131
- private actions: ExtensionActions | undefined;
132
- private readonly execution = new AsyncLocalStorage<ToolContext>();
133
- private disposed = false;
134
- private readonly integrations = new Map<string, LazyIntegration>();
135
- private readonly integrationTools = new Map<string, LazyIntegration>();
136
- private readonly options: ExtensionRegistryOptions;
137
- constructor(options: ExtensionRegistryOptions = {}) { this.options = options; }
138
-
139
- bind(actions: ExtensionActions): void { this.actions = actions; }
140
- onDispose(detach: () => void): () => void { this.detachListeners.add(detach); return () => this.detachListeners.delete(detach); }
141
- onToolUpdate(listener: (name: string, id: string, input: unknown, result: ExtensionToolResult) => void): () => void {
142
- this.toolUpdateListeners.add(listener); return () => this.toolUpdateListeners.delete(listener);
143
- }
144
- private active(): ExtensionActions {
145
- if (this.disposed || !this.actions) throw new Error("Extension session is not active");
146
- return this.actions;
147
- }
148
- private enqueue(operation: () => Promise<void>): void {
149
- const pending = this.actionTail.then(operation).catch(error => this.report(String(error))).finally(() => this.pending.delete(pending));
150
- this.pending.add(pending);
151
- // A failing observer must not poison later accepted work or create an unhandled rejection.
152
- this.actionTail = pending.catch(() => {});
153
- }
154
- async settle(): Promise<void> { while (this.pending.size) await Promise.all([...this.pending]); }
155
- private report(message: string): void { this.diagnostics.push(message); this.options.report?.(`Extension: ${message}`); }
156
- private insert<T>(map: Map<string, T>, name: string, value: T, source: string): void {
157
- if (!name || map.has(name)) throw new Error(`${source}: duplicate or empty registration: ${name}`);
158
- map.set(name, value);
159
- }
160
- api(source: string): ExtensionAPI {
161
- return {
162
- on: (event, handler) => {
163
- if (prohibited.has(event)) throw new Error(`${event} rewrites model context or summarizes history; xeno requires append-only events and mechanical checkpoints`);
164
- const handlers = this.handlers.get(event) ?? new Set<ExtensionHandler>(); this.handlers.set(event, handlers); handlers.add(handler);
165
- return () => { handlers.delete(handler); };
166
- },
167
- registerCommand: (name, command) => {
168
- if (this.options.reserved?.includes(name)) throw new Error(`Command /${name} is reserved`);
169
- this.insert(this.commands, name, command, source);
170
- },
171
- registerTool: tool => {
172
- if (this.builtinTools.has(tool.name)) throw Error(`${tool.name} is owned by the harness`);
173
- if (!/^[a-zA-Z0-9_-]+$/.test(tool.name) || typeof tool.execute !== "function") throw new Error("Invalid tool definition");
174
- if (["note", "history", "branch", "resolve", DISCOVERY_TOOL_NAME].includes(tool.name)) throw new Error(`${tool.name} owns the append-only harness and cannot be replaced`);
175
- if (tool.exposure !== undefined && tool.exposure !== "direct" && tool.exposure !== "deferred") throw new Error("Unknown tool exposure");
176
- if (tool.exposure === "deferred" && tool.name.length > 128) throw new Error("Deferred tool names must fit the discovery receipt");
177
- extensionSchema(tool.parameters); this.insert(this.tools, tool.name, tool, source);
178
- },
179
- registerIntegration: integration => {
180
- if (!/^[a-zA-Z0-9_-]{1,64}$/.test(integration.name) || this.integrations.has(integration.name)
181
- || typeof integration.connect !== "function" || !Array.isArray(integration.tools) || !integration.tools.length) throw Error("Invalid or duplicate integration");
182
- const repair = integration.repair;
183
- if (repair && (!/^[a-zA-Z0-9_-]{1,64}$/.test(repair.command) || this.commands.has(repair.command)
184
- || this.options.reserved?.includes(repair.command) || typeof repair.run !== "function")) throw Error("Invalid or duplicate integration repair command");
185
- const names = new Set<string>();
186
- for (const tool of integration.tools) {
187
- if (names.has(tool.name) || this.tools.has(tool.name)) throw Error("Duplicate integration tool");
188
- names.add(tool.name);
189
- }
190
- // Copy the manifest before publishing. Registrations and live connection ownership share one generation.
191
- const owner = new LazyIntegration({ ...integration, ...(repair ? { repair: { ...repair } } : {}),
192
- tools: integration.tools.map(tool => ({ ...tool, parameters: structuredClone(tool.parameters) })) });
193
- const added: string[] = [];
194
- try {
195
- for (const descriptor of owner.definition.tools) {
196
- this.api(source).registerTool({ ...descriptor, exposure: descriptor.exposure ?? "deferred", execute: async () => { throw Error("Integration tools require their owner"); } });
197
- added.push(descriptor.name); this.integrationTools.set(descriptor.name, owner);
198
- }
199
- if (repair) this.api(source).registerCommand(repair.command, { description: repair.description, handler: async (_args, ctx) => {
200
- const result = await owner.repair(ctx);
201
- await this.active().append({ customType: "integration_repair", content: result.content, display: true, inContext: false,
202
- ...(result.data === undefined ? {} : { data: result.data }) });
203
- } });
204
- this.integrations.set(integration.name, owner);
205
- } catch (error) {
206
- for (const name of added) { this.tools.delete(name); this.integrationTools.delete(name); }
207
- void owner.dispose(); throw error;
208
- }
209
- },
210
- registerShortcut: (name, value) => this.insert(this.shortcuts, name, value, source),
211
- registerFlag: (name, value) => {
212
- if (!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(name) || this.options.reservedFlags?.includes(name)) throw new Error(`CLI flag --${name} is reserved or invalid`);
213
- this.insert(this.flags, name, value, source);
214
- },
215
- getFlag: name => { const flag = this.flags.get(name); return this.options.flags && Object.hasOwn(this.options.flags, name) ? this.options.flags[name] : (flag ? this.options.flagValue?.(name, flag) ?? flag.default : undefined); },
216
- registerMessageRenderer: (name, value) => this.insert(this.renderers, name, value, source),
217
- registerEntryRenderer: (name, value) => this.insert(this.entryRenderers, name, value, source),
218
- registerMarkdownTransformer: transform => { this.markdown.push(transform); },
219
- sendMessage: (message, options) => this.enqueue(() => this.active().append({ customType: message.customType,
220
- content: typeof message.content === "string" ? message.content : message.content.map(c => c.text).join("\n"),
221
- display: message.display !== false, inContext: true, ...(message.details === undefined ? {} : { data: message.details }) }, options)),
222
- appendEntry: (name, data) => this.enqueue(() => this.active().append({ customType: name, content: "", display: this.entryRenderers.has(name), inContext: false, ...(data === undefined ? {} : { data }) })),
223
- sendUserMessage: (text, options) => this.enqueue(() => this.active().user(text, options)),
224
- setSessionName: name => this.enqueue(() => this.active().setName(name)),
225
- setLabel: (id, label) => this.enqueue(() => this.active().setLabel(id, label)),
226
- getActiveTools: () => this.active().getTools().map(tool => tool.name),
227
- getAllTools: () => this.active().getAllTools?.() ?? this.active().getTools(),
228
- setActiveTools: names => this.enqueue(() => this.active().setTools(names)),
229
- setModel: model => this.active().setModel(model),
230
- getThinkingLevel: () => this.active().context().thinkingLevel,
231
- setThinkingLevel: level => this.enqueue(() => this.active().setThinking(level)),
232
- getCommands: () => [...this.commands].map(([name, command]) => ({ name, ...(command.description ? { description: command.description } : {}) })),
233
- exec: (command, args, options) => {
234
- const pending = (async () => {
235
- this.active();
236
- const scope = this.execution.getStore();
237
- if (!scope) return this.active().exec(command, [...args], options);
238
- if (!scope.invokeHost || !scope.outputStore) throw new Error("native commands require a live Session host scope");
239
- const result = await scope.invokeHost({ argv: [command, ...args], ...(options?.cwd !== undefined ? { cwd: options.cwd } : {}),
240
- ...(options?.timeout !== undefined ? { timeout: options.timeout } : {}) }, options?.signal);
241
- return nativeCommandResult(result, scope.outputStore);
242
- })();
243
- // The host owns accepted work even if the extension drops this SDK promise.
244
- // Keep the rejection available to an awaiter while observing it ourselves.
245
- void pending.catch(error => this.report(`exec: ${String(error)}`));
246
- return pending;
247
- },
248
- events: {
249
- on: (name, handler) => { const listeners = this.bus.get(name) ?? new Set(); this.bus.set(name, listeners); listeners.add(handler); return () => { listeners.delete(handler); }; },
250
- emit: (name, data) => { for (const handler of this.bus.get(name) ?? []) try { handler(data); } catch (error) { this.report(String(error)); } },
251
- },
252
- };
253
- }
254
-
255
- async load(index: ResourceIndex): Promise<void> {
256
- const version = ++generation;
257
- for (const resource of index.entries.filter(r => r.kind === "extensions" && r.enabled)) {
258
- const maps = [this.commands, this.tools, this.shortcuts, this.flags, this.renderers, this.entryRenderers, this.integrations, this.integrationTools];
259
- const before = maps.map(map => new Set(map.keys()));
260
- const oldHandlers = new Map([...this.handlers].map(([name, handlers]) => [name, new Set(handlers)]));
261
- const oldBus = new Map([...this.bus].map(([name, handlers]) => [name, new Set(handlers)]));
262
- const transforms = this.markdown.length;
263
- try {
264
- // External entry paths are runtime resources, never internal module aliases.
265
- const factory = await loadExtensionFactory(resource.path, version);
266
- if (typeof factory !== "function") throw new Error("Expected a default extension factory");
267
- const dispose: unknown = await factory(this.api(resource.path));
268
- if (typeof dispose === "function") this.disposers.push(dispose as () => void);
269
- this.loaded.push(resource.path);
270
- } catch (error) {
271
- maps.forEach((map, i) => { for (const key of map.keys()) if (!before[i]!.has(key)) map.delete(key); });
272
- this.handlers = oldHandlers; this.bus = oldBus; this.markdown.length = transforms;
273
- this.report(`${resource.path}: ${String(error)}`);
274
- }
275
- }
276
- }
277
-
278
- async emit(type: string, event: Record<string, unknown> = {}): Promise<Record<string, unknown>[]> {
279
- if (!this.actions || this.disposed) return [];
280
- const results: Record<string, unknown>[] = [];
281
- for (const handler of [...this.handlers.get(type) ?? []]) {
282
- try { const value = await handler({ ...event, type }, this.active().context()); if (typeof value === "object" && value !== null) results.push(value as Record<string, unknown>); }
283
- catch (error) { this.report(`${type}: ${String(error)}`); }
284
- }
285
- return results;
286
- }
287
- async command(name: string, args: string): Promise<boolean> {
288
- const command = this.commands.get(name); if (!command) return false;
289
- try { await command.handler(args, this.active().context()); await this.settle(); } catch (error) { this.report(`/${name}: ${String(error)}`); }
290
- return true;
291
- }
292
- definitions(): ReadonlyMap<string, ToolDefinition> {
293
- const definitions = new Map<string, ToolDefinition>([...this.tools].map(([name, tool]) => [name, {
294
- name, description: tool.description, input_schema: extensionSchema(tool.parameters),
295
- // Arbitrary local code is not provably read-only. Keep admission and effects
296
- // serial; an extension's annotation cannot grant a parallel write capability.
297
- access: "write", execution: "exclusive", branch: "none",
298
- ...(this.integrationTools.has(name) ? { integrationHealth: () => this.integrationTools.get(name)!.observe() } : {}),
299
- ...(tool.prepareArguments ? { prepareArguments: tool.prepareArguments } : {}),
300
- ...(tool.validateInput ? { validateInput: tool.validateInput } : {}),
301
- run: async (ctx: ToolContext, raw: Record<string, unknown>) => {
302
- const context = { ...this.active().context(), signal: ctx.signal, tools: this.active().getTools(),
303
- executeTool: async (target: string, input: unknown) => {
304
- if (!ctx.invoke) return { isError: true, content: [{ type: "text", text: "Nested tools require a live Session execution scope" }] };
305
- const out = await ctx.invoke(target, input);
306
- return { isError: !out.ok, content: [{ type: "text", text: out.content }], details: out.data,
307
- outcome: out.outcome, diagnostic: out.diagnostic, warnings: out.warnings, artifact: out.content_ref, observation: out.observation };
308
- } };
309
- const update = (update: ExtensionToolResult) => {
310
- for (const listener of this.toolUpdateListeners) try { listener(name, ctx.callId ?? name, raw, update); } catch (error) { this.report(String(error)); }
311
- ctx.onOutput?.(new TextEncoder().encode(update.content.filter(c => c.type === "text").map(c => c.text).join("\n")));
312
- };
313
- const integration = this.integrationTools.get(name);
314
- const managed = integration ? await this.execution.run(ctx, () => integration.execute(name, ctx.callId ?? name, raw, ctx.signal, update, context)) : undefined;
315
- if (managed && !managed.ok) return managed.result;
316
- const result = managed?.ok ? managed.result : await this.execution.run(ctx, () => tool.execute(ctx.callId ?? name, raw, ctx.signal, update, context));
317
- const text = result.content.filter(c => c.type === "text").map(c => c.text).join("\n");
318
- const images = [];
319
- for (const item of result.content) if (item.type === "image" && ctx.imageStore) images.push(await attachImageBytes(Buffer.from(item.data, "base64"), name + ".png", ctx.imageStore, ctx.imageAutoResize));
320
- return { ok: result.isError !== true && !ctx.signal?.aborted, content: text,
321
- ...(managed?.ok ? { data: { kind: "integration_observation", version: 1, phase: "call", health: managed.observation,
322
- ...(isJsonValue(result.details) ? { result: result.details } : {}) } } : isJsonValue(result.details) ? { data: result.details } : {}),
323
- ...(images.length ? { images } : {}) };
324
- },
325
- }]));
326
- if ([...this.tools.values()].some(tool => tool.exposure === "deferred")) definitions.set(DISCOVERY_TOOL_NAME, DISCOVERY_TOOL);
327
- for (const [name, tool] of this.builtinTools) definitions.set(name, tool);
328
- return definitions;
329
- }
330
- toolHooks(): NonNullable<ToolContext["extensionHooks"]> {
331
- return {
332
- scope: (ctx, action) => this.execution.run(ctx, action),
333
- serial: () => ["tool_call", "tool_execution_start", "tool_execution_update", "tool_result", "tool_execution_end"].some(type => (this.handlers.get(type)?.size ?? 0) > 0),
334
- before: async (toolName, input, toolCallId, parentToolCallId) => {
335
- const blocked = (await this.emit("tool_call", { toolName, input, toolCallId, ...(parentToolCallId ? { parentToolCallId } : {}) })).find(result => result["block"] === true);
336
- return blocked ? String(blocked["reason"] ?? "Blocked by extension") : undefined;
337
- },
338
- event: async (type, fields) => { await this.emit(type, fields); },
339
- };
340
- }
341
- async dispose(): Promise<void> {
342
- if (this.disposed) return;
343
- for (const dispose of this.ownedDisposers.splice(0)) await dispose();
344
- await Promise.all([...this.integrations.values()].map(owner => owner.dispose()));
345
- for (const detach of this.detachListeners) try { detach(); } catch (error) { this.report(String(error)); }
346
- this.detachListeners.clear(); this.toolUpdateListeners.clear();
347
- await this.emit("session_shutdown"); await this.settle(); this.disposed = true;
348
- for (const dispose of this.disposers.reverse()) try { dispose(); } catch (error) { this.report(String(error)); }
349
- this.handlers.clear(); this.bus.clear(); this.actions = undefined;
350
- }
351
- /** Q.13: a new generation must not overlap an unresolved callback from its predecessor. */
352
- async releaseIntegrations(): Promise<boolean> {
353
- if ([...this.integrations.values()].some(owner => !owner.canReleaseForReload())) return false;
354
- const released = await Promise.all([...this.integrations.values()].map(owner => owner.releaseForReload()));
355
- return released.every(Boolean);
356
- }
357
- restoreIntegrations(): boolean {
358
- const restored = [...this.integrations.values()].map(owner => owner.resumeAfterReload());
359
- return restored.every(Boolean);
360
- }
361
- }
362
-
363
- function extensionSchema(parameters: unknown): ToolDefinition["input_schema"] {
364
- if (typeof parameters !== "object" || parameters === null) throw new Error("Tool parameters must be an object schema");
365
- const schema = parameters as Record<string, unknown>;
366
- if (schema["type"] !== "object" || schema["properties"] !== undefined && (typeof schema["properties"] !== "object" || schema["properties"] === null || Array.isArray(schema["properties"]))) throw new Error("Tool parameters need an object schema");
367
- const required = schema["required"] ?? [];
368
- if (!Array.isArray(required) || required.some(value => typeof value !== "string")) throw new Error("Tool required fields must be names");
369
- return { ...schema, type: "object", properties: (schema["properties"] ?? {}) as Record<string, unknown>, required: required as string[] };
370
- }
@@ -1,38 +0,0 @@
1
- /** Trusted formatter configuration controls commands; model arguments supply only paths. */
2
- import { planFormatters, runConfiguredFormatters, validateFormatters, type FormatterConfig } from "./formatter.ts";
3
- import { presentObservation, type ToolDefinition } from "./tools.ts";
4
- import { failureDiagnostic, failureFromError, NOT_STARTED, UNKNOWN_EFFECT } from "../core/tool-failure.ts";
5
- import { readEditableFile } from "../store/file-checkpoints.ts";
6
-
7
- export function createFormatterTool(configs: readonly FormatterConfig[]): ToolDefinition {
8
- const frozen = validateFormatters(configs);
9
- return {
10
- name: "format", access: "shell", execution: "exclusive", branch: "write",
11
- description: "Run explicitly configured trusted formatters for these existing workspace files. Every formatter command goes through the session's normal command admission, logs and checkpoints; no executable is auto-discovered or installed. Returns real command outcomes and resulting file digests. Formatting can change content; read the current file before deriving further edits.",
12
- input_schema: { type: "object", required: ["paths"], properties: { paths: { type: "array", minItems: 1, maxItems: 100, items: { type: "string" } } } },
13
- run: async (ctx, input) => {
14
- if (!ctx.invokeHost) return { ok: false, content: "Formatter requires the session command lifecycle.", diagnostic: failureDiagnostic("environment_missing", "Formatter requires the session command lifecycle.", NOT_STARTED) };
15
- let executing = false;
16
- try {
17
- const paths = input["paths"] as string[], plans = await planFormatters(ctx.root, paths, frozen);
18
- if (!plans.length) return { ok: false, content: "No configured formatter matches these file extensions.", diagnostic: failureDiagnostic("environment_missing", "No configured formatter matches these file extensions.", NOT_STARTED) };
19
- executing = true;
20
- const results = await runConfiguredFormatters(plans, ctx.invokeHost, ctx.signal);
21
- if (!results.length) return { ok: false, content: "Formatter cancelled before execution.", diagnostic: failureDiagnostic("cancelled", "Formatter cancelled before execution.", NOT_STARTED) };
22
- const files = [];
23
- for (const path of [...new Set(results.map(result => result.plan.path))]) {
24
- const observed = await readEditableFile(ctx.root, path);
25
- files.push({ path, sha256: observed.version?.sha256 ?? null });
26
- }
27
- const receipts = results.map(({ plan, result }) => `${plan.formatter} ${plan.path}: ${result.ok ? "completed" : "failed"}\n${result.content}`).join("\n\n");
28
- const last = results.at(-1)!.result;
29
- return { ...await presentObservation(ctx, receipts + "\n\nCurrent file versions:\n" + files.map(file => `${file.path}: ${file.sha256 ?? "absent"}`).join("\n")),
30
- ok: results.length === plans.length && results.every(result => result.result.ok), data: { files, commands: results.length },
31
- ...(last.outcome === undefined ? {} : { outcome: last.outcome }), ...(last.diagnostic === undefined ? {} : { diagnostic: last.diagnostic }) };
32
- } catch (error) {
33
- const diagnostic = failureFromError(error, executing ? UNKNOWN_EFFECT : NOT_STARTED);
34
- return { ok: false, content: diagnostic.message, diagnostic };
35
- }
36
- },
37
- };
38
- }
@@ -1,50 +0,0 @@
1
- /** Explicit trusted formatters use the original command broker, never hidden subprocesses. */
2
- import { extname, resolve } from "node:path";
3
- import { readEditableFile } from "../store/file-checkpoints.ts";
4
- import type { NativeCommandInput, ToolResult } from "./tools.ts";
5
- export interface FormatterConfig { name: string; extensions: readonly string[]; command: readonly string[]; disabled?: boolean }
6
- export interface FormatterPlan { formatter: string; path: string; input: NativeCommandInput }
7
-
8
- export function validateFormatters(value: unknown): FormatterConfig[] {
9
- if (!Array.isArray(value)) throw new Error("formatters must be an array of trusted command definitions");
10
- const names = new Set<string>();
11
- return value.map((entry: unknown) => {
12
- if (entry === null || typeof entry !== "object" || Array.isArray(entry)) throw new Error("formatter must be an object");
13
- const config = entry as Record<string, unknown>, name = config["name"], extensions = config["extensions"], command = config["command"], disabled = config["disabled"];
14
- if (Object.keys(config).some(key => !["name", "extensions", "command", "disabled"].includes(key))) throw new Error("unknown formatter configuration field");
15
- if (typeof name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(name) || names.has(name)) throw new Error("formatter names must be unique stable identifiers");
16
- if (!Array.isArray(extensions) || !extensions.length || !extensions.every(extension => typeof extension === "string" && (extension === "*" || /^\.[A-Za-z0-9]+$/.test(extension)))) throw new Error("formatter extensions must contain dotted extensions or *");
17
- if (!Array.isArray(command) || !command.length || !command.every(argument => typeof argument === "string" && !argument.includes("\0")) || !command[0]?.trim() || !command.some(argument => argument.includes("$FILE"))) throw new Error("formatter command must be literal argv containing $FILE");
18
- if (disabled !== undefined && typeof disabled !== "boolean") throw new Error("formatter disabled must be boolean");
19
- names.add(name);
20
- return { name, extensions: extensions as string[], command: command as string[], ...(disabled === undefined ? {} : { disabled }) };
21
- });
22
- }
23
-
24
- /** Absolute filename substitution prevents option and shell parsing of model paths. */
25
- export async function planFormatters(root: string, paths: readonly string[], configs: readonly FormatterConfig[]): Promise<FormatterPlan[]> {
26
- const definitions = validateFormatters(configs), plans: FormatterPlan[] = [];
27
- const known = new Set<string>();
28
- for (const path of paths) {
29
- const absolute = resolve(root, path);
30
- if (known.has(absolute)) continue;
31
- known.add(absolute);
32
- const source = await readEditableFile(root, path);
33
- if (source.version === null) throw new Error(`formatter target is missing: ${path}`);
34
- for (const config of definitions) if (!config.disabled && (config.extensions.includes("*") || config.extensions.includes(extname(absolute))))
35
- plans.push({ formatter: config.name, path, input: { argv: config.command.map(argument => argument.replaceAll("$FILE", absolute)), cwd: root } });
36
- }
37
- return plans;
38
- }
39
-
40
- /** Every command returns its real receipt; stop after failure or cancellation. */
41
- export async function runConfiguredFormatters(plans: readonly FormatterPlan[], invokeHost: (input: NativeCommandInput, signal?: AbortSignal) => Promise<ToolResult>, signal?: AbortSignal): Promise<{ plan: FormatterPlan; result: ToolResult }[]> {
42
- const results: { plan: FormatterPlan; result: ToolResult }[] = [];
43
- for (const plan of plans) {
44
- if (signal?.aborted) break;
45
- const result = await invokeHost(plan.input, signal);
46
- results.push({ plan, result });
47
- if (!result.ok || result.outcome?.status === "stopped") break;
48
- }
49
- return results;
50
- }
@@ -1,116 +0,0 @@
1
- /** Foundations use the existing catalog, broker, permission and checkpoint owners. */
2
- import { parsePermissionRule, type PermissionRule } from "../core/permissions.ts";
3
- import { parsePatch, patchPaths } from "../core/patch.ts";
4
- import { parseWebSearchConfig } from "../core/web-content.ts";
5
- import { lastTurnChanges } from "../core/review.ts";
6
- import { discoverTargetInstructions, renderTargetInstructions } from "../store/context-files.ts";
7
- import { createPatchTool } from "./patch-tool.ts";
8
- import { createDiffTool } from "./review-tool.ts";
9
- import { createQuestionTool, type QuestionSlot } from "./question-tool.ts";
10
- import { createWebFetchTool, createWebSearchTool } from "./web-tools.ts";
11
- import { LspManager, parseLspConfig } from "./lsp-client.ts";
12
- import { validateFormatters } from "./formatter.ts";
13
- import { createFormatterTool } from "./formatter-tool.ts";
14
- import { permissionResourceAction } from "./permission-resources.ts";
15
- import type { InstructionCheck, ToolContext, ToolDefinition } from "./tools.ts";
16
- import { McpManager } from "./mcp-client.ts";
17
- import { parseMcpServers } from "../core/mcp-config.ts";
18
- import type { ExtensionRegistry } from "./extensions.ts";
19
- import { join } from "node:path";
20
- import { getBlob } from "../store/blobs.ts";
21
-
22
- /**
23
- * Calls that only observe run and carry newly discovered rules (S.6 item 3:
24
- * OpenCode, Gemini CLI and Claude Code do the same). A change is still held
25
- * back until the rules have been read: the reason for blocking first was never
26
- * to notify after a modification (foundation-editing research).
27
- */
28
- const OBSERVING_TOOLS = new Set(["read", "query", "lsp"]);
29
- /** Assumed: more batches than any interleaving of live sessions sharing this runtime. */
30
- const RECENT_BATCHES = 16;
31
-
32
- export class FoundationRuntime {
33
- readonly questionSlot: QuestionSlot;
34
- /** Per batch (assistant event id): instruction key -> the call that carried it. */
35
- private readonly batches = new Map<string, Map<string, string>>();
36
- readonly mcp: McpManager;
37
- readonly tools: ToolDefinition[];
38
- readonly permissions: PermissionRule[];
39
- private readonly lsp: LspManager;
40
- constructor(workspace: string, config: Record<string, unknown>, offline: boolean, configDirectory: string, questionSlot: QuestionSlot = {}) {
41
- this.questionSlot = questionSlot;
42
- const servers = Object.fromEntries(Object.entries(parseMcpServers(config["mcp"])).map(([name, server]) => [name, offline && server.transport === "http" ? { ...server, enabled: false } : server]));
43
- this.mcp = new McpManager(servers, { cwd: workspace, credentialsDirectory: join(configDirectory, "mcp-credentials") });
44
- if (config["permissions"] !== undefined && !Array.isArray(config["permissions"])) throw Error("permissions must be an array");
45
- this.permissions = Array.isArray(config["permissions"]) ? config["permissions"].map(parsePermissionRule) : [];
46
- const lsp = parseLspConfig(config["lsp"]); this.lsp = new LspManager(workspace, lsp);
47
- const formatters = config["formatters"] === undefined ? [] : validateFormatters(config["formatters"]);
48
- const search = parseWebSearchConfig(config["webSearch"]);
49
- const network = [createWebFetchTool(), createWebSearchTool(search)];
50
- if (offline) for (const tool of network) tool.run = async () => ({ ok: false, content: "Network tools are unavailable in offline mode." });
51
- this.tools = [createQuestionTool(this.questionSlot), createPatchTool(), createDiffTool({ lastTurn: async ctx => {
52
- if (!ctx.historyEvents || !ctx.checkpointBlobs) throw Error("original checkpoint history is unavailable");
53
- return { blobs: ctx.checkpointBlobs, ...lastTurnChanges(await ctx.historyEvents()) };
54
- }, canRead: async (ctx, path) => await permissionResourceAction(ctx.root, path, ctx.rules ?? [], ctx.sessionRules ?? [], "diff") === "allow" }), ...network, ...(lsp.length ? [this.lsp.tool()] : [])];
55
- this.tools.push({ ...createFormatterTool(formatters), branch: "none" });
56
- }
57
- async register(registry: ExtensionRegistry): Promise<void> {
58
- for (const tool of this.tools) registry.installBuiltin(tool);
59
- const discovered = await this.mcp.discover();
60
- for (const integration of discovered.integrations) registry.api("xeno-mcp").registerIntegration(integration);
61
- }
62
- /** Calls are prepared one at a time in their batch, so "first to discover carries the rules" is deterministic. */
63
- private claims(batch: string): Map<string, string> {
64
- let claimed = this.batches.get(batch);
65
- if (claimed === undefined) {
66
- claimed = new Map();
67
- this.batches.set(batch, claimed);
68
- for (const old of this.batches.keys()) if (this.batches.size > RECENT_BATCHES) this.batches.delete(old);
69
- }
70
- return claimed;
71
- }
72
- async instructions(ctx: ToolContext, name: string, input: unknown, noContextFiles: boolean): Promise<InstructionCheck | null> {
73
- if (noContextFiles || !["read", "write", "edit", "apply_patch", "query", "format", "lsp"].includes(name) || !input || typeof input !== "object") return null;
74
- const args = input as Record<string, unknown>;
75
- const paths = name === "apply_patch" ? patchPaths(parsePatch(String(args["patchText"]))) : typeof args["path"] === "string" ? [args["path"]] : Array.isArray(args["paths"]) ? args["paths"].filter((path): path is string => typeof path === "string") : [];
76
- if (!paths.length) return null;
77
- const blocked = (content: string): InstructionCheck => ({ kind: "block", result: { ok: false, content } });
78
- const known = new Set<string>();
79
- const history = await ctx.historyEvents?.() ?? [];
80
- // Rules count as read only once the model has seen them: results of this
81
- // same batch may already be committed, but they reach the model together
82
- // with this call's result. Within the batch, claims decide instead.
83
- const batch = history.findLast(event => event.t === "assistant");
84
- const sameBatch = new Set(batch?.t === "assistant" ? batch.blocks.flatMap(block => block.type === "tool_use" ? [block.call_id] : []) : []);
85
- for (const event of history) {
86
- if (event.t !== "tool_result" || sameBatch.has(event.call_id)) continue;
87
- for (const key of event.instructions?.keys ?? []) known.add(key);
88
- let data: unknown = event.data;
89
- if (event.data_ref && ctx.outputStore) {
90
- const saved = await getBlob(ctx.outputStore, event.data_ref.sha256);
91
- if (saved) data = JSON.parse(saved) as unknown;
92
- }
93
- if (!data || typeof data !== "object" || Array.isArray(data)) continue;
94
- const receipt = data as Record<string, unknown>;
95
- if (receipt["kind"] !== "scoped_instructions") continue;
96
- const keys = receipt["keys"]; if (Array.isArray(keys)) for (const key of keys) if (typeof key === "string") known.add(key);
97
- }
98
- const found = await discoverTargetInstructions({ workspace: ctx.root, paths, known,
99
- canRead: async path => await permissionResourceAction(ctx.root, path, ctx.rules ?? [], ctx.sessionRules ?? []) === "allow" });
100
- if (found.notices.some(notice => notice.kind === "skipped" && notice.reason === "instruction read permission is unavailable")) return blocked("Scoped instructions require read permission before this operation.");
101
- const files = found.files.filter(file => !ctx.frozenSystem?.().includes(file.content));
102
- if (!files.length) return null;
103
- for (const file of files) if (await permissionResourceAction(ctx.root, file.path, ctx.rules ?? [], ctx.sessionRules ?? []) !== "allow") return blocked("Scoped instructions require read permission before this operation.");
104
- // Once per batch: a later call in the same assistant message does not receive the same rules again.
105
- const claimed = this.claims(batch?.id ?? "");
106
- const fresh = files.filter(file => !claimed.has(file.key));
107
- const carriers = [...new Set(files.flatMap(file => claimed.get(file.key) ?? []))];
108
- for (const file of fresh) claimed.set(file.key, ctx.callId ?? name);
109
- if (OBSERVING_TOOLS.has(name)) return fresh.length ? { kind: "attach", instructions: { keys: fresh.map(file => file.key), text: renderTargetInstructions(fresh) } } : null;
110
- const earlier = carriers.length ? `Scoped instructions for this target were attached to the result of ${carriers.join(", ")} in this batch. ` : "";
111
- if (!fresh.length) return blocked(`${earlier}No original operation was executed. Follow them, then retry.`);
112
- return { kind: "block", result: { ok: false, content: `Scoped instructions were discovered. ${earlier}Read and follow them before deciding whether to retry the original operation. No original operation was executed.\n\n` + renderTargetInstructions(fresh),
113
- data: { kind: "scoped_instructions", keys: fresh.map(file => file.key), files: fresh.map(file => ({ path: file.path, scope: file.scope, key: file.key, content: file.content, truncated: file.truncated })) } } };
114
- }
115
- async dispose(): Promise<void> { await Promise.all([this.lsp.dispose(), this.mcp.dispose()]); }
116
- }
@@ -1,57 +0,0 @@
1
- /** O.15: a disposable cache over already verified source contributions. */
2
- import { createHash } from "node:crypto";
3
- import type { LogEvent } from "../core/events.ts";
4
- import { HISTORY_INDEX_VERSION, historyKeyInput, historyEntries, historyIndex } from "../core/history-query.ts";
5
-
6
- type View = { entries: ReturnType<typeof historyEntries>; indexed: ReturnType<typeof historyIndex> };
7
- function freeze(value: unknown): void {
8
- if (value === null || typeof value !== "object" || Object.isFrozen(value)) return;
9
- for (const child of Object.values(value)) freeze(child);
10
- Object.freeze(value);
11
- }
12
- function sealedMap<K, V>(source: ReadonlyMap<K, V>): ReadonlyMap<K, V> {
13
- for (const value of source.values()) freeze(value);
14
- // Object.freeze(Map) still permits set/delete. Expose only read methods,
15
- // including a forEach that cannot leak the mutable backing map.
16
- const view: ReadonlyMap<K, V> = {
17
- size: source.size, has: key => source.has(key), get: key => source.get(key),
18
- keys: () => source.keys(), values: () => source.values(), entries: () => source.entries(),
19
- [Symbol.iterator]: () => source[Symbol.iterator](),
20
- forEach(callback, thisArg) { for (const [key, value] of source) callback.call(thisArg, value, key, view); },
21
- };
22
- return Object.freeze(view);
23
- }
24
- export class HistoryCache {
25
- private readonly capacity: number;
26
- private readonly payloadBudget: number;
27
- // Operational assumptions, not segment thresholds or a hard heap bound.
28
- constructor(capacity = 8, payloadBudget = 4 * 1024 * 1024) {
29
- if (!Number.isSafeInteger(capacity) || capacity < 0 || !Number.isSafeInteger(payloadBudget) || payloadBudget < 0) throw new RangeError("invalid history cache budget");
30
- this.capacity = capacity; this.payloadBudget = payloadBudget;
31
- }
32
- private readonly views = new Map<string, { view: View; bytes: number }>();
33
- private builds = 0;
34
- private hits = 0;
35
- private bytes = 0;
36
- get statistics() { return { builds: this.builds, hits: this.hits, entries: this.views.size, payloadBytes: this.bytes }; }
37
- clear(): void { this.views.clear(); this.bytes = 0; }
38
- read(scope: string, nodes: readonly { node: number; events: readonly LogEvent[] }[], sealed: readonly { node: number; sha256: string }[], version = HISTORY_INDEX_VERSION): View {
39
- const key = createHash("sha256").update(JSON.stringify([scope, sealed])).update(historyKeyInput(nodes, version)).digest("hex");
40
- const cached = version === HISTORY_INDEX_VERSION ? this.views.get(key) : undefined;
41
- if (cached) { this.hits++; this.views.delete(key); this.views.set(key, cached); return cached.view; }
42
- this.builds++;
43
- // Freeze a detached projection; never freeze the source log or session state.
44
- const entries = structuredClone(historyEntries(nodes)); freeze(entries);
45
- const indexed = historyIndex(entries);
46
- const view = { entries, indexed: { byEvent: sealedMap(indexed.byEvent), byResource: sealedMap(indexed.byResource), byWork: sealedMap(indexed.byWork) } };
47
- freeze(view);
48
- const bytes = new TextEncoder().encode(JSON.stringify(entries)).length;
49
- if (version !== HISTORY_INDEX_VERSION || this.capacity === 0 || bytes > this.payloadBudget) return view;
50
- this.views.set(key, { view, bytes }); this.bytes += bytes;
51
- while (this.views.size > this.capacity || this.bytes > this.payloadBudget) {
52
- const oldest = this.views.keys().next().value!;
53
- this.bytes -= this.views.get(oldest)!.bytes; this.views.delete(oldest);
54
- }
55
- return view;
56
- }
57
- }