baxian 2.0.2 → 2.0.4

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 (232) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +3 -3
  3. package/dist/agent/bootstrap.js +4 -4
  4. package/dist/agent/branch.d.ts.map +1 -1
  5. package/dist/agent/branch.js +0 -7
  6. package/dist/agent/branch.js.map +1 -1
  7. package/dist/agent/detect/region.d.ts.map +1 -1
  8. package/dist/agent/detect/region.js +0 -3
  9. package/dist/agent/detect/region.js.map +1 -1
  10. package/dist/agent/dispatch-reconciler.d.ts +2 -0
  11. package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
  12. package/dist/agent/dispatch-reconciler.js +48 -55
  13. package/dist/agent/dispatch-reconciler.js.map +1 -1
  14. package/dist/agent/lineage.d.ts.map +1 -1
  15. package/dist/agent/lineage.js +0 -9
  16. package/dist/agent/lineage.js.map +1 -1
  17. package/dist/agent/manager.d.ts +42 -8
  18. package/dist/agent/manager.d.ts.map +1 -1
  19. package/dist/agent/manager.js +717 -375
  20. package/dist/agent/manager.js.map +1 -1
  21. package/dist/agent/net-exec.d.ts.map +1 -1
  22. package/dist/agent/net-exec.js +0 -5
  23. package/dist/agent/net-exec.js.map +1 -1
  24. package/dist/agent/pane-streamer-manager.d.ts.map +1 -1
  25. package/dist/agent/pane-streamer-manager.js +0 -36
  26. package/dist/agent/pane-streamer-manager.js.map +1 -1
  27. package/dist/agent/pane-streamer.d.ts +6 -0
  28. package/dist/agent/pane-streamer.d.ts.map +1 -1
  29. package/dist/agent/pane-streamer.js +126 -83
  30. package/dist/agent/pane-streamer.js.map +1 -1
  31. package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
  32. package/dist/agent/phase-signal-watcher.js +2 -77
  33. package/dist/agent/phase-signal-watcher.js.map +1 -1
  34. package/dist/agent/phase-signal.d.ts +0 -1
  35. package/dist/agent/phase-signal.d.ts.map +1 -1
  36. package/dist/agent/phase-signal.js +0 -17
  37. package/dist/agent/phase-signal.js.map +1 -1
  38. package/dist/agent/preflight.d.ts.map +1 -1
  39. package/dist/agent/preflight.js +0 -18
  40. package/dist/agent/preflight.js.map +1 -1
  41. package/dist/agent/prompt.d.ts.map +1 -1
  42. package/dist/agent/prompt.js +0 -1
  43. package/dist/agent/prompt.js.map +1 -1
  44. package/dist/agent/repo-store.d.ts.map +1 -1
  45. package/dist/agent/repo-store.js +0 -38
  46. package/dist/agent/repo-store.js.map +1 -1
  47. package/dist/agent/runner.d.ts.map +1 -1
  48. package/dist/agent/runner.js +0 -2
  49. package/dist/agent/runner.js.map +1 -1
  50. package/dist/agent/tmux-install.js +0 -1
  51. package/dist/agent/tmux-install.js.map +1 -1
  52. package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
  53. package/dist/agent/tmux-probe-poller.js +4 -11
  54. package/dist/agent/tmux-probe-poller.js.map +1 -1
  55. package/dist/agent/tmux.d.ts.map +1 -1
  56. package/dist/agent/tmux.js +0 -86
  57. package/dist/agent/tmux.js.map +1 -1
  58. package/dist/agent/vt-visible-text.d.ts +46 -1
  59. package/dist/agent/vt-visible-text.d.ts.map +1 -1
  60. package/dist/agent/vt-visible-text.js +639 -63
  61. package/dist/agent/vt-visible-text.js.map +1 -1
  62. package/dist/api/agents.d.ts.map +1 -1
  63. package/dist/api/agents.js +3 -49
  64. package/dist/api/agents.js.map +1 -1
  65. package/dist/api/config.d.ts.map +1 -1
  66. package/dist/api/config.js +10 -59
  67. package/dist/api/config.js.map +1 -1
  68. package/dist/api/hosts.d.ts.map +1 -1
  69. package/dist/api/hosts.js +8 -63
  70. package/dist/api/hosts.js.map +1 -1
  71. package/dist/api/platform-guard.d.ts.map +1 -1
  72. package/dist/api/platform-guard.js +2 -3
  73. package/dist/api/platform-guard.js.map +1 -1
  74. package/dist/api/probe.d.ts.map +1 -1
  75. package/dist/api/probe.js +0 -1
  76. package/dist/api/probe.js.map +1 -1
  77. package/dist/api/projects.d.ts.map +1 -1
  78. package/dist/api/projects.js +15 -28
  79. package/dist/api/projects.js.map +1 -1
  80. package/dist/api/tasks.d.ts.map +1 -1
  81. package/dist/api/tasks.js +94 -58
  82. package/dist/api/tasks.js.map +1 -1
  83. package/dist/app.d.ts +2 -2
  84. package/dist/app.d.ts.map +1 -1
  85. package/dist/app.js +0 -6
  86. package/dist/app.js.map +1 -1
  87. package/dist/cli.d.ts.map +1 -1
  88. package/dist/cli.js +6 -31
  89. package/dist/cli.js.map +1 -1
  90. package/dist/config/hot-reload.d.ts.map +1 -1
  91. package/dist/config/hot-reload.js +0 -4
  92. package/dist/config/hot-reload.js.map +1 -1
  93. package/dist/config/loader.d.ts.map +1 -1
  94. package/dist/config/loader.js +6 -29
  95. package/dist/config/loader.js.map +1 -1
  96. package/dist/config/normalizer.d.ts.map +1 -1
  97. package/dist/config/normalizer.js +0 -19
  98. package/dist/config/normalizer.js.map +1 -1
  99. package/dist/config/validator.d.ts.map +1 -1
  100. package/dist/config/validator.js +31 -229
  101. package/dist/config/validator.js.map +1 -1
  102. package/dist/event/handlers.d.ts.map +1 -1
  103. package/dist/event/handlers.js +3 -11
  104. package/dist/event/handlers.js.map +1 -1
  105. package/dist/event/log.js +0 -2
  106. package/dist/event/log.js.map +1 -1
  107. package/dist/event/ws.d.ts.map +1 -1
  108. package/dist/event/ws.js +3 -3
  109. package/dist/event/ws.js.map +1 -1
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +5 -46
  112. package/dist/index.js.map +1 -1
  113. package/dist/platform/body-digest.d.ts.map +1 -1
  114. package/dist/platform/body-digest.js +0 -4
  115. package/dist/platform/body-digest.js.map +1 -1
  116. package/dist/platform/command-renderer.d.ts.map +1 -1
  117. package/dist/platform/command-renderer.js +0 -3
  118. package/dist/platform/command-renderer.js.map +1 -1
  119. package/dist/platform/comment-cursor.d.ts.map +1 -1
  120. package/dist/platform/comment-cursor.js +0 -21
  121. package/dist/platform/comment-cursor.js.map +1 -1
  122. package/dist/platform/driver-spec.d.ts.map +1 -1
  123. package/dist/platform/driver-spec.js +1 -42
  124. package/dist/platform/driver-spec.js.map +1 -1
  125. package/dist/platform/feedback.d.ts +5 -1
  126. package/dist/platform/feedback.d.ts.map +1 -1
  127. package/dist/platform/feedback.js +7 -1
  128. package/dist/platform/feedback.js.map +1 -1
  129. package/dist/platform/field-mapper.d.ts.map +1 -1
  130. package/dist/platform/field-mapper.js +3 -11
  131. package/dist/platform/field-mapper.js.map +1 -1
  132. package/dist/platform/git-driver.d.ts.map +1 -1
  133. package/dist/platform/git-driver.js +0 -20
  134. package/dist/platform/git-driver.js.map +1 -1
  135. package/dist/platform/manifest.d.ts.map +1 -1
  136. package/dist/platform/manifest.js +0 -3
  137. package/dist/platform/manifest.js.map +1 -1
  138. package/dist/platform/markers.d.ts.map +1 -1
  139. package/dist/platform/markers.js +0 -19
  140. package/dist/platform/markers.js.map +1 -1
  141. package/dist/platform/platform-poller.d.ts +5 -1
  142. package/dist/platform/platform-poller.d.ts.map +1 -1
  143. package/dist/platform/platform-poller.js +8 -49
  144. package/dist/platform/platform-poller.js.map +1 -1
  145. package/dist/platform/plugin-registry.d.ts.map +1 -1
  146. package/dist/platform/plugin-registry.js +1 -12
  147. package/dist/platform/plugin-registry.js.map +1 -1
  148. package/dist/platform/pr-conversation-cache.d.ts +12 -7
  149. package/dist/platform/pr-conversation-cache.d.ts.map +1 -1
  150. package/dist/platform/pr-conversation-cache.js +60 -40
  151. package/dist/platform/pr-conversation-cache.js.map +1 -1
  152. package/dist/platform/preflight-exec.d.ts.map +1 -1
  153. package/dist/platform/preflight-exec.js +0 -9
  154. package/dist/platform/preflight-exec.js.map +1 -1
  155. package/dist/platform/response-parser.d.ts.map +1 -1
  156. package/dist/platform/response-parser.js +0 -4
  157. package/dist/platform/response-parser.js.map +1 -1
  158. package/dist/platform/review-timeline.d.ts +17 -2
  159. package/dist/platform/review-timeline.d.ts.map +1 -1
  160. package/dist/platform/review-timeline.js +93 -106
  161. package/dist/platform/review-timeline.js.map +1 -1
  162. package/dist/platform/row-schema.d.ts.map +1 -1
  163. package/dist/platform/row-schema.js +0 -11
  164. package/dist/platform/row-schema.js.map +1 -1
  165. package/dist/platform/startup.d.ts.map +1 -1
  166. package/dist/platform/startup.js +1 -12
  167. package/dist/platform/startup.js.map +1 -1
  168. package/dist/platform/types.d.ts.map +1 -1
  169. package/dist/platform/types.js +0 -5
  170. package/dist/platform/types.js.map +1 -1
  171. package/dist/platform/verdict-engine.d.ts.map +1 -1
  172. package/dist/platform/verdict-engine.js +0 -15
  173. package/dist/platform/verdict-engine.js.map +1 -1
  174. package/dist/shared/concurrency.d.ts.map +1 -1
  175. package/dist/shared/concurrency.js +0 -2
  176. package/dist/shared/concurrency.js.map +1 -1
  177. package/dist/shared/constants.d.ts +0 -2
  178. package/dist/shared/constants.d.ts.map +1 -1
  179. package/dist/shared/constants.js +0 -3
  180. package/dist/shared/constants.js.map +1 -1
  181. package/dist/shared/git-url.d.ts.map +1 -1
  182. package/dist/shared/git-url.js +0 -9
  183. package/dist/shared/git-url.js.map +1 -1
  184. package/dist/shared/types.d.ts +31 -12
  185. package/dist/shared/types.d.ts.map +1 -1
  186. package/dist/shared/types.js +14 -0
  187. package/dist/shared/types.js.map +1 -1
  188. package/dist/skill/registry.d.ts.map +1 -1
  189. package/dist/skill/registry.js +0 -5
  190. package/dist/skill/registry.js.map +1 -1
  191. package/dist/skills/baxian-signals/SKILL.md +0 -6
  192. package/dist/state/agent-store.d.ts.map +1 -1
  193. package/dist/state/agent-store.js +0 -6
  194. package/dist/state/agent-store.js.map +1 -1
  195. package/dist/state/error-record-store.d.ts.map +1 -1
  196. package/dist/state/error-record-store.js +0 -5
  197. package/dist/state/error-record-store.js.map +1 -1
  198. package/dist/state/init.d.ts.map +1 -1
  199. package/dist/state/init.js +0 -1
  200. package/dist/state/init.js.map +1 -1
  201. package/dist/state/lock.d.ts.map +1 -1
  202. package/dist/state/lock.js +0 -1
  203. package/dist/state/lock.js.map +1 -1
  204. package/dist/state/snapshot.d.ts +0 -1
  205. package/dist/state/snapshot.d.ts.map +1 -1
  206. package/dist/state/snapshot.js +0 -13
  207. package/dist/state/snapshot.js.map +1 -1
  208. package/dist/state/task-store.d.ts.map +1 -1
  209. package/dist/state/task-store.js +96 -11
  210. package/dist/state/task-store.js.map +1 -1
  211. package/dist/terminal/attach.d.ts.map +1 -1
  212. package/dist/terminal/attach.js +0 -4
  213. package/dist/terminal/attach.js.map +1 -1
  214. package/dist/terminal/stream-ws.js +0 -5
  215. package/dist/terminal/stream-ws.js.map +1 -1
  216. package/dist/web/assets/index-CzVzThuT.js +15 -0
  217. package/dist/web/assets/{index-BS2adAfI.css → index-Rmp4wT_5.css} +1 -1
  218. package/dist/web/index.html +2 -2
  219. package/package.json +1 -1
  220. package/dist/agent/root-agent-runtime.d.ts +0 -80
  221. package/dist/agent/root-agent-runtime.d.ts.map +0 -1
  222. package/dist/agent/root-agent-runtime.js +0 -637
  223. package/dist/agent/root-agent-runtime.js.map +0 -1
  224. package/dist/agent/root-recovery-coordinator.d.ts +0 -124
  225. package/dist/agent/root-recovery-coordinator.d.ts.map +0 -1
  226. package/dist/agent/root-recovery-coordinator.js +0 -1557
  227. package/dist/agent/root-recovery-coordinator.js.map +0 -1
  228. package/dist/state/root-recovery-store.d.ts +0 -107
  229. package/dist/state/root-recovery-store.d.ts.map +0 -1
  230. package/dist/state/root-recovery-store.js +0 -400
  231. package/dist/state/root-recovery-store.js.map +0 -1
  232. package/dist/web/assets/index-BKiM0MSM.js +0 -15
