@mono-agent/agent-app 0.6.2 → 0.8.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 (211) hide show
  1. package/README.md +210 -19
  2. package/dist/adapter-send-proxy.d.ts +18 -0
  3. package/dist/adapter-send-proxy.d.ts.map +1 -0
  4. package/dist/adapter-send-proxy.js +229 -0
  5. package/dist/adapter-send-proxy.js.map +1 -0
  6. package/dist/adapter-send-tools-main.js +62 -4
  7. package/dist/adapter-send-tools-main.js.map +1 -1
  8. package/dist/adapter-send-tools.d.ts +33 -6
  9. package/dist/adapter-send-tools.d.ts.map +1 -1
  10. package/dist/adapter-send-tools.js +485 -87
  11. package/dist/adapter-send-tools.js.map +1 -1
  12. package/dist/app-config.d.ts.map +1 -1
  13. package/dist/app-config.js +8 -0
  14. package/dist/app-config.js.map +1 -1
  15. package/dist/app.d.ts +2 -0
  16. package/dist/app.d.ts.map +1 -1
  17. package/dist/app.js +528 -135
  18. package/dist/app.js.map +1 -1
  19. package/dist/broadcast-recorder.d.ts.map +1 -1
  20. package/dist/broadcast-recorder.js +24 -2
  21. package/dist/broadcast-recorder.js.map +1 -1
  22. package/dist/channels.d.ts.map +1 -1
  23. package/dist/channels.js +16 -6
  24. package/dist/channels.js.map +1 -1
  25. package/dist/cli.d.ts +76 -4
  26. package/dist/cli.d.ts.map +1 -1
  27. package/dist/cli.js +2102 -102
  28. package/dist/cli.js.map +1 -1
  29. package/dist/config-reference.d.ts.map +1 -1
  30. package/dist/config-reference.js +195 -6
  31. package/dist/config-reference.js.map +1 -1
  32. package/dist/configuration-proposal-main.d.ts +3 -0
  33. package/dist/configuration-proposal-main.d.ts.map +1 -0
  34. package/dist/configuration-proposal-main.js +13 -0
  35. package/dist/configuration-proposal-main.js.map +1 -0
  36. package/dist/configuration-proposal-tool.d.ts +26 -0
  37. package/dist/configuration-proposal-tool.d.ts.map +1 -0
  38. package/dist/configuration-proposal-tool.js +139 -0
  39. package/dist/configuration-proposal-tool.js.map +1 -0
  40. package/dist/configured-agent.d.ts +12 -0
  41. package/dist/configured-agent.d.ts.map +1 -1
  42. package/dist/configured-agent.js +150 -18
  43. package/dist/configured-agent.js.map +1 -1
  44. package/dist/consumer-contract.d.ts +3 -0
  45. package/dist/consumer-contract.d.ts.map +1 -1
  46. package/dist/consumer-contract.js +44 -1
  47. package/dist/consumer-contract.js.map +1 -1
  48. package/dist/doctor.d.ts +27 -5
  49. package/dist/doctor.d.ts.map +1 -1
  50. package/dist/doctor.js +1197 -101
  51. package/dist/doctor.js.map +1 -1
  52. package/dist/first-run-managed-memory.d.ts +39 -0
  53. package/dist/first-run-managed-memory.d.ts.map +1 -0
  54. package/dist/first-run-managed-memory.js +521 -0
  55. package/dist/first-run-managed-memory.js.map +1 -0
  56. package/dist/first-run-readiness.d.ts +111 -0
  57. package/dist/first-run-readiness.d.ts.map +1 -0
  58. package/dist/first-run-readiness.js +757 -0
  59. package/dist/first-run-readiness.js.map +1 -0
  60. package/dist/index.d.ts +8 -3
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +4 -1
  63. package/dist/index.js.map +1 -1
  64. package/dist/init.d.ts +88 -3
  65. package/dist/init.d.ts.map +1 -1
  66. package/dist/init.js +1212 -23
  67. package/dist/init.js.map +1 -1
  68. package/dist/interaction-bridge.d.ts +25 -1
  69. package/dist/interaction-bridge.d.ts.map +1 -1
  70. package/dist/interaction-bridge.js +281 -19
  71. package/dist/interaction-bridge.js.map +1 -1
  72. package/dist/local-configuration.d.ts +56 -0
  73. package/dist/local-configuration.d.ts.map +1 -0
  74. package/dist/local-configuration.js +914 -0
  75. package/dist/local-configuration.js.map +1 -0
  76. package/dist/memory-command.d.ts +4 -0
  77. package/dist/memory-command.d.ts.map +1 -1
  78. package/dist/memory-command.js +751 -27
  79. package/dist/memory-command.js.map +1 -1
  80. package/dist/memory-recall.d.ts +38 -10
  81. package/dist/memory-recall.d.ts.map +1 -1
  82. package/dist/memory-recall.js +82 -25
  83. package/dist/memory-recall.js.map +1 -1
  84. package/dist/memory-retrieval.d.ts +66 -0
  85. package/dist/memory-retrieval.d.ts.map +1 -0
  86. package/dist/memory-retrieval.js +332 -0
  87. package/dist/memory-retrieval.js.map +1 -0
  88. package/dist/modules/base.d.ts +6 -5
  89. package/dist/modules/base.d.ts.map +1 -1
  90. package/dist/modules/base.js +11 -7
  91. package/dist/modules/base.js.map +1 -1
  92. package/dist/modules/catalog.d.ts.map +1 -1
  93. package/dist/modules/catalog.js +54 -20
  94. package/dist/modules/catalog.js.map +1 -1
  95. package/dist/modules/index.d.ts +2 -2
  96. package/dist/modules/index.d.ts.map +1 -1
  97. package/dist/modules/index.js +1 -1
  98. package/dist/modules/index.js.map +1 -1
  99. package/dist/modules/known-tools.d.ts +3 -0
  100. package/dist/modules/known-tools.d.ts.map +1 -1
  101. package/dist/modules/known-tools.js +11 -1
  102. package/dist/modules/known-tools.js.map +1 -1
  103. package/dist/modules/types.d.ts +20 -6
  104. package/dist/modules/types.d.ts.map +1 -1
  105. package/dist/modules/types.js.map +1 -1
  106. package/dist/package-version.d.ts +3 -0
  107. package/dist/package-version.d.ts.map +1 -0
  108. package/dist/package-version.js +14 -0
  109. package/dist/package-version.js.map +1 -0
  110. package/dist/pi-auth-store-inspection.d.ts +21 -0
  111. package/dist/pi-auth-store-inspection.d.ts.map +1 -0
  112. package/dist/pi-auth-store-inspection.js +120 -0
  113. package/dist/pi-auth-store-inspection.js.map +1 -0
  114. package/dist/pi-oauth-login-main.d.ts +3 -0
  115. package/dist/pi-oauth-login-main.d.ts.map +1 -0
  116. package/dist/pi-oauth-login-main.js +14 -0
  117. package/dist/pi-oauth-login-main.js.map +1 -0
  118. package/dist/pi-oauth-login.d.ts +22 -0
  119. package/dist/pi-oauth-login.d.ts.map +1 -0
  120. package/dist/pi-oauth-login.js +79 -0
  121. package/dist/pi-oauth-login.js.map +1 -0
  122. package/dist/project-skills.d.ts +40 -0
  123. package/dist/project-skills.d.ts.map +1 -0
  124. package/dist/project-skills.js +573 -0
  125. package/dist/project-skills.js.map +1 -0
  126. package/dist/provider-setup.d.ts +117 -2
  127. package/dist/provider-setup.d.ts.map +1 -1
  128. package/dist/provider-setup.js +1505 -49
  129. package/dist/provider-setup.js.map +1 -1
  130. package/dist/readiness-probe-worker.d.ts +12 -0
  131. package/dist/readiness-probe-worker.d.ts.map +1 -0
  132. package/dist/readiness-probe-worker.js +276 -0
  133. package/dist/readiness-probe-worker.js.map +1 -0
  134. package/dist/readiness-probe.d.ts +100 -0
  135. package/dist/readiness-probe.d.ts.map +1 -0
  136. package/dist/readiness-probe.js +780 -0
  137. package/dist/readiness-probe.js.map +1 -0
  138. package/dist/request-model-override.d.ts +43 -1
  139. package/dist/request-model-override.d.ts.map +1 -1
  140. package/dist/request-model-override.js +178 -19
  141. package/dist/request-model-override.js.map +1 -1
  142. package/dist/run-history.d.ts +32 -0
  143. package/dist/run-history.d.ts.map +1 -0
  144. package/dist/run-history.js +859 -0
  145. package/dist/run-history.js.map +1 -0
  146. package/dist/runs-health.d.ts.map +1 -1
  147. package/dist/runs-health.js +12 -4
  148. package/dist/runs-health.js.map +1 -1
  149. package/dist/runtime-option-extensions.d.ts +14 -0
  150. package/dist/runtime-option-extensions.d.ts.map +1 -0
  151. package/dist/runtime-option-extensions.js +98 -0
  152. package/dist/runtime-option-extensions.js.map +1 -0
  153. package/dist/runtime-routes.d.ts +9 -0
  154. package/dist/runtime-routes.d.ts.map +1 -0
  155. package/dist/runtime-routes.js +13 -0
  156. package/dist/runtime-routes.js.map +1 -0
  157. package/dist/sandbox-manager.d.ts +69 -0
  158. package/dist/sandbox-manager.d.ts.map +1 -0
  159. package/dist/sandbox-manager.js +856 -0
  160. package/dist/sandbox-manager.js.map +1 -0
  161. package/dist/supermemory-plugin.d.ts +54 -0
  162. package/dist/supermemory-plugin.d.ts.map +1 -0
  163. package/dist/supermemory-plugin.js +136 -0
  164. package/dist/supermemory-plugin.js.map +1 -0
  165. package/dist/tui-command.d.ts +15 -0
  166. package/dist/tui-command.d.ts.map +1 -1
  167. package/dist/tui-command.js +46 -6
  168. package/dist/tui-command.js.map +1 -1
  169. package/dist/web-command.d.ts +8 -0
  170. package/dist/web-command.d.ts.map +1 -1
  171. package/dist/web-command.js +174 -17
  172. package/dist/web-command.js.map +1 -1
  173. package/dist/wizard/answers.d.ts +31 -4
  174. package/dist/wizard/answers.d.ts.map +1 -1
  175. package/dist/wizard/answers.js +145 -16
  176. package/dist/wizard/answers.js.map +1 -1
  177. package/dist/wizard/from-flags.d.ts +7 -0
  178. package/dist/wizard/from-flags.d.ts.map +1 -1
  179. package/dist/wizard/from-flags.js +36 -2
  180. package/dist/wizard/from-flags.js.map +1 -1
  181. package/dist/wizard/index.d.ts +4 -4
  182. package/dist/wizard/index.d.ts.map +1 -1
  183. package/dist/wizard/index.js +2 -2
  184. package/dist/wizard/index.js.map +1 -1
  185. package/dist/wizard/model-discovery.d.ts +33 -2
  186. package/dist/wizard/model-discovery.d.ts.map +1 -1
  187. package/dist/wizard/model-discovery.js +826 -108
  188. package/dist/wizard/model-discovery.js.map +1 -1
  189. package/dist/wizard/presets.d.ts +1 -1
  190. package/dist/wizard/presets.d.ts.map +1 -1
  191. package/dist/wizard/presets.js +7 -12
  192. package/dist/wizard/presets.js.map +1 -1
  193. package/dist/wizard/prompts.d.ts +56 -9
  194. package/dist/wizard/prompts.d.ts.map +1 -1
  195. package/dist/wizard/prompts.js +172 -20
  196. package/dist/wizard/prompts.js.map +1 -1
  197. package/dist/wizard/run.d.ts +59 -3
  198. package/dist/wizard/run.d.ts.map +1 -1
  199. package/dist/wizard/run.js +1133 -191
  200. package/dist/wizard/run.js.map +1 -1
  201. package/package.json +21 -16
  202. package/resources/srt/package-lock.json +66 -0
  203. package/resources/srt/package.json +9 -0
  204. package/schema/mono-agent.config.schema.json +381 -17
  205. package/skills/mono-agent-composer/SKILL.md +4 -4
  206. package/skills/mono-agent-composer/references/config-blueprint.md +5 -4
  207. package/skills/mono-agent-composer/references/discovery-questions.md +8 -8
  208. package/skills/mono-agent-composer/references/feature-coverage.md +8 -6
  209. package/skills/mono-agent-composer/references/package-map.md +1 -1
  210. package/skills/mono-agent-composer/references/playbooks.md +8 -7
  211. package/skills/mono-agent-composer/references/validation.md +1 -1