@@ -1,637 +0,0 @@
1
- import { randomBytes } from 'node:crypto';
2
- import { homedir } from 'node:os';
3
- import { posix } from 'node:path';
4
- import { ROOT_AGENT_ID } from '../shared/index.js';
5
- import { scanRootDoneSignals } from './phase-signal.js';
6
- import { visibleText } from './vt-visible-text.js';
7
- import { ancestorSymlinkGuard, canonicalSelfGuard, isUnder, moveFileIntoPlace, stageFileGuarded, } from './repo-store.js';
8
- import { ExecOutcomeUnknownError, execOutcomeUnknown, isTransientNetworkFailure, } from './net-exec.js';
9
- import { createRunner, mayShareHostAccount, resolveAgentHost, shellQuote, } from './runner.js';
10
- import { launchCommandIn } from './manager.js';
11
- import { TmuxManager, PaneGoneError, TmuxOutcomeUnknownError, } from './tmux.js';
12
- export class RootPromptNotSubmittedError extends Error {
13
- constructor(message) {
14
- super(message);
15
- this.name = 'RootPromptNotSubmittedError';
16
- }
17
- }
18
- export class RootAgentResponseInvalidError extends Error {
19
- constructor(message) {
20
- super(message);
21
- this.name = 'RootAgentResponseInvalidError';
22
- }
23
- }
24
- export class RootAgentTerminationError extends Error {
25
- hostConnectionUnknown;
26
- constructor(message, hostConnectionUnknown) {
27
- super(message);
28
- this.hostConnectionUnknown = hostConnectionUnknown;
29
- this.name = 'RootAgentTerminationError';
30
- }
31
- }
32
- class RootMailboxPathError extends Error {
33
- constructor(message) {
34
- super(message);
35
- this.name = 'RootMailboxPathError';
36
- }
37
- }
38
- const ROOT_RUNTIME_DIR = '.baxian/root-agent';
39
- const ROOT_INBOX_DIR = `${ROOT_RUNTIME_DIR}/inbox`;
40
- export const ROOT_OUTBOX_DIR = `${ROOT_RUNTIME_DIR}/outbox`;
41
- const MAX_REQUEST_BYTES = 128 * 1024;
42
- const MAX_RESPONSE_BYTES = 16 * 1024;
43
- const MAILBOX_READ_TIMEOUT_MS = 10_000;
44
- const RESPONSE_ABSENT = 'BX_ROOT_RESPONSE_ABSENT';
45
- const MATCH_BUFFER_CHARS = 1024;
46
- const ROOT_LAUNCH_COMMAND_OPTION = '@baxian-launch-command';
47
- export class RootAgentRuntime {
48
- config;
49
- hosts;
50
- agents;
51
- paneStreamerManager;
52
- runnerFactory;
53
- tmuxFactory;
54
- physicalWorkdir;
55
- physicalHome;
56
- ensureChain = null;
57
- unsubscribe;
58
- onSignal;
59
- signalBuffer = '';
60
- seenSignals = new Set();
61
- constructor(deps) {
62
- this.config = deps.config;
63
- this.hosts = deps.hosts;
64
- this.agents = deps.agents ?? (() => []);
65
- this.paneStreamerManager = deps.paneStreamerManager;
66
- this.runnerFactory = deps.runnerFactory;
67
- this.tmuxFactory = deps.tmuxFactory ?? (runner => new TmuxManager(runner));
68
- }
69
- async start(onSignal) {
70
- this.onSignal = onSignal;
71
- await this.ensureSession();
72
- if (this.unsubscribe)
73
- return;
74
- const streamer = this.paneStreamerManager.ensure(this.runtimeConfig());
75
- const subscription = await streamer.subscribeAtomic({
76
- onVisible: visible => this.consumePaneData(visible),
77
- onSessionGone: () => {
78
- this.unsubscribe = undefined;
79
- this.signalBuffer = '';
80
- },
81
- });
82
- this.unsubscribe = subscription.unsubscribe;
83
- this.consumePaneData(visibleText(subscription.snapshot.data));
84
- }
85
- async writeRequest(record, body) {
86
- const bytes = Buffer.byteLength(body, 'utf8');
87
- if (bytes > MAX_REQUEST_BYTES) {
88
- throw new Error(`root recovery request ${record.id} exceeds ${MAX_REQUEST_BYTES} bytes`);
89
- }
90
- const { runner, workdir } = await this.mailboxContext();
91
- const final = `${workdir}/${ROOT_INBOX_DIR}/${record.id}.json`;
92
- const tmp = `${final}.tmp-${randomBytes(8).toString('hex')}`;
93
- await stageFileGuarded(runner, workdir, tmp, body, { mode: 0o600 });
94
- await moveFileIntoPlace(runner, tmp, final, { guardRoot: workdir });
95
- }
96
- async notify(record) {
97
- let tmux;
98
- let pane;
99
- let baseline;
100
- try {
101
- await this.start(this.onSignal ?? (() => undefined));
102
- const runner = this.createRunner();
103
- tmux = this.tmuxFactory(runner);
104
- pane = await this.resolveOwnedPane(tmux);
105
- await tmux.waitReplReady(pane, this.config.runtime, {
106
- timeoutMs: 30_000,
107
- scrollback: 0,
108
- titleIdleFastPath: true,
109
- });
110
- baseline = await tmux.capturePaneSnapshot(pane);
111
- }
112
- catch (err) {
113
- throw new RootPromptNotSubmittedError(`root prompt was not submitted: ${err instanceof Error ? err.message : String(err)}`);
114
- }
115
- try {
116
- await tmux.injectPrompt(pane, this.requestPrompt(record), ROOT_AGENT_ID);
117
- await tmux.sendEnter(pane);
118
- }
119
- catch (err) {
120
- if (err instanceof PaneGoneError) {
121
- throw new RootPromptNotSubmittedError(`root prompt was not submitted: ${err.message}`);
122
- }
123
- throw err;
124
- }
125
- await tmux.waitSubmitAck(pane, baseline, this.config.runtime, {
126
- timeoutMs: 30_000,
127
- acceptComposerChange: true,
128
- resend: () => tmux.sendEnter(pane),
129
- });
130
- }
131
- async readResponse(record) {
132
- let context;
133
- try {
134
- context = await this.mailboxContext();
135
- }
136
- catch (err) {
137
- if (err instanceof RootMailboxPathError) {
138
- throw new RootAgentResponseInvalidError(err.message);
139
- }
140
- throw err;
141
- }
142
- const { runner, workdir } = context;
143
- const target = `${workdir}/${ROOT_OUTBOX_DIR}/${record.id}.json`;
144
- const guard = ancestorSymlinkGuard(workdir, target);
145
- const command = `${guard} || exit 11; ` +
146
- `if [ -L ${shellQuote(target)} ]; then exit 9; fi; ` +
147
- `if [ ! -e ${shellQuote(target)} ]; then printf '%s' ${shellQuote(RESPONSE_ABSENT)}; exit 0; fi; ` +
148
- `[ -f ${shellQuote(target)} ] && [ ! -L ${shellQuote(target)} ] || exit 9; ` +
149
- `oversized=$(find ${shellQuote(target)} -prune -size +${MAX_RESPONSE_BYTES}c -print) || exit 9; ` +
150
- `[ -z "$oversized" ] || exit 10; ` +
151
- `cat ${shellQuote(target)}`;
152
- const result = await runner.exec(`sh -c ${shellQuote(command)}`, {
153
- timeout: MAILBOX_READ_TIMEOUT_MS,
154
- maxBuffer: MAX_RESPONSE_BYTES + 1,
155
- });
156
- if (result.exitCode !== 0 && execOutcomeUnknown(result)) {
157
- throw new ExecOutcomeUnknownError(`root response read outcome unknown for ${record.id}: ${result.stderr.trim()}`);
158
- }
159
- if (result.exitCode === 9 || result.exitCode === 11) {
160
- throw new RootAgentResponseInvalidError(`root response path is unsafe or not a regular file: ${target}`);
161
- }
162
- if (result.exitCode === 10) {
163
- throw new RootAgentResponseInvalidError(`root response ${target} exceeds ${MAX_RESPONSE_BYTES} bytes`);
164
- }
165
- if (result.exitCode !== 0) {
166
- throw new Error(`root response read failed for ${record.id} (exit ${result.exitCode}): ${result.stderr.trim()}`);
167
- }
168
- if (Buffer.byteLength(result.stdout, 'utf8') > MAX_RESPONSE_BYTES) {
169
- throw new RootAgentResponseInvalidError(`root response ${target} exceeds ${MAX_RESPONSE_BYTES} bytes`);
170
- }
171
- if (result.stdout === RESPONSE_ABSENT)
172
- return null;
173
- try {
174
- return JSON.parse(result.stdout);
175
- }
176
- catch (err) {
177
- throw new RootAgentResponseInvalidError(`root response ${target} is invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
178
- }
179
- }
180
- async cleanup(record) {
181
- const { runner, workdir } = await this.mailboxContext();
182
- const targets = [
183
- `${workdir}/${ROOT_INBOX_DIR}/${record.id}.json`,
184
- `${workdir}/${ROOT_OUTBOX_DIR}/${record.id}.json`,
185
- ];
186
- const failures = [];
187
- for (const target of targets) {
188
- const command = `${ancestorSymlinkGuard(workdir, target)} && ` +
189
- `if [ -e ${shellQuote(target)} ] || [ -L ${shellQuote(target)} ]; then ` +
190
- `[ -f ${shellQuote(target)} ] && [ ! -L ${shellQuote(target)} ] || exit 9; ` +
191
- `rm -f -- ${shellQuote(target)} || exit 10; fi`;
192
- let result;
193
- try {
194
- result = await runner.exec(`sh -c ${shellQuote(command)}`);
195
- }
196
- catch (err) {
197
- const failure = `root mailbox cleanup probe failed for ${target}: ${err instanceof Error ? err.message : String(err)}`;
198
- throw new Error([...failures, failure].join('; '));
199
- }
200
- if (execOutcomeUnknown(result)) {
201
- const failure = `root mailbox cleanup outcome unknown for ${target}: ${result.stderr.trim()}`;
202
- throw new Error([...failures, failure].join('; '));
203
- }
204
- else if (result.exitCode === 9) {
205
- failures.push(`root mailbox cleanup refused unsafe target ${target} (exit 9)`);
206
- }
207
- else if (result.exitCode !== 0) {
208
- failures.push(`root mailbox cleanup failed for ${target} (exit ${result.exitCode}): ${result.stderr.trim()}`);
209
- }
210
- }
211
- if (failures.length > 0)
212
- throw new Error(failures.join('; '));
213
- }
214
- async isLive() {
215
- const snapshot = await this.tmuxFactory(this.createRunner()).getSessionSnapshot(ROOT_AGENT_ID);
216
- return snapshot?.claim === ROOT_AGENT_ID;
217
- }
218
- async invalidateStreamer() {
219
- try {
220
- this.unsubscribe?.();
221
- }
222
- finally {
223
- this.unsubscribe = undefined;
224
- this.signalBuffer = '';
225
- this.physicalWorkdir = undefined;
226
- this.physicalHome = undefined;
227
- await this.paneStreamerManager.destroy(ROOT_AGENT_ID);
228
- }
229
- }
230
- async terminate() {
231
- const failures = [];
232
- try {
233
- await this.invalidateStreamer();
234
- }
235
- catch (err) {
236
- failures.push({
237
- detail: `streamer cleanup failed: ${err instanceof Error ? err.message : String(err)}`,
238
- hostConnectionUnknown: false,
239
- });
240
- }
241
- try {
242
- const tmux = this.tmuxFactory(this.createRunner());
243
- const snapshot = await tmux.getSessionSnapshot(ROOT_AGENT_ID);
244
- if (snapshot && snapshot.claim !== ROOT_AGENT_ID) {
245
- failures.push({
246
- detail: `tmux session "${ROOT_AGENT_ID}" is not owned by baxian (claim=${snapshot.claim})`,
247
- hostConnectionUnknown: false,
248
- });
249
- }
250
- else if (snapshot) {
251
- const outcome = await tmux.killSessionRef(snapshot.ref, { kind: 'equals', claim: ROOT_AGENT_ID });
252
- if (outcome === 'refused') {
253
- failures.push({
254
- detail: 'tmux ownership changed while stopping root agent; session was kept',
255
- hostConnectionUnknown: false,
256
- });
257
- }
258
- else if (outcome === 'killed') {
259
- console.log(`[root-agent] stopped owned tmux session ${snapshot.ref.sessionId}`);
260
- }
261
- }
262
- }
263
- catch (err) {
264
- const detail = err instanceof Error ? err.message : String(err);
265
- failures.push({
266
- detail: `tmux stop failed: ${detail}`,
267
- hostConnectionUnknown: err instanceof TmuxOutcomeUnknownError
268
- || isTransientNetworkFailure(detail),
269
- });
270
- }
271
- if (failures.length > 0) {
272
- throw new RootAgentTerminationError(failures.map(failure => failure.detail).join('; '), failures.every(failure => failure.hostConnectionUnknown));
273
- }
274
- }
275
- async stop() {
276
- await this.invalidateStreamer();
277
- }
278
- ensureSession() {
279
- if (this.ensureChain)
280
- return this.ensureChain;
281
- const pending = this.ensureSessionOnce();
282
- this.ensureChain = pending;
283
- void pending.finally(() => {
284
- if (this.ensureChain === pending)
285
- this.ensureChain = null;
286
- }).catch(() => undefined);
287
- return pending;
288
- }
289
- async ensureSessionOnce() {
290
- const { runner, workdir } = await this.mailboxContext();
291
- const tmux = this.tmuxFactory(runner);
292
- const snapshot = await tmux.getSessionSnapshot(ROOT_AGENT_ID);
293
- if (!snapshot)
294
- return this.createSession(tmux, workdir);
295
- if (snapshot.claim !== ROOT_AGENT_ID) {
296
- throw new Error(`tmux session "${ROOT_AGENT_ID}" exists but is not owned by baxian ` +
297
- `(claim=${snapshot.claim ?? 'empty'}); choose another host or remove it manually`);
298
- }
299
- const pane = await tmux.getSinglePaneByRef(snapshot.ref, ROOT_AGENT_ID);
300
- const currentPath = await tmux.getPaneCurrentPath(pane);
301
- if (!(await this.samePhysicalPath(runner, currentPath, workdir))) {
302
- throw new Error(`root agent pane Workdir changed (${currentPath} != ${workdir}); baxian will not replace the live session`);
303
- }
304
- const state = await tmux.classifyPaneForAdopt(pane, this.config.runtime);
305
- if (state.kind === 'other') {
306
- throw new Error(`root agent pane foreground "${state.paneCurrentCommand}" is neither the configured runtime nor a shell`);
307
- }
308
- if (state.kind === 'startup-dialog') {
309
- throw new Error('root agent runtime is blocked on a startup dialog; attach and dismiss it');
310
- }
311
- const launchCommand = launchCommandIn(workdir, this.runtimeConfig());
312
- if (state.kind !== 'shell') {
313
- const recorded = await tmux.getSessionOptionByRef(snapshot.ref, ROOT_AGENT_ID, ROOT_LAUNCH_COMMAND_OPTION);
314
- if (recorded === null) {
315
- throw new Error('root agent session predates launch-command tracking; stop root-agent once to relaunch it with verified configuration');
316
- }
317
- if (recorded !== launchCommand) {
318
- throw new Error('root agent launch configuration differs from the running session; stop root-agent before applying model or permission changes');
319
- }
320
- }
321
- await this.setSessionOptions(tmux, snapshot.ref, [
322
- ['@baxian-runtime', this.config.runtime],
323
- ['@baxian-workdir', workdir],
324
- [ROOT_LAUNCH_COMMAND_OPTION, launchCommand],
325
- ]);
326
- if (state.kind === 'live-runtime')
327
- return pane;
328
- if (state.kind === 'trust-dialog') {
329
- await tmux.handleTrustDialog(pane, this.config.runtime, { timeoutMs: 10_000 });
330
- }
331
- else {
332
- await tmux.sendKeysLiteral(pane, launchCommand);
333
- await tmux.sendEnter(pane);
334
- }
335
- await tmux.waitReplReady(pane, this.config.runtime, { timeoutMs: 30_000, scrollback: 0 });
336
- return pane;
337
- }
338
- async createSession(tmux, workdir) {
339
- let ref;
340
- try {
341
- ref = await tmux.createSession(ROOT_AGENT_ID, workdir);
342
- const launchCommand = launchCommandIn(workdir, this.runtimeConfig());
343
- const applied = await tmux.setSessionOptionsIfAlive(ref, [
344
- ['@baxian-agent-id', ROOT_AGENT_ID],
345
- ['@baxian-runtime', this.config.runtime],
346
- ['@baxian-workdir', workdir],
347
- [ROOT_LAUNCH_COMMAND_OPTION, launchCommand],
348
- ['allow-passthrough', 'on'],
349
- ['set-titles', 'on'],
350
- ['window-size', 'latest'],
351
- ['status-right', ''],
352
- ], { expectedClaim: '' });
353
- if (applied === 'gone')
354
- throw new Error('root agent tmux session vanished during initialization');
355
- await tmux.setServerOption('extended-keys', 'on');
356
- await tmux.appendServerOptionIfMissing('terminal-features', 'xterm*:extkeys');
357
- const pane = await tmux.getSinglePaneByRef(ref, ROOT_AGENT_ID);
358
- await tmux.sendKeysLiteral(pane, launchCommand);
359
- await tmux.sendEnter(pane);
360
- await tmux.handleTrustDialog(pane, this.config.runtime, { timeoutMs: 10_000 });
361
- await tmux.waitReplReady(pane, this.config.runtime, { timeoutMs: 30_000, scrollback: 0 });
362
- return pane;
363
- }
364
- catch (err) {
365
- if (ref) {
366
- try {
367
- const outcome = await tmux.killSessionRef(ref, { kind: 'emptyOr', claim: ROOT_AGENT_ID });
368
- if (outcome === 'killed') {
369
- console.warn(`[root-agent] rolled back newly created tmux session after startup failure`);
370
- }
371
- else if (outcome === 'refused') {
372
- console.warn('[root-agent] startup rollback refused because tmux ownership changed; session kept');
373
- }
374
- }
375
- catch (cleanupErr) {
376
- console.warn('[root-agent] startup rollback could not confirm cleanup:', cleanupErr);
377
- }
378
- }
379
- throw err;
380
- }
381
- }
382
- async resolveOwnedPane(tmux) {
383
- const snapshot = await tmux.getSessionSnapshot(ROOT_AGENT_ID);
384
- if (!snapshot || snapshot.claim !== ROOT_AGENT_ID) {
385
- throw new Error(`root agent tmux session is absent or ownership changed`);
386
- }
387
- return tmux.getSinglePaneByRef(snapshot.ref, ROOT_AGENT_ID);
388
- }
389
- async mailboxContext() {
390
- const runner = this.createRunner();
391
- const workdir = await this.resolveWorkdir(runner);
392
- await this.ensureMailboxDirs(runner, workdir);
393
- return { runner, workdir };
394
- }
395
- async resolveWorkdir(runner) {
396
- if (this.physicalWorkdir) {
397
- await this.assertWorkdirIsolation(runner, this.physicalWorkdir);
398
- return this.physicalWorkdir;
399
- }
400
- const result = await runner.exec(`cd -- ${shellQuote(this.config.workdir)} 2>/dev/null && pwd -P`);
401
- if (execOutcomeUnknown(result)) {
402
- throw new ExecOutcomeUnknownError(`root Workdir probe outcome unknown: ${result.stderr.trim()}`);
403
- }
404
- const physical = result.stdout.trim().replace(/\/+$/, '') || '/';
405
- if (result.exitCode !== 0 || !physical.startsWith('/')) {
406
- throw new Error(`root.workdir ${this.config.workdir} does not exist or is not accessible; create it explicitly`);
407
- }
408
- await this.assertWorkdirIsolation(runner, physical);
409
- this.physicalWorkdir = physical;
410
- return physical;
411
- }
412
- async assertWorkdirIsolation(runner, rootWorkdir) {
413
- const rootHost = resolveAgentHost(this.hosts(), this.config.host);
414
- const peerAgents = this.agents().filter(agent => {
415
- const agentHost = resolveAgentHost(this.hosts(), agent.host);
416
- return mayShareHostAccount(this.config.mode, rootHost, agent.mode, agentHost);
417
- });
418
- for (const agent of peerAgents) {
419
- if (agent.yolo !== false) {
420
- throw new RootMailboxPathError(`root mailbox cannot share an OS account with yolo agent ${agent.id}; ` +
421
- 'configure yolo: false or use a different explicit SSH user or hostname');
422
- }
423
- const configuredWorkdir = agent.workdir;
424
- const workdir = configuredWorkdir
425
- ?? `${await this.resolveDefaultAgentHome(runner)}/.baxian/agents/${agent.id}/repo`;
426
- const physicalWorkdir = await this.resolvePeerPhysicalPath(runner, agent, 'Workdir', workdir);
427
- this.assertPeerPathIsolated(agent, 'Workdir', workdir, physicalWorkdir, rootWorkdir);
428
- for (const addDir of agent.addDirs ?? []) {
429
- const path = posix.isAbsolute(addDir)
430
- ? addDir
431
- : posix.resolve(physicalWorkdir, addDir);
432
- const physical = await this.resolvePeerPhysicalPath(runner, agent, 'addDir', path);
433
- this.assertPeerPathIsolated(agent, 'addDir', path, physical, rootWorkdir);
434
- }
435
- }
436
- }
437
- async resolvePeerPhysicalPath(runner, agent, kind, path) {
438
- const result = await runner.exec(`cd -- ${shellQuote(path)} 2>/dev/null && pwd -P`);
439
- if (execOutcomeUnknown(result)) {
440
- throw new ExecOutcomeUnknownError(`agent ${agent.id} ${kind} probe outcome unknown while validating root.workdir: ${result.stderr.trim()}`);
441
- }
442
- if (result.exitCode !== 0) {
443
- return this.resolveProspectivePeerPath(runner, agent, kind, path);
444
- }
445
- const physical = result.stdout.trim().replace(/\/+$/, '') || '/';
446
- if (!physical.startsWith('/')) {
447
- throw new Error(`cannot resolve agent ${agent.id} ${kind} ${path} while validating root.workdir`);
448
- }
449
- return physical;
450
- }
451
- async resolveProspectivePeerPath(runner, agent, kind, path) {
452
- const command = [
453
- `bx_path=${shellQuote(posix.normalize(path))}`,
454
- 'bx_suffix=\'\'',
455
- 'while [ ! -e "$bx_path" ] && [ ! -L "$bx_path" ]; do ' +
456
- '[ "$bx_path" != / ] || exit 12; ' +
457
- 'bx_name=${bx_path##*/}; ' +
458
- 'bx_suffix="/$bx_name$bx_suffix"; ' +
459
- 'bx_path=${bx_path%/*}; ' +
460
- '[ -n "$bx_path" ] || bx_path=/; ' +
461
- 'done',
462
- 'cd -- "$bx_path" 2>/dev/null',
463
- 'bx_prefix=$(pwd -P)',
464
- 'case "$bx_prefix" in /) bx_prefix=\'\';; esac',
465
- 'printf \'%s%s\' "$bx_prefix" "$bx_suffix"',
466
- ].join(' && ');
467
- const result = await runner.exec(command);
468
- if (execOutcomeUnknown(result)) {
469
- throw new ExecOutcomeUnknownError(`agent ${agent.id} missing ${kind} probe outcome unknown while validating root.workdir: ` +
470
- result.stderr.trim());
471
- }
472
- const physical = result.stdout.trim().replace(/\/+$/, '') || '/';
473
- if (result.exitCode !== 0 || !physical.startsWith('/')) {
474
- throw new Error(`agent ${agent.id} ${kind} ${path} does not exist and its future physical path ` +
475
- 'cannot be resolved safely while validating root.workdir');
476
- }
477
- return physical;
478
- }
479
- assertPeerPathIsolated(agent, kind, path, physical, rootWorkdir) {
480
- if (!physicalPathsOverlap(physical, rootWorkdir))
481
- return;
482
- const relation = physical === rootWorkdir
483
- ? `resolve to the same physical directory ${rootWorkdir}`
484
- : `overlap as physical directories ${physical} and ${rootWorkdir}`;
485
- throw new RootMailboxPathError(`root.workdir ${this.config.workdir} and agent ${agent.id} ${kind} ${path} ${relation}`);
486
- }
487
- async resolveDefaultAgentHome(runner) {
488
- if (this.physicalHome)
489
- return this.physicalHome;
490
- const target = this.config.mode === 'local' ? shellQuote(homedir()) : '"$HOME"';
491
- const result = await runner.exec(`cd -- ${target} 2>/dev/null && pwd -P`);
492
- if (execOutcomeUnknown(result)) {
493
- throw new ExecOutcomeUnknownError(`default agent home probe outcome unknown: ${result.stderr.trim()}`);
494
- }
495
- const physical = result.stdout.trim().replace(/\/+$/, '') || '/';
496
- if (result.exitCode !== 0 || !physical.startsWith('/')) {
497
- throw new Error(`cannot resolve the default agent home while validating root.workdir`);
498
- }
499
- this.physicalHome = physical;
500
- return physical;
501
- }
502
- async ensureMailboxDirs(runner, workdir) {
503
- const dirs = [
504
- `${workdir}/.baxian`,
505
- `${workdir}/${ROOT_RUNTIME_DIR}`,
506
- `${workdir}/${ROOT_INBOX_DIR}`,
507
- `${workdir}/${ROOT_OUTBOX_DIR}`,
508
- ];
509
- const ancestors = ['/'];
510
- let ancestor = '';
511
- for (const part of workdir.split('/').filter(Boolean)) {
512
- ancestor += `/${part}`;
513
- ancestors.push(ancestor);
514
- }
515
- const statFunction = 'bx_stat() { ' +
516
- 'stat -c \'%u %a\' -- "$1" 2>/dev/null || stat -f \'%u %Mp%03Lp\' "$1" 2>/dev/null; ' +
517
- '}';
518
- const trustChain = `for bx_path in ${ancestors.map(shellQuote).join(' ')}; do ` +
519
- '[ -d "$bx_path" ] && [ ! -L "$bx_path" ] || { ' +
520
- 'printf \'unsafe root Workdir ancestor: %s\\n\' "$bx_path" >&2; exit 11; }; ' +
521
- 'bx_meta=$(bx_stat "$bx_path") || { ' +
522
- 'printf \'cannot inspect root Workdir ancestor: %s\\n\' "$bx_path" >&2; exit 11; }; ' +
523
- 'bx_owner=${bx_meta%% *}; bx_mode=${bx_meta#* }; ' +
524
- 'case "$bx_owner" in \'\'|*[!0-9]*) exit 11;; esac; ' +
525
- 'case "$bx_mode" in \'\'|*[!0-7]*) exit 11;; esac; ' +
526
- '[ "$bx_owner" = "$bx_uid" ] || [ "$bx_owner" = 0 ] || { ' +
527
- 'printf \'untrusted owner for root Workdir ancestor: %s\\n\' "$bx_path" >&2; exit 11; }; ' +
528
- 'bx_permissions=$((0$bx_mode)); ' +
529
- 'if [ $((bx_permissions & 0022)) -ne 0 ] && [ $((bx_permissions & 01000)) -eq 0 ]; then ' +
530
- 'printf \'root Workdir ancestor is writable by other users: %s\\n\' "$bx_path" >&2; exit 11; fi; ' +
531
- 'done';
532
- const preflight = dirs.map(path => `${ancestorSymlinkGuard(workdir, path)} && ` +
533
- `{ [ ! -e ${shellQuote(path)} ] || { [ -d ${shellQuote(path)} ] && [ ! -L ${shellQuote(path)} ]; }; }`);
534
- const verify = dirs.map(path => `[ -d ${shellQuote(path)} ] && [ ! -L ${shellQuote(path)} ]`);
535
- const verifyOwnership = `for bx_path in ${dirs.map(shellQuote).join(' ')}; do ` +
536
- 'bx_meta=$(bx_stat "$bx_path") || exit 11; ' +
537
- 'bx_owner=${bx_meta%% *}; bx_mode=${bx_meta#* }; ' +
538
- 'case "$bx_owner" in \'\'|*[!0-9]*) exit 11;; esac; ' +
539
- 'case "$bx_mode" in \'\'|*[!0-7]*) exit 11;; esac; ' +
540
- 'bx_permissions=$((0$bx_mode)); ' +
541
- '[ "$bx_owner" = "$bx_uid" ] && [ "$bx_permissions" -eq $((0700)) ] || { ' +
542
- 'printf \'root mailbox directory is not private and owner-controlled: %s\\n\' "$bx_path" >&2; exit 11; }; ' +
543
- 'done';
544
- const command = [
545
- `${canonicalSelfGuard(workdir)} || exit 11`,
546
- statFunction,
547
- 'bx_uid=$(id -u) || exit 11',
548
- trustChain,
549
- `${preflight.join(' && ')} || exit 11`,
550
- 'umask 077',
551
- `mkdir -p ${dirs.map(shellQuote).join(' ')} || exit 10`,
552
- `chmod 700 ${dirs.map(shellQuote).join(' ')} || exit 10`,
553
- `${verify.join(' && ')} || exit 11`,
554
- verifyOwnership,
555
- ].join('; ');
556
- const result = await runner.exec(`sh -c ${shellQuote(command)}`);
557
- if (execOutcomeUnknown(result)) {
558
- throw new ExecOutcomeUnknownError(`root mailbox directory creation outcome unknown: ${result.stderr.trim()}`);
559
- }
560
- if (result.exitCode === 11) {
561
- throw new RootMailboxPathError(`root mailbox requires an owner-controlled canonical Workdir and real private directories under ${workdir}: ` +
562
- `${result.stderr.trim() || `exit ${result.exitCode}`}`);
563
- }
564
- if (result.exitCode !== 0) {
565
- throw new Error(`root mailbox directory creation failed under ${workdir}: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
566
- }
567
- }
568
- async samePhysicalPath(runner, left, right) {
569
- if (left.replace(/\/+$/, '') === right.replace(/\/+$/, ''))
570
- return true;
571
- const result = await runner.exec(`cd -- ${shellQuote(left)} 2>/dev/null && pwd -P`);
572
- if (execOutcomeUnknown(result)) {
573
- throw new ExecOutcomeUnknownError(`root pane path probe outcome unknown: ${result.stderr.trim()}`);
574
- }
575
- return result.exitCode === 0 && result.stdout.trim().replace(/\/+$/, '') === right.replace(/\/+$/, '');
576
- }
577
- async setSessionOptions(tmux, ref, entries) {
578
- const outcome = await tmux.setSessionOptionsIfAlive(ref, entries, { expectedClaim: ROOT_AGENT_ID });
579
- if (outcome === 'gone')
580
- throw new Error('root agent tmux session vanished while updating options');
581
- }
582
- runtimeConfig() {
583
- return {
584
- id: ROOT_AGENT_ID,
585
- runtime: this.config.runtime,
586
- mode: this.config.mode,
587
- ...(this.config.host !== undefined ? { host: this.config.host } : {}),
588
- workdir: this.config.workdir,
589
- yolo: this.config.yolo,
590
- ...(this.config.model !== undefined ? { model: this.config.model } : {}),
591
- };
592
- }
593
- createRunner() {
594
- return this.runnerFactory?.()
595
- ?? createRunner(this.config.mode, resolveAgentHost(this.hosts(), this.config.host));
596
- }
597
- requestPrompt(record) {
598
- const workdir = this.physicalWorkdir ?? this.config.workdir;
599
- const requestPath = `${workdir}/${ROOT_INBOX_DIR}/${record.id}.json`;
600
- const responsePath = `${workdir}/${ROOT_OUTBOX_DIR}/${record.id}.json`;
601
- return [
602
- '你是 Baxian root agent。Server 的正常 task 状态机仍是主流程;你只处理这一次卡住恢复请求。',
603
- '禁止调用 Baxian MCP、REST API 或直接修改 task 状态。只允许根据请求中的证据选择一个 allowedDecisions 动作。',
604
- `读取请求文件:${requestPath}`,
605
- '输出必须是严格 JSON:',
606
- `{"version":1,"requestId":"${record.id}","attemptToken":"${record.attemptToken}","decision":{"action":"<allowed-action>","reason":"..."}}`,
607
- `先写同目录唯一 tmp 文件,确认目标不是目录后再原子 mv 到:${responsePath}`,
608
- '写完后在助手回复中单独输出 [bx:root-done:<attemptToken>],把占位符替换为请求中的 attemptToken。不要用 shell echo/printf 发送 signal。',
609
- '一次只返回一个决策;不要执行该决策,Server 会校验并执行。',
610
- ].join('\n');
611
- }
612
- // `data` is visible text — decoded by PaneStreamer (live) or at the call site (snapshot).
613
- consumePaneData(data) {
614
- const combined = this.signalBuffer + data;
615
- for (const token of scanRootDoneSignals(combined)) {
616
- if (this.seenSignals.has(token))
617
- continue;
618
- this.seenSignals.add(token);
619
- if (this.seenSignals.size > 256) {
620
- const oldest = this.seenSignals.values().next().value;
621
- if (oldest !== undefined)
622
- this.seenSignals.delete(oldest);
623
- }
624
- try {
625
- this.onSignal?.(token);
626
- }
627
- catch (err) {
628
- console.warn('[root-agent] signal callback failed:', err);
629
- }
630
- }
631
- this.signalBuffer = combined.slice(-MATCH_BUFFER_CHARS);
632
- }
633
- }
634
- function physicalPathsOverlap(left, right) {
635
- return left === right || isUnder(left, right) || isUnder(right, left);
636
- }
637
- //# sourceMappingURL=root-agent-runtime.js.map