@@ -0,0 +1,914 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, realpathSync, renameSync, } from "node:fs";
3
+ import { chmod, lstat, mkdir, mkdtemp, open, readFile, realpath, rm, writeFile, } from "node:fs/promises";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { dirname, join, relative, resolve, sep } from "node:path";
6
+ import { CONFIG_ENV_KEYS, readMonoAgentConfigJson, } from "@mono-agent/config";
7
+ import { createToolPolicy } from "@mono-agent/agent-harness";
8
+ import { loadAppCoreConfig } from "./app-config.js";
9
+ import { createConfiguredAgentResponder } from "./configured-agent.js";
10
+ import { CONFIGURATION_PROPOSAL_MCP_SERVER_NAME, CONFIGURATION_PROPOSAL_TOOL_NAME, configurationProposalMcpServerSpec, } from "./configuration-proposal-tool.js";
11
+ import { validateMonoAgentFolder } from "./doctor.js";
12
+ export const LOCAL_CONFIGURATION_PROMPT = "Begin local configuration mode. Read the mono-agent-configure skill, then ask the operator one concise question: how would they like to configure you further? Mention that behavior, memory, skills, tools, or channels can be discussed, but do not repeat the setup wizard and do not ask for secrets.";
13
+ const CONFIGURATION_READ_ONLY_TOOLS = [
14
+ "ReadSkill",
15
+ "MemoryRecall",
16
+ CONFIGURATION_PROPOSAL_TOOL_NAME,
17
+ `mcp__${CONFIGURATION_PROPOSAL_MCP_SERVER_NAME}__${CONFIGURATION_PROPOSAL_TOOL_NAME}`,
18
+ ];
19
+ /** Build a current-folder responder without starting channels or creating service state. */
20
+ export async function createLocalConfigurationSession(options) {
21
+ const manager = await LocalConfigurationManager.create(options);
22
+ let active = await manager.buildResponder();
23
+ const activateResponder = async (replacement) => {
24
+ const previous = active;
25
+ active = replacement;
26
+ await previous.dispose?.();
27
+ };
28
+ const replaceActiveResponder = async () => {
29
+ const replacement = await manager.buildResponder();
30
+ await activateResponder(replacement);
31
+ };
32
+ const proxy = {
33
+ respond: async (request, stream) => await active.respond(request, stream),
34
+ cancel: (conversationId, reason) => active.cancel?.(conversationId, reason),
35
+ deliverVerbatim: async (conversationId, text) => await active.deliverVerbatim?.(conversationId, text),
36
+ };
37
+ return {
38
+ responder: proxy,
39
+ title: manager.currentConfig.agent?.name ?? "Mono Agent",
40
+ configuration: {
41
+ ...(options.configure ? { initialPrompt: LOCAL_CONFIGURATION_PROMPT } : {}),
42
+ prompt: LOCAL_CONFIGURATION_PROMPT,
43
+ takeProposal: async () => {
44
+ let proposal;
45
+ try {
46
+ proposal = await manager.takeProposal();
47
+ }
48
+ catch (error) {
49
+ // A failed proposal must not leave its request-scoped MCP server in a
50
+ // retained provider session. Rotate first, then report the validation
51
+ // failure to the TUI.
52
+ await replaceActiveResponder();
53
+ throw error;
54
+ }
55
+ if (proposal === undefined) {
56
+ // Codex and other resumable providers retain the MCP configuration
57
+ // from the turn that opened their provider session. Rotating after a
58
+ // proposal-free configuration turn makes the next reply start with a
59
+ // fresh request-scoped server and removes the tool before ordinary
60
+ // turns. The stable proxy keeps the TUI connection unchanged.
61
+ await replaceActiveResponder();
62
+ }
63
+ return proposal;
64
+ },
65
+ approve: async (id) => {
66
+ const applied = await manager.apply(id);
67
+ let replacement;
68
+ try {
69
+ replacement = await manager.buildResponder();
70
+ }
71
+ catch (error) {
72
+ await applied.rollback();
73
+ throw new Error(`The approved files were restored because the replacement responder could not start: ${error instanceof Error ? error.message : String(error)}`);
74
+ }
75
+ await activateResponder(replacement);
76
+ return {
77
+ message: `Applied configuration change ${applied.changeId}. A fresh provider conversation is now active. Rollback evidence: ${applied.rollbackDir}`,
78
+ };
79
+ },
80
+ reject: async (id) => {
81
+ const result = await manager.reject(id);
82
+ await replaceActiveResponder();
83
+ return result;
84
+ },
85
+ },
86
+ async dispose() {
87
+ await active.dispose?.();
88
+ await manager.dispose();
89
+ },
90
+ };
91
+ }
92
+ export class LocalConfigurationManager {
93
+ options;
94
+ sessionDir;
95
+ currentConfig;
96
+ pendingSinks = new Set();
97
+ prepared = new Map();
98
+ constructor(options, sessionDir, currentConfig) {
99
+ this.options = options;
100
+ this.sessionDir = sessionDir;
101
+ this.currentConfig = currentConfig;
102
+ }
103
+ static async create(options) {
104
+ const authenticated = await authenticatedLocalConfig(options.cwd, options.configPath);
105
+ const secureOptions = {
106
+ ...options,
107
+ cwd: authenticated.cwd,
108
+ configPath: authenticated.configPath,
109
+ };
110
+ const parent = await ensureOwnedDirectoryInside(secureOptions.cwd, join(secureOptions.cwd, ".mono-agent", "configuration-proposals"), "Configuration proposal directory");
111
+ const sessionDir = await mkdtemp(join(parent, "session-"));
112
+ await chmod(sessionDir, 0o700);
113
+ const currentConfig = await loadAppCoreConfig(secureOptions);
114
+ return new LocalConfigurationManager(secureOptions, sessionDir, currentConfig);
115
+ }
116
+ async buildResponder() {
117
+ await resolveOwnedRegularFileInside(this.options.cwd, this.options.configPath, "Config file");
118
+ const config = await loadAppCoreConfig(this.options);
119
+ return await createConfiguredAgentResponder({
120
+ config,
121
+ runtimeOptionsForRequest: async (input) => await this.runtimeExtension(input),
122
+ });
123
+ }
124
+ async runtimeExtension(input) {
125
+ if (!isLocalConfigurationRequest(input.request.metadata)) {
126
+ return { runtimeOptions: {} };
127
+ }
128
+ const secureConfig = await resolveOwnedRegularFileInside(this.options.cwd, this.options.configPath, "Config file");
129
+ const snapshot = await readMonoAgentConfigJson(secureConfig);
130
+ const sessionDir = await ensureOwnedDirectoryInside(this.options.cwd, this.sessionDir, "Configuration proposal session");
131
+ const sinkPath = join(sessionDir, `${safeFilePart(input.runId)}.json`);
132
+ this.pendingSinks.add(sinkPath);
133
+ const proposalServer = configurationProposalMcpServerSpec({
134
+ sinkPath,
135
+ baseVersion: snapshot.version,
136
+ }, this.options.cwd);
137
+ const directProviderNeedsNativeReadOnly = this.currentConfig.runtime.model.sdk === "codex"
138
+ || this.currentConfig.runtime.model.sdk === "opencode";
139
+ return {
140
+ toolPolicyOverride: createToolPolicy({
141
+ // Direct Codex/OpenCode cannot project a finite tool list. Their native
142
+ // plan posture below denies mutating built-ins; replacing MCP policy
143
+ // still removes every configured external/action server.
144
+ allowedTools: directProviderNeedsNativeReadOnly ? ["*"] : CONFIGURATION_READ_ONLY_TOOLS,
145
+ disallowedTools: [],
146
+ mcpServers: {
147
+ [CONFIGURATION_PROPOSAL_MCP_SERVER_NAME]: proposalServer,
148
+ },
149
+ }),
150
+ runtimeOptions: {
151
+ // Native direct-provider bridges enforce read-only/deny-all action
152
+ // posture with this field; finite-list runtimes get both defenses.
153
+ permissionMode: "plan",
154
+ },
155
+ };
156
+ }
157
+ async takeProposal() {
158
+ await ensureOwnedDirectoryInside(this.options.cwd, this.sessionDir, "Configuration proposal session");
159
+ for (const sink of [...this.pendingSinks]) {
160
+ this.pendingSinks.delete(sink);
161
+ const contents = await readOptional(sink);
162
+ await rm(sink, { force: true });
163
+ if (contents === undefined)
164
+ continue;
165
+ const proposal = parseProposal(contents);
166
+ return await this.prepareProposal(proposal);
167
+ }
168
+ return undefined;
169
+ }
170
+ async reject(id) {
171
+ if (!this.prepared.delete(id)) {
172
+ throw new Error(`Configuration proposal ${id} is no longer pending.`);
173
+ }
174
+ return { message: `Rejected configuration proposal ${id}; no files changed.` };
175
+ }
176
+ async prepareProposal(proposal) {
177
+ const prepared = await this.prepare(proposal);
178
+ this.prepared.set(proposal.id, prepared);
179
+ return prepared.card;
180
+ }
181
+ async prepare(proposal) {
182
+ const secureConfig = await resolveOwnedRegularFileInside(this.options.cwd, this.options.configPath, "Config file");
183
+ const current = await readMonoAgentConfigJson(secureConfig);
184
+ if (proposal.baseVersion !== current.version) {
185
+ throw new Error("Configuration changed while the agent was preparing its proposal. Run /configure again from the current config.");
186
+ }
187
+ assertProposalContainsNoSecrets(proposal, this.options.env);
188
+ assertNoEnvironmentShadow(proposal.patch, this.options.env);
189
+ const candidate = applyJsonPatch(current.json, proposal.patch);
190
+ assertConversationalPatchAllowed(current.json, candidate);
191
+ const configBefore = await readFile(secureConfig, "utf8");
192
+ if (sha256(configBefore) !== current.version) {
193
+ throw new Error("Configuration changed while the agent was preparing its proposal. Run /configure again from the current config.");
194
+ }
195
+ await this.validateCandidate(candidate, proposal.id);
196
+ let rolePath;
197
+ let roleBefore;
198
+ let roleAfter;
199
+ let expectedRoleHash;
200
+ if (proposal.role !== undefined) {
201
+ if (proposal.patch.some((operation) => pathsOverlap(operation.path, "/context/identityPath"))) {
202
+ throw new Error("Change context.identityPath separately from a Role update so the host can verify one identity file at a time.");
203
+ }
204
+ const effective = await loadAppCoreConfig(this.options);
205
+ rolePath = effective.context.identityPath;
206
+ rolePath = await resolveOwnedRegularFileInside(this.options.cwd, rolePath, "Identity file");
207
+ roleBefore = await readFile(rolePath, "utf8");
208
+ expectedRoleHash = sha256(roleBefore);
209
+ roleAfter = replaceRoleSection(roleBefore, proposal.role);
210
+ }
211
+ const details = proposal.patch.map(formatPatchOperation);
212
+ if (proposal.role !== undefined)
213
+ details.push("replace IDENTITY.md ## Role body");
214
+ const card = {
215
+ id: proposal.id,
216
+ title: "Agent configuration proposal",
217
+ rationale: proposal.rationale,
218
+ details,
219
+ };
220
+ return {
221
+ proposal,
222
+ candidate,
223
+ expectedConfigVersion: current.version,
224
+ configBefore,
225
+ ...(rolePath === undefined ? {} : { rolePath }),
226
+ ...(roleBefore === undefined ? {} : { roleBefore }),
227
+ ...(roleAfter === undefined ? {} : { roleAfter }),
228
+ ...(expectedRoleHash === undefined ? {} : { expectedRoleHash }),
229
+ card,
230
+ };
231
+ }
232
+ async apply(id) {
233
+ const prepared = this.prepared.get(id);
234
+ if (prepared === undefined)
235
+ throw new Error(`Configuration proposal ${id} is no longer pending.`);
236
+ return await withConfigurationTransactionLock(this.options.cwd, id, async () => await this.applyPrepared(id, prepared));
237
+ }
238
+ async applyPrepared(id, prepared) {
239
+ await assertPreparedSourcesCurrent(this.options.cwd, this.options.configPath, prepared, "after the proposal was shown");
240
+ await this.validateCandidate(prepared.candidate, `${id}-approval`);
241
+ const changeId = `${new Date().toISOString().replace(/[:.]/gu, "-")}-${id.slice(0, 8)}`;
242
+ const rollbackRoot = await ensureOwnedDirectoryInside(this.options.cwd, join(this.options.cwd, ".mono-agent", "config-changes"), "Configuration rollback directory");
243
+ const rollbackDir = await ensureOwnedDirectoryInside(this.options.cwd, join(rollbackRoot, changeId), "Configuration change directory");
244
+ await writeFile(join(rollbackDir, "mono-agent.config.json.before"), prepared.configBefore, { flag: "wx", mode: 0o600 });
245
+ if (prepared.roleBefore !== undefined) {
246
+ await writeFile(join(rollbackDir, "IDENTITY.md.before"), prepared.roleBefore, { flag: "wx", mode: 0o600 });
247
+ }
248
+ const configAfter = `${JSON.stringify(prepared.candidate, null, 2)}\n`;
249
+ let configWritten = false;
250
+ let roleWritten = false;
251
+ try {
252
+ // Candidate validation and rollback-evidence staging both await I/O. A
253
+ // second comparison at the actual commit boundary prevents either step
254
+ // from opening a stale-snapshot overwrite window. The owner-only lock
255
+ // serializes every mono-agent writer; the comparison also catches an
256
+ // editor or other non-cooperating process.
257
+ await assertPreparedSourcesCurrent(this.options.cwd, this.options.configPath, prepared, "while the approved change was being prepared");
258
+ await atomicReplaceExact(this.options.cwd, this.options.configPath, prepared.configBefore, configAfter);
259
+ configWritten = true;
260
+ if (prepared.rolePath !== undefined && prepared.roleAfter !== undefined) {
261
+ // Config and Role are two files, so re-check both sides after the first
262
+ // rename and before the second. If either changed, the guarded catch
263
+ // restores only files that still equal our exact committed bytes.
264
+ await assertExactOwnedContents(this.options.cwd, this.options.configPath, configAfter, "Committed config changed before the Role update");
265
+ await assertExactOwnedContents(this.options.cwd, prepared.rolePath, prepared.roleBefore, "IDENTITY.md changed at the Role commit boundary");
266
+ await atomicReplaceExact(this.options.cwd, prepared.rolePath, prepared.roleBefore, prepared.roleAfter);
267
+ roleWritten = true;
268
+ }
269
+ const verified = await readMonoAgentConfigJson(this.options.configPath);
270
+ if (!isDeepStrictEqual(verified.json, prepared.candidate)) {
271
+ throw new Error("The committed config did not verify byte-for-byte against the approved candidate.");
272
+ }
273
+ if (prepared.rolePath !== undefined && prepared.roleAfter !== undefined) {
274
+ await assertExactOwnedContents(this.options.cwd, prepared.rolePath, prepared.roleAfter, "The committed Role did not verify");
275
+ }
276
+ await ensureOwnedDirectoryInside(this.options.cwd, rollbackDir, "Configuration change directory");
277
+ await writeFile(join(rollbackDir, "change.json"), `${JSON.stringify({
278
+ schema: "mono-agent.configuration-change.v1",
279
+ changeId,
280
+ proposalId: prepared.proposal.id,
281
+ appliedAt: new Date().toISOString(),
282
+ previousConfigVersion: prepared.expectedConfigVersion,
283
+ configVersion: verified.version,
284
+ changedPaths: prepared.proposal.patch.map((operation) => operation.path),
285
+ roleChanged: prepared.roleAfter !== undefined,
286
+ }, null, 2)}\n`, { flag: "wx", mode: 0o600 });
287
+ }
288
+ catch (error) {
289
+ const recoveryErrors = [];
290
+ if (roleWritten && prepared.rolePath !== undefined && prepared.roleAfter !== undefined && prepared.roleBefore !== undefined) {
291
+ try {
292
+ await restoreIfExact(this.options.cwd, prepared.rolePath, prepared.roleAfter, prepared.roleBefore);
293
+ }
294
+ catch (restoreError) {
295
+ recoveryErrors.push(restoreError instanceof Error ? restoreError.message : String(restoreError));
296
+ }
297
+ }
298
+ if (configWritten) {
299
+ try {
300
+ await restoreIfExact(this.options.cwd, this.options.configPath, configAfter, prepared.configBefore);
301
+ }
302
+ catch (restoreError) {
303
+ recoveryErrors.push(restoreError instanceof Error ? restoreError.message : String(restoreError));
304
+ }
305
+ }
306
+ else if (!roleWritten) {
307
+ await rm(rollbackDir, { recursive: true, force: true });
308
+ }
309
+ if (recoveryErrors.length > 0) {
310
+ throw new Error(`${error instanceof Error ? error.message : String(error)} Manual recovery is required; concurrent edits were preserved. ` +
311
+ `${recoveryErrors.join(" ")} Rollback evidence: ${rollbackDir}`);
312
+ }
313
+ throw error;
314
+ }
315
+ this.prepared.delete(id);
316
+ return {
317
+ changeId,
318
+ rollbackDir,
319
+ rollback: async () => {
320
+ await withConfigurationTransactionLock(this.options.cwd, `${id}-rollback`, async () => {
321
+ await assertExactOwnedContents(this.options.cwd, this.options.configPath, configAfter, "Configuration changed before rollback");
322
+ let roleRestored = false;
323
+ if (prepared.rolePath !== undefined && prepared.roleAfter !== undefined) {
324
+ await assertExactOwnedContents(this.options.cwd, prepared.rolePath, prepared.roleAfter, "IDENTITY.md changed before rollback");
325
+ await atomicReplaceExact(this.options.cwd, prepared.rolePath, prepared.roleAfter, prepared.roleBefore);
326
+ roleRestored = true;
327
+ }
328
+ try {
329
+ await atomicReplaceExact(this.options.cwd, this.options.configPath, configAfter, prepared.configBefore);
330
+ }
331
+ catch (error) {
332
+ if (roleRestored && prepared.rolePath !== undefined && prepared.roleAfter !== undefined) {
333
+ try {
334
+ await atomicReplaceExact(this.options.cwd, prepared.rolePath, prepared.roleBefore, prepared.roleAfter);
335
+ }
336
+ catch (compensationError) {
337
+ throw new Error(`${error instanceof Error ? error.message : String(error)} ` +
338
+ `Role compensation also failed: ${compensationError instanceof Error ? compensationError.message : String(compensationError)} ` +
339
+ `Concurrent edits were preserved; manual recovery is required. Rollback evidence: ${rollbackDir}`);
340
+ }
341
+ }
342
+ throw error;
343
+ }
344
+ await ensureOwnedDirectoryInside(this.options.cwd, rollbackDir, "Configuration change directory");
345
+ await writeFile(join(rollbackDir, "rollback.json"), `${JSON.stringify({
346
+ schema: "mono-agent.configuration-rollback.v1",
347
+ changeId,
348
+ rolledBackAt: new Date().toISOString(),
349
+ }, null, 2)}\n`, { flag: "wx", mode: 0o600 });
350
+ });
351
+ },
352
+ };
353
+ }
354
+ async validateCandidate(candidate, label) {
355
+ const sessionDir = await ensureOwnedDirectoryInside(this.options.cwd, this.sessionDir, "Configuration proposal session");
356
+ const path = join(sessionDir, `${safeFilePart(label)}.candidate.json`);
357
+ await writeFile(path, `${JSON.stringify(candidate, null, 2)}\n`, { mode: 0o600 });
358
+ try {
359
+ const report = await validateMonoAgentFolder({
360
+ cwd: this.options.cwd,
361
+ configPath: path,
362
+ env: this.options.env,
363
+ allowFilesystemWrites: false,
364
+ liveness: false,
365
+ });
366
+ const errors = report.sections.filter((section) => section.status === "error");
367
+ if (errors.length > 0) {
368
+ throw new Error(`Proposed configuration does not validate: ${errors.map((section) => `${section.label}: ${section.details.join(" ")}`).join("; ")}`);
369
+ }
370
+ await loadAppCoreConfig({ cwd: this.options.cwd, configPath: path, env: this.options.env });
371
+ }
372
+ finally {
373
+ await rm(path, { force: true });
374
+ }
375
+ }
376
+ async dispose() {
377
+ await rm(this.sessionDir, { recursive: true, force: true });
378
+ }
379
+ }
380
+ export function isLocalConfigurationRequest(metadata) {
381
+ const tui = metadata?.tui;
382
+ return typeof tui === "object" && tui !== null
383
+ && tui.local === true
384
+ && tui.configuration === true;
385
+ }
386
+ function parseProposal(contents) {
387
+ const parsed = JSON.parse(contents);
388
+ if (parsed.schema !== "mono-agent.configuration-proposal.v1"
389
+ || typeof parsed.id !== "string"
390
+ || typeof parsed.baseVersion !== "string"
391
+ || typeof parsed.rationale !== "string"
392
+ || !Array.isArray(parsed.patch)
393
+ || typeof parsed.createdAt !== "string") {
394
+ throw new Error("The configuration proposal payload was malformed.");
395
+ }
396
+ return parsed;
397
+ }
398
+ export function applyJsonPatch(input, operations) {
399
+ const root = JSON.parse(JSON.stringify(input));
400
+ for (const operation of operations) {
401
+ if (operation.path === "")
402
+ throw new Error("Replacing the entire config document is not allowed.");
403
+ const path = pointerSegments(operation.path);
404
+ if (operation.op === "add")
405
+ addValue(root, path, cloneJson(operation.value));
406
+ else if (operation.op === "remove")
407
+ removeValue(root, path);
408
+ else if (operation.op === "replace")
409
+ replaceValue(root, path, cloneJson(operation.value));
410
+ else if (operation.op === "test") {
411
+ if (!isDeepStrictEqual(getValue(root, path), operation.value)) {
412
+ throw new Error(`JSON Patch test failed at ${operation.path}.`);
413
+ }
414
+ }
415
+ else {
416
+ if (operation.from === undefined)
417
+ throw new Error(`${operation.op} requires from.`);
418
+ const from = pointerSegments(operation.from);
419
+ const value = cloneJson(getValue(root, from));
420
+ if (operation.op === "move")
421
+ removeValue(root, from);
422
+ addValue(root, path, value);
423
+ }
424
+ }
425
+ return root;
426
+ }
427
+ function pointerSegments(pointer) {
428
+ if (!pointer.startsWith("/"))
429
+ throw new Error(`Invalid JSON Pointer: ${pointer}`);
430
+ const segments = pointer.slice(1).split("/").map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"));
431
+ for (const segment of segments) {
432
+ if (segment === "__proto__" || segment === "prototype" || segment === "constructor") {
433
+ throw new Error(`Unsafe JSON Pointer segment: ${segment}`);
434
+ }
435
+ }
436
+ return segments;
437
+ }
438
+ function parentAt(root, segments) {
439
+ if (segments.length === 0)
440
+ throw new Error("Root JSON Patch operations are not allowed.");
441
+ let current = root;
442
+ for (const segment of segments.slice(0, -1)) {
443
+ if (Array.isArray(current)) {
444
+ const index = arrayIndex(segment, current.length, false);
445
+ current = current[index];
446
+ }
447
+ else if (isRecord(current) && Object.hasOwn(current, segment)) {
448
+ current = current[segment];
449
+ }
450
+ else {
451
+ throw new Error(`JSON Patch path does not exist: /${segments.join("/")}`);
452
+ }
453
+ }
454
+ if (!Array.isArray(current) && !isRecord(current)) {
455
+ throw new Error(`JSON Patch parent is not a container: /${segments.join("/")}`);
456
+ }
457
+ return { parent: current, key: segments.at(-1) };
458
+ }
459
+ function addValue(root, segments, value) {
460
+ const { parent, key } = parentAt(root, segments);
461
+ if (Array.isArray(parent)) {
462
+ const index = key === "-" ? parent.length : arrayIndex(key, parent.length, true);
463
+ parent.splice(index, 0, value);
464
+ }
465
+ else {
466
+ parent[key] = value;
467
+ }
468
+ }
469
+ function replaceValue(root, segments, value) {
470
+ const { parent, key } = parentAt(root, segments);
471
+ if (Array.isArray(parent))
472
+ parent[arrayIndex(key, parent.length, false)] = value;
473
+ else {
474
+ if (!Object.hasOwn(parent, key))
475
+ throw new Error(`JSON Patch replace target does not exist: /${segments.join("/")}`);
476
+ parent[key] = value;
477
+ }
478
+ }
479
+ function removeValue(root, segments) {
480
+ const { parent, key } = parentAt(root, segments);
481
+ if (Array.isArray(parent))
482
+ return parent.splice(arrayIndex(key, parent.length, false), 1)[0];
483
+ if (!Object.hasOwn(parent, key))
484
+ throw new Error(`JSON Patch remove target does not exist: /${segments.join("/")}`);
485
+ const value = parent[key];
486
+ delete parent[key];
487
+ return value;
488
+ }
489
+ function getValue(root, segments) {
490
+ let current = root;
491
+ for (const segment of segments) {
492
+ if (Array.isArray(current))
493
+ current = current[arrayIndex(segment, current.length, false)];
494
+ else if (isRecord(current) && Object.hasOwn(current, segment))
495
+ current = current[segment];
496
+ else
497
+ throw new Error(`JSON Patch path does not exist: /${segments.join("/")}`);
498
+ }
499
+ return current;
500
+ }
501
+ function arrayIndex(segment, length, allowEnd) {
502
+ if (!/^(?:0|[1-9]\d*)$/u.test(segment))
503
+ throw new Error(`Invalid JSON Patch array index: ${segment}`);
504
+ const index = Number(segment);
505
+ if (!Number.isSafeInteger(index) || index < 0 || index > length || (!allowEnd && index === length)) {
506
+ throw new Error(`JSON Patch array index out of bounds: ${segment}`);
507
+ }
508
+ return index;
509
+ }
510
+ function cloneJson(value) {
511
+ return value === undefined ? value : JSON.parse(JSON.stringify(value));
512
+ }
513
+ function assertProposalContainsNoSecrets(proposal, env) {
514
+ const secretValues = Object.entries(env)
515
+ .filter(([name, value]) => /(?:api.?key|credential|password|secret|token)/iu.test(name) && (value?.length ?? 0) >= 4)
516
+ .map(([, value]) => value);
517
+ for (const operation of proposal.patch) {
518
+ if (secretBearingPointer(operation.path) || (operation.from !== undefined && secretBearingPointer(operation.from))) {
519
+ throw new Error("Secret-bearing config fields cannot be proposed in chat. Use the masked mono-agent auth or owner-only .env flow.");
520
+ }
521
+ if (containsSecret(operation.value, secretValues)) {
522
+ throw new Error("A proposal matched a configured secret value. It was rejected and will not be displayed or written.");
523
+ }
524
+ }
525
+ if (containsSecret(proposal.rationale, secretValues) || containsSecretLikeValue(proposal.rationale)) {
526
+ throw new Error("The proposal rationale appears to contain a secret. It was rejected.");
527
+ }
528
+ if (proposal.role !== undefined && containsSecret(proposal.role, secretValues)) {
529
+ throw new Error("The proposed Role matched a configured secret value. It was rejected.");
530
+ }
531
+ if (proposal.role !== undefined && containsSecretLikeValue(proposal.role)) {
532
+ throw new Error("The proposed Role appears to contain a secret. It was rejected.");
533
+ }
534
+ }
535
+ function secretBearingPointer(pointer) {
536
+ return pointerSegments(pointer).some((segment) => /(?:api.?key|credential|password|secret|token)/iu.test(segment) && !/(?:env|path)$/iu.test(segment));
537
+ }
538
+ function containsSecret(value, secrets) {
539
+ if (typeof value === "string")
540
+ return secrets.some((secret) => value.includes(secret)) || containsSecretLikeValue(value);
541
+ if (Array.isArray(value))
542
+ return value.some((entry) => containsSecret(entry, secrets));
543
+ if (isRecord(value)) {
544
+ return Object.entries(value).some(([key, entry]) => (/(?:api.?key|credential|password|secret|token)/iu.test(key) && !/(?:env|path)$/iu.test(key))
545
+ || containsSecret(entry, secrets));
546
+ }
547
+ return false;
548
+ }
549
+ function containsSecretLikeValue(value) {
550
+ return /\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{12,}|Bearer\s+\S{12,}|\d{6,12}:[A-Za-z0-9_-]{20,})\b/u.test(value);
551
+ }
552
+ function assertNoEnvironmentShadow(patch, env) {
553
+ const shadowed = Object.entries(CONFIG_ENV_KEYS).filter(([field, envKey]) => {
554
+ if ((env[envKey]?.trim().length ?? 0) === 0)
555
+ return false;
556
+ const pointer = `/${field.split(".").join("/")}`;
557
+ return patch.some((operation) => pathsOverlap(operation.path, pointer)
558
+ || (operation.from !== undefined && pathsOverlap(operation.from, pointer)));
559
+ });
560
+ if (shadowed.length > 0) {
561
+ throw new Error(`The effective environment overrides this proposal (${shadowed.map(([field, key]) => `${field} via ${key}`).join(", ")}). ` +
562
+ "Update or remove the durable environment override first, then run /configure again.");
563
+ }
564
+ }
565
+ /**
566
+ * Conversational configuration deliberately exposes a small positive surface.
567
+ * New schema fields therefore fail closed until they receive an explicit
568
+ * safety decision here; everything involving paths, providers, credentials,
569
+ * background work, network access, or sandbox policy stays in a guided flow.
570
+ */
571
+ const CONVERSATIONAL_CONFIG_POINTERS = new Set([
572
+ "/agent/name",
573
+ "/runtime/effort",
574
+ "/runtime/maxTurns",
575
+ "/runtime/session/mode",
576
+ "/runtime/session/idleTimeoutMs",
577
+ "/runtime/session/rollover",
578
+ "/runtime/session/rolloverTimezone",
579
+ "/runtime/session/rolloverNotice",
580
+ "/context/selectedSkills",
581
+ "/context/skillMaxBytes",
582
+ "/context/skillDisclosure",
583
+ "/memory/maxBytes",
584
+ "/memory/recallTool/enabled",
585
+ "/tools/allowedTools",
586
+ "/tools/disallowedTools",
587
+ ]);
588
+ function assertConversationalPatchAllowed(before, after) {
589
+ const changed = changedJsonPointers(before, after);
590
+ const unsupported = changed.filter((pointer) => !CONVERSATIONAL_CONFIG_POINTERS.has(pointer));
591
+ if (unsupported.length > 0) {
592
+ throw new Error(`Conversational configuration cannot change ${unsupported.join(", ")}. ` +
593
+ "Paths, memory tiers or capture cost, providers/models/routes, embeddings endpoints, credentials, channels/cron/plugins, MCP, sandbox/network policy, exporters, and other new schema fields require the explicit guided flow.");
594
+ }
595
+ if (toolAuthorityBroadened(before.tools, after.tools)) {
596
+ throw new Error("Broader tool authority requires explicit guided confirmation outside the model conversation.");
597
+ }
598
+ }
599
+ function changedJsonPointers(before, after, pointer = "") {
600
+ if (isDeepStrictEqual(before, after))
601
+ return [];
602
+ const beforeObject = isRecord(before);
603
+ const afterObject = isRecord(after);
604
+ if ((beforeObject || before === undefined)
605
+ && (afterObject || after === undefined)
606
+ && (beforeObject || afterObject)) {
607
+ const keys = new Set([
608
+ ...(beforeObject ? Object.keys(before) : []),
609
+ ...(afterObject ? Object.keys(after) : []),
610
+ ]);
611
+ if (keys.size === 0)
612
+ return [pointer || "/"];
613
+ return [...keys].flatMap((key) => changedJsonPointers(beforeObject ? before[key] : undefined, afterObject ? after[key] : undefined, `${pointer}/${escapeJsonPointerSegment(key)}`));
614
+ }
615
+ return [pointer || "/"];
616
+ }
617
+ function escapeJsonPointerSegment(value) {
618
+ return value.replace(/~/gu, "~0").replace(/\//gu, "~1");
619
+ }
620
+ function toolAuthorityBroadened(before, after) {
621
+ const beforeAllowed = new Set(before?.allowedTools ?? []);
622
+ const afterAllowed = new Set(after?.allowedTools ?? []);
623
+ const beforeDenied = new Set(before?.disallowedTools ?? []);
624
+ const afterDenied = new Set(after?.disallowedTools ?? []);
625
+ const beforeAllowsAll = before?.allowedTools === undefined || beforeAllowed.has("*");
626
+ const afterAllowsAll = after?.allowedTools === undefined || afterAllowed.has("*");
627
+ if (afterAllowsAll) {
628
+ return !beforeAllowsAll || [...beforeDenied].some((tool) => !afterDenied.has(tool));
629
+ }
630
+ return [...afterAllowed].some((tool) => tool !== "*"
631
+ && !afterDenied.has(tool)
632
+ && ((!beforeAllowsAll && !beforeAllowed.has(tool)) || beforeDenied.has(tool)));
633
+ }
634
+ function replaceRoleSection(identity, role) {
635
+ const body = role.trim();
636
+ if (body.length === 0 || /^##\s/mu.test(body)) {
637
+ throw new Error("The generated Role must be non-empty and cannot introduce another level-two Identity section.");
638
+ }
639
+ const match = /^## Role\s*\n([\s\S]*?)(?=\n##\s|$)/mu.exec(identity);
640
+ if (match === null || match.index === undefined) {
641
+ throw new Error("IDENTITY.md has no canonical ## Role section to replace safely.");
642
+ }
643
+ const start = match.index;
644
+ const end = start + match[0].length;
645
+ return `${identity.slice(0, start)}## Role\n\n${body}\n${identity.slice(end).replace(/^\n*/u, "\n")}`;
646
+ }
647
+ function formatPatchOperation(operation) {
648
+ if (operation.op === "remove")
649
+ return `remove ${operation.path}`;
650
+ if (operation.op === "move" || operation.op === "copy")
651
+ return `${operation.op} ${operation.from} -> ${operation.path}`;
652
+ if (operation.op === "test")
653
+ return `verify current value at ${operation.path}`;
654
+ const rendered = JSON.stringify(operation.value);
655
+ const compact = rendered === undefined ? "" : rendered.length <= 180 ? rendered : `${rendered.slice(0, 179)}…`;
656
+ return `${operation.op} ${operation.path} = ${compact}`;
657
+ }
658
+ function pathsOverlap(left, right) {
659
+ return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
660
+ }
661
+ async function authenticatedLocalConfig(cwd, configPath) {
662
+ const lexicalCwd = resolve(cwd);
663
+ const lexicalConfig = resolve(configPath);
664
+ assertLexicalPathInside(lexicalCwd, lexicalConfig, "Config path");
665
+ const canonicalCwd = await realpath(lexicalCwd);
666
+ await assertOwnedDirectory(canonicalCwd, "Current agent folder");
667
+ const relativeConfig = relative(lexicalCwd, lexicalConfig);
668
+ const canonicalConfig = await resolveOwnedRegularFileInside(canonicalCwd, resolve(canonicalCwd, relativeConfig), "Config file");
669
+ return { cwd: canonicalCwd, configPath: canonicalConfig };
670
+ }
671
+ async function resolveOwnedRegularFileInside(root, path, label) {
672
+ const canonicalRoot = await realpath(resolve(root));
673
+ const absolute = resolve(path);
674
+ assertLexicalPathInside(canonicalRoot, absolute, label);
675
+ const segments = relative(canonicalRoot, absolute).split(sep).filter((segment) => segment.length > 0);
676
+ if (segments.length === 0)
677
+ throw new Error(`${label} must name a file inside the current agent folder.`);
678
+ let parent = canonicalRoot;
679
+ await assertOwnedDirectory(parent, "Current agent folder");
680
+ for (const segment of segments.slice(0, -1)) {
681
+ parent = join(parent, segment);
682
+ await assertOwnedDirectory(parent, `${label} parent`);
683
+ }
684
+ const target = join(parent, segments.at(-1));
685
+ const info = await lstat(target);
686
+ assertOwnedRegularFileInfo(info, target, label);
687
+ const canonicalTarget = await realpath(target);
688
+ assertLexicalPathInside(canonicalRoot, canonicalTarget, label);
689
+ if (canonicalTarget !== target) {
690
+ throw new Error(`${label} must not traverse a symbolic-link parent: ${path}`);
691
+ }
692
+ return target;
693
+ }
694
+ async function ensureOwnedDirectoryInside(root, path, label) {
695
+ const canonicalRoot = await realpath(resolve(root));
696
+ const absolute = resolve(path);
697
+ assertLexicalPathInside(canonicalRoot, absolute, label);
698
+ const segments = relative(canonicalRoot, absolute).split(sep).filter((segment) => segment.length > 0);
699
+ let current = canonicalRoot;
700
+ await assertOwnedDirectory(current, "Current agent folder");
701
+ for (const segment of segments) {
702
+ current = join(current, segment);
703
+ try {
704
+ await mkdir(current, { mode: 0o700 });
705
+ }
706
+ catch (error) {
707
+ if (error.code !== "EEXIST")
708
+ throw error;
709
+ }
710
+ await assertOwnedDirectory(current, label);
711
+ }
712
+ return current;
713
+ }
714
+ async function assertOwnedDirectory(path, label) {
715
+ assertOwnedDirectoryInfo(await lstat(path), path, label);
716
+ }
717
+ function assertOwnedDirectoryInfo(info, path, label) {
718
+ if (!info.isDirectory())
719
+ throw new Error(`${label} must be a real directory, not a symbolic link: ${path}`);
720
+ const uid = process.getuid?.();
721
+ if (uid !== undefined && info.uid !== uid)
722
+ throw new Error(`${label} must be owned by the current user: ${path}`);
723
+ if ((info.mode & 0o022) !== 0)
724
+ throw new Error(`${label} must not be group/world writable: ${path}`);
725
+ }
726
+ function assertOwnedRegularFileInfo(info, path, label) {
727
+ if (!info.isFile() || info.nlink !== 1)
728
+ throw new Error(`${label} must be one regular file with one link: ${path}`);
729
+ const uid = process.getuid?.();
730
+ if (uid !== undefined && info.uid !== uid)
731
+ throw new Error(`${label} must be owned by the current user: ${path}`);
732
+ if ((info.mode & 0o022) !== 0)
733
+ throw new Error(`${label} must not be group/world writable: ${path}`);
734
+ }
735
+ function isLexicallyInside(root, path) {
736
+ const rel = relative(resolve(root), resolve(path));
737
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`));
738
+ }
739
+ function assertLexicalPathInside(root, path, label) {
740
+ if (!isLexicallyInside(root, path)) {
741
+ throw new Error(`${label} must stay inside the current agent folder: ${path}`);
742
+ }
743
+ }
744
+ async function assertPreparedSourcesCurrent(cwd, configPath, prepared, phase) {
745
+ const secureConfig = await resolveOwnedRegularFileInside(cwd, configPath, "Config file");
746
+ const current = await readMonoAgentConfigJson(secureConfig);
747
+ if (current.version !== prepared.expectedConfigVersion) {
748
+ throw new Error(`Configuration changed ${phase}. Nothing was written; run /configure again.`);
749
+ }
750
+ if (prepared.rolePath !== undefined) {
751
+ const secureRole = await resolveOwnedRegularFileInside(cwd, prepared.rolePath, "Identity file");
752
+ const currentRole = await readFile(secureRole, "utf8");
753
+ if (sha256(currentRole) !== prepared.expectedRoleHash) {
754
+ throw new Error(`IDENTITY.md changed ${phase}. Nothing was written; run /configure again.`);
755
+ }
756
+ }
757
+ }
758
+ async function assertExactOwnedContents(cwd, path, expected, label) {
759
+ const securePath = await resolveOwnedRegularFileInside(cwd, path, label);
760
+ if (await readFile(securePath, "utf8") !== expected) {
761
+ throw new Error(`${label}; the concurrent edit was preserved.`);
762
+ }
763
+ }
764
+ async function restoreIfExact(cwd, path, expected, restore) {
765
+ await atomicReplaceExact(cwd, path, expected, restore);
766
+ }
767
+ async function atomicReplaceExact(root, path, expected, contents) {
768
+ const securePath = await resolveOwnedRegularFileInside(root, path, "Configuration transaction file");
769
+ const info = await lstat(securePath);
770
+ const temporary = join(dirname(securePath), `.${randomUUID()}.mono-agent-tmp`);
771
+ let handle;
772
+ try {
773
+ handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), info.mode & 0o777);
774
+ await handle.writeFile(contents, "utf8");
775
+ await handle.chmod(info.mode & 0o777);
776
+ await handle.sync();
777
+ await handle.close();
778
+ handle = undefined;
779
+ // Temp creation and fsync intentionally happen before the last source
780
+ // comparison. From that comparison through rename there is no JavaScript
781
+ // yield: an editor that reacts to the staged temp is observed, while a
782
+ // cooperating mono-agent writer is also serialized by the owner lock.
783
+ commitAtomicReplacementSync(root, securePath, temporary, expected, info);
784
+ }
785
+ finally {
786
+ await handle?.close();
787
+ await rm(temporary, { force: true });
788
+ }
789
+ }
790
+ function commitAtomicReplacementSync(root, path, temporary, expected, initialInfo) {
791
+ const securePath = resolveOwnedRegularFileInsideSync(root, path, "Configuration transaction file");
792
+ let sourceHandle;
793
+ try {
794
+ sourceHandle = openSync(securePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
795
+ const openedBefore = fstatSync(sourceHandle);
796
+ assertOwnedRegularFileInfo(openedBefore, securePath, "Configuration transaction file");
797
+ const current = readFileSync(sourceHandle, "utf8");
798
+ const openedAfter = fstatSync(sourceHandle);
799
+ const named = lstatSync(securePath);
800
+ if (current !== expected
801
+ || !sameFileIdentityAndMetadata(initialInfo, openedBefore)
802
+ || !sameFileIdentityAndMetadata(openedBefore, openedAfter)
803
+ || !sameFileIdentityAndMetadata(openedAfter, named)) {
804
+ throw new Error(`Refusing to replace changed file ${securePath}; the concurrent edit was preserved.`);
805
+ }
806
+ renameSync(temporary, securePath);
807
+ }
808
+ finally {
809
+ if (sourceHandle !== undefined)
810
+ closeSync(sourceHandle);
811
+ }
812
+ }
813
+ function resolveOwnedRegularFileInsideSync(root, path, label) {
814
+ const canonicalRoot = realpathSync(resolve(root));
815
+ const absolute = resolve(path);
816
+ assertLexicalPathInside(canonicalRoot, absolute, label);
817
+ const segments = relative(canonicalRoot, absolute).split(sep).filter((segment) => segment.length > 0);
818
+ if (segments.length === 0)
819
+ throw new Error(`${label} must name a file inside the current agent folder.`);
820
+ let parent = canonicalRoot;
821
+ assertOwnedDirectoryInfo(lstatSync(parent), parent, "Current agent folder");
822
+ for (const segment of segments.slice(0, -1)) {
823
+ parent = join(parent, segment);
824
+ assertOwnedDirectoryInfo(lstatSync(parent), parent, `${label} parent`);
825
+ }
826
+ const target = join(parent, segments.at(-1));
827
+ assertOwnedRegularFileInfo(lstatSync(target), target, label);
828
+ const canonicalTarget = realpathSync(target);
829
+ assertLexicalPathInside(canonicalRoot, canonicalTarget, label);
830
+ if (canonicalTarget !== target) {
831
+ throw new Error(`${label} must not traverse a symbolic-link parent: ${path}`);
832
+ }
833
+ return target;
834
+ }
835
+ function sameFileIdentityAndMetadata(left, right) {
836
+ return left.dev === right.dev
837
+ && left.ino === right.ino
838
+ && left.nlink === right.nlink
839
+ && left.mode === right.mode
840
+ && left.size === right.size
841
+ && left.mtimeMs === right.mtimeMs
842
+ && left.ctimeMs === right.ctimeMs;
843
+ }
844
+ async function withConfigurationTransactionLock(cwd, label, operation) {
845
+ const monoAgentDir = await ensureOwnedDirectoryInside(cwd, join(cwd, ".mono-agent"), "Configuration state directory");
846
+ const lockPath = join(monoAgentDir, "configuration.lock");
847
+ const token = randomUUID();
848
+ const contents = `${JSON.stringify({
849
+ schema: "mono-agent.configuration-lock.v1",
850
+ pid: process.pid,
851
+ token,
852
+ label: safeFilePart(label),
853
+ createdAt: new Date().toISOString(),
854
+ })}\n`;
855
+ let handle;
856
+ let identity;
857
+ try {
858
+ try {
859
+ handle = await open(lockPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
860
+ }
861
+ catch (error) {
862
+ if (error.code === "EEXIST") {
863
+ throw new Error(`Another local configuration transaction owns ${lockPath}. Wait for it to finish; inspect and remove the lock manually only if its owner crashed.`);
864
+ }
865
+ throw error;
866
+ }
867
+ await handle.chmod(0o600);
868
+ await handle.writeFile(contents, "utf8");
869
+ await handle.sync();
870
+ const info = await handle.stat();
871
+ assertOwnedRegularFileInfo(info, lockPath, "Configuration transaction lock");
872
+ identity = { dev: info.dev, ino: info.ino };
873
+ await handle.close();
874
+ handle = undefined;
875
+ return await operation();
876
+ }
877
+ finally {
878
+ await handle?.close();
879
+ if (identity !== undefined) {
880
+ const current = await lstat(lockPath).catch((error) => {
881
+ if (error.code === "ENOENT")
882
+ return undefined;
883
+ throw error;
884
+ });
885
+ if (current === undefined
886
+ || current.dev !== identity.dev
887
+ || current.ino !== identity.ino
888
+ || await readFile(lockPath, "utf8") !== contents) {
889
+ throw new Error(`Configuration transaction lock changed unexpectedly and was left untouched: ${lockPath}`);
890
+ }
891
+ await rm(lockPath);
892
+ }
893
+ }
894
+ }
895
+ function sha256(value) {
896
+ return createHash("sha256").update(value).digest("hex");
897
+ }
898
+ function safeFilePart(value) {
899
+ return value.replace(/[^A-Za-z0-9._-]/gu, "_").slice(0, 120);
900
+ }
901
+ async function readOptional(path) {
902
+ try {
903
+ return await readFile(path, "utf8");
904
+ }
905
+ catch (error) {
906
+ if (error.code === "ENOENT")
907
+ return undefined;
908
+ throw error;
909
+ }
910
+ }
911
+ function isRecord(value) {
912
+ return typeof value === "object" && value !== null && !Array.isArray(value);
913
+ }
914
+ //# sourceMappingURL=local-configuration.js.map