@cortexkit/opencode-magic-context 0.33.0 → 0.34.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 (265) hide show
  1. package/dist/config/agent-disable.d.ts +22 -0
  2. package/dist/config/agent-disable.d.ts.map +1 -1
  3. package/dist/config/index.d.ts.map +1 -1
  4. package/dist/config/project-security.d.ts +7 -0
  5. package/dist/config/project-security.d.ts.map +1 -1
  6. package/dist/config/schema/magic-context.d.ts +144 -24
  7. package/dist/config/schema/magic-context.d.ts.map +1 -1
  8. package/dist/config/transform-mode.d.ts +2 -0
  9. package/dist/config/transform-mode.d.ts.map +1 -1
  10. package/dist/{conflict-warning-hook-bwa99a04.js → conflict-warning-hook-fvpfvt0w.js} +1 -1
  11. package/dist/{derive-budgets-e9j09sjr.js → derive-budgets-9crp0cvd.js} +1 -1
  12. package/dist/features/builtin-commands/commands.d.ts +1 -1
  13. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  14. package/dist/features/magic-context/compaction-marker.d.ts +106 -0
  15. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  16. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +0 -2
  17. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  18. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  21. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  23. package/dist/features/magic-context/dreamer/task-registry.d.ts +0 -1
  24. package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
  25. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  26. package/dist/features/magic-context/fail-closed-block.d.ts +10 -0
  27. package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
  28. package/dist/features/magic-context/git-commits/index.d.ts +2 -2
  29. package/dist/features/magic-context/git-commits/index.d.ts.map +1 -1
  30. package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts +0 -2
  31. package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts.map +1 -1
  32. package/dist/features/magic-context/git-commits/storage-git-commits.d.ts +0 -13
  33. package/dist/features/magic-context/git-commits/storage-git-commits.d.ts.map +1 -1
  34. package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
  35. package/dist/features/magic-context/memory/embedding-local.d.ts +7 -1
  36. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  37. package/dist/features/magic-context/memory/embedding.d.ts +1 -2
  38. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  39. package/dist/features/magic-context/memory/memory-migration.d.ts +0 -6
  40. package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -1
  41. package/dist/features/magic-context/memory/project-identity.d.ts +2 -6
  42. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  43. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts +0 -1
  44. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
  45. package/dist/features/magic-context/memory/storage-memory.d.ts +1 -6
  46. package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
  47. package/dist/features/magic-context/memory/verification-paths.d.ts +0 -1
  48. package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
  49. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  50. package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
  51. package/dist/features/magic-context/mural/render-mural.d.ts.map +1 -1
  52. package/dist/features/magic-context/mural/storage-mural.d.ts +0 -1
  53. package/dist/features/magic-context/mural/storage-mural.d.ts.map +1 -1
  54. package/dist/features/magic-context/overflow-detection.d.ts +8 -1
  55. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  56. package/dist/features/magic-context/project-embedding-registry.d.ts +16 -2
  57. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  58. package/dist/features/magic-context/scheduler.d.ts.map +1 -1
  59. package/dist/features/magic-context/schema-fence-probe.d.ts +28 -0
  60. package/dist/features/magic-context/schema-fence-probe.d.ts.map +1 -0
  61. package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
  62. package/dist/features/magic-context/smart-notes/capabilities.d.ts +0 -2
  63. package/dist/features/magic-context/smart-notes/capabilities.d.ts.map +1 -1
  64. package/dist/features/magic-context/smart-notes/compiler.d.ts +0 -1
  65. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  66. package/dist/features/magic-context/smart-notes/types.d.ts +0 -16
  67. package/dist/features/magic-context/smart-notes/types.d.ts.map +1 -1
  68. package/dist/features/magic-context/storage-db.d.ts +35 -1
  69. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  70. package/dist/features/magic-context/storage-meta-persisted.d.ts +38 -5
  71. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  72. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  73. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  74. package/dist/features/magic-context/storage.d.ts +1 -1
  75. package/dist/features/magic-context/storage.d.ts.map +1 -1
  76. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  77. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +0 -2
  78. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
  79. package/dist/hooks/auto-update-checker/types.d.ts +0 -3
  80. package/dist/hooks/auto-update-checker/types.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/boundary-execution.d.ts +1 -1
  82. package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
  83. package/dist/hooks/magic-context/cache-busting-signals.d.ts +1 -1
  84. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  86. package/dist/hooks/magic-context/child-session-spawn.d.ts +26 -0
  87. package/dist/hooks/magic-context/child-session-spawn.d.ts.map +1 -0
  88. package/dist/hooks/magic-context/command-handler.d.ts +2 -0
  89. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/compaction-marker-manager.d.ts +49 -0
  91. package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
  92. package/dist/hooks/magic-context/compaction-off-transition.d.ts +96 -0
  93. package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -0
  94. package/dist/hooks/magic-context/compartment-parser.d.ts +12 -0
  95. package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
  96. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -0
  97. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  99. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts +0 -12
  100. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
  101. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  103. package/dist/hooks/magic-context/compartment-trigger.d.ts +12 -5
  104. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +47 -1
  106. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/emergency-drop.d.ts +2 -2
  108. package/dist/hooks/magic-context/event-handler.d.ts +7 -0
  109. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  110. package/dist/hooks/magic-context/event-resolvers.d.ts +13 -21
  111. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
  113. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -1
  115. package/dist/hooks/magic-context/historian-prompt.generated.d.ts +1 -1
  116. package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -1
  117. package/dist/hooks/magic-context/hook-handlers.d.ts +1 -0
  118. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  119. package/dist/hooks/magic-context/hook.d.ts +11 -1
  120. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  121. package/dist/hooks/magic-context/inject-compartments.d.ts +18 -0
  122. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  123. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  124. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  125. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  126. package/dist/hooks/magic-context/rust-mode-transform.d.ts +3 -0
  127. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  128. package/dist/hooks/magic-context/send-session-notification.d.ts +20 -1
  129. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  130. package/dist/hooks/magic-context/sentinel.d.ts +39 -0
  131. package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
  132. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  133. package/dist/hooks/magic-context/tool-drop-target.d.ts +19 -0
  134. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  135. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -0
  136. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  137. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +30 -0
  138. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  139. package/dist/hooks/magic-context/transform.d.ts +27 -0
  140. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  141. package/dist/{index-kxz4tn6m.js → index-56yjaeb2.js} +56 -4
  142. package/dist/{index-bmkea2dq.js → index-b563sga5.js} +40 -287
  143. package/dist/{index-y1c9tcm3.js → index-cn960ffd.js} +101 -20
  144. package/dist/{index-k5y60ymm.js → index-dtas91s5.js} +27 -6
  145. package/dist/index-fcyvznch.js +279 -0
  146. package/dist/{index-63dq2g4d.js → index-ry8rh8cv.js} +138 -24
  147. package/dist/{index-h86twgsp.js → index-s0mdn690.js} +2113 -1067
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +1356 -627
  150. package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
  151. package/dist/plugin/hooks/create-session-hooks.d.ts +19 -8
  152. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  153. package/dist/plugin/messages-transform.d.ts +10 -0
  154. package/dist/plugin/messages-transform.d.ts.map +1 -1
  155. package/dist/plugin/rpc-handlers.d.ts +3 -7
  156. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  157. package/dist/plugin/tool-registry.d.ts +8 -0
  158. package/dist/plugin/tool-registry.d.ts.map +1 -1
  159. package/dist/{recomp-orchestrator-yh7pepvx.js → recomp-orchestrator-8e82vmvb.js} +6 -5
  160. package/dist/send-session-notification-6njqewzk.js +20 -0
  161. package/dist/shared/conflict-detector.d.ts +38 -1
  162. package/dist/shared/conflict-detector.d.ts.map +1 -1
  163. package/dist/shared/conflict-fixer.d.ts +18 -1
  164. package/dist/shared/conflict-fixer.d.ts.map +1 -1
  165. package/dist/shared/context-limit-provenance.d.ts +3 -0
  166. package/dist/shared/context-limit-provenance.d.ts.map +1 -0
  167. package/dist/shared/escalation-bands.d.ts +9 -0
  168. package/dist/shared/escalation-bands.d.ts.map +1 -0
  169. package/dist/shared/format-threshold.d.ts +1 -1
  170. package/dist/shared/models-dev-cache.d.ts +29 -3
  171. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  172. package/dist/shared/rpc-server.d.ts.map +1 -1
  173. package/dist/shared/rpc-types.d.ts +46 -1
  174. package/dist/shared/rpc-types.d.ts.map +1 -1
  175. package/dist/shared/sqlite.d.ts +9 -4
  176. package/dist/shared/sqlite.d.ts.map +1 -1
  177. package/dist/shared/storage-permissions.d.ts +5 -0
  178. package/dist/shared/storage-permissions.d.ts.map +1 -0
  179. package/dist/{storage-meta-session-1phy0b1z.js → storage-meta-session-xt41zbw4.js} +2 -1
  180. package/dist/{task-config-dn9k9szr.js → task-config-5nx3qfz8.js} +1 -2
  181. package/dist/tui/compaction-off.d.ts +13 -0
  182. package/dist/tui/compaction-off.d.ts.map +1 -0
  183. package/dist/tui/data/context-db.d.ts.map +1 -1
  184. package/dist/tui-compiled/compaction-off.d.ts +13 -0
  185. package/dist/tui-compiled/compaction-off.d.ts.map +1 -0
  186. package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
  187. package/package.json +8 -3
  188. package/src/agents/dreamer.ts +68 -0
  189. package/src/agents/hidden-agent-registrations.ts +331 -0
  190. package/src/agents/historian.ts +3 -0
  191. package/src/agents/language-directive.test.ts +138 -0
  192. package/src/agents/language-directive.ts +121 -0
  193. package/src/agents/magic-context-prompt.language.test.ts +35 -0
  194. package/src/agents/magic-context-prompt.test.ts +292 -0
  195. package/src/agents/magic-context-prompt.ts +182 -0
  196. package/src/agents/permissions.test.ts +252 -0
  197. package/src/agents/permissions.ts +186 -0
  198. package/src/agents/sidekick.ts +1 -0
  199. package/src/agents/smart-note-compiler.ts +1 -0
  200. package/src/config/agent-disable.ts +106 -0
  201. package/src/config/compaction-accessor-guard.test.ts +82 -0
  202. package/src/config/compaction-backcompat.test.ts +52 -0
  203. package/src/config/index.test.ts +904 -0
  204. package/src/config/index.ts +601 -0
  205. package/src/config/migrate-config-location.test.ts +332 -0
  206. package/src/config/migrate-config-location.ts +635 -0
  207. package/src/config/migrate-dreamer-v2.test.ts +287 -0
  208. package/src/config/migrate-dreamer-v2.ts +336 -0
  209. package/src/config/migrate-experimental.ts +159 -0
  210. package/src/config/project-security.test.ts +366 -0
  211. package/src/config/project-security.ts +588 -0
  212. package/src/config/prune-config-leaf.test.ts +78 -0
  213. package/src/config/prune-config-leaf.ts +68 -0
  214. package/src/config/removed-config-keys.test.ts +116 -0
  215. package/src/config/schema/agent-overrides.ts +45 -0
  216. package/src/config/schema/magic-context.test.ts +358 -0
  217. package/src/config/schema/magic-context.ts +1002 -0
  218. package/src/config/transform-mode.test.ts +62 -0
  219. package/src/config/transform-mode.ts +34 -0
  220. package/src/config/variable.test.ts +323 -0
  221. package/src/config/variable.ts +192 -0
  222. package/src/features/magic-context/defaults.ts +1 -0
  223. package/src/features/magic-context/dreamer/cron.ts +243 -0
  224. package/src/features/magic-context/dreamer/task-registry.ts +104 -0
  225. package/src/shared/conflict-detector.test.ts +251 -1
  226. package/src/shared/conflict-detector.ts +84 -5
  227. package/src/shared/conflict-fixer.test.ts +309 -0
  228. package/src/shared/conflict-fixer.ts +82 -5
  229. package/src/shared/context-limit-provenance.ts +6 -0
  230. package/src/shared/escalation-bands.test.ts +23 -0
  231. package/src/shared/escalation-bands.ts +18 -0
  232. package/src/shared/format-threshold.ts +1 -1
  233. package/src/shared/models-dev-cache.test.ts +171 -0
  234. package/src/shared/models-dev-cache.ts +195 -38
  235. package/src/shared/rpc-server.ts +27 -18
  236. package/src/shared/rpc-types.ts +46 -1
  237. package/src/shared/sqlite.ts +13 -33
  238. package/src/shared/storage-permissions.ts +20 -0
  239. package/src/tui/compaction-off.test.ts +56 -0
  240. package/src/tui/compaction-off.ts +29 -0
  241. package/src/tui/data/context-db.ts +6 -0
  242. package/src/tui/index.tsx +113 -65
  243. package/src/tui/slots/sidebar-content.tsx +139 -83
  244. package/src/tui-compiled/compaction-off.ts +29 -0
  245. package/src/tui-compiled/data/context-db.ts +6 -0
  246. package/src/tui-compiled/index.tsx +558 -455
  247. package/src/tui-compiled/slots/sidebar-content.tsx +237 -161
  248. package/dist/config/schema.d.ts +0 -3
  249. package/dist/config/schema.d.ts.map +0 -1
  250. package/dist/features/magic-context/plugin-messages.d.ts +0 -75
  251. package/dist/features/magic-context/plugin-messages.d.ts.map +0 -1
  252. package/dist/hooks/magic-context/compartment-runner-state-xml.d.ts +0 -21
  253. package/dist/hooks/magic-context/compartment-runner-state-xml.d.ts.map +0 -1
  254. package/dist/index-anhrfygk.js +0 -57
  255. package/dist/send-session-notification-tghq9gnh.js +0 -11
  256. package/dist/shared/opencode-compaction-detector.d.ts +0 -2
  257. package/dist/shared/opencode-compaction-detector.d.ts.map +0 -1
  258. package/dist/shared/transcript-opencode.d.ts +0 -71
  259. package/dist/shared/transcript-opencode.d.ts.map +0 -1
  260. package/dist/tools/index.d.ts +0 -6
  261. package/dist/tools/index.d.ts.map +0 -1
  262. package/src/shared/opencode-compaction-detector.test.ts +0 -239
  263. package/src/shared/opencode-compaction-detector.ts +0 -81
  264. package/src/shared/transcript-opencode.ts +0 -302
  265. /package/dist/{index-v7fmc1s2.js → index-ygcng9t8.js} +0 -0
@@ -31,7 +31,7 @@ export function formatThresholdPercent(value: number | undefined | null): string
31
31
  * Build a terse "clamped" annotation for execute-threshold display, or "" when the
32
32
  * configured value was not clamped. Status surfaces (/ctx-status text, pi status
33
33
  * dialog) append this so a user who configured e.g. 190000 tokens on a 128K model
34
- * sees that it was reduced to the 80% safety cap, instead of the value being
34
+ * sees that it was reduced to the 90% safety cap, instead of the value being
35
35
  * silently ignored (issue #241). The note includes the configured value and the cap
36
36
  * so the user can see the math.
37
37
  *
@@ -10,6 +10,8 @@ import {
10
10
  refreshModelLimitsAfterAuthOnce,
11
11
  refreshModelLimitsFromApi,
12
12
  resetAuthRewarmLatchForTest,
13
+ resolveLimit,
14
+ setOutputReserveConfig,
13
15
  } from "./models-dev-cache";
14
16
 
15
17
  /**
@@ -18,6 +20,88 @@ import {
18
20
  * cold start. We no longer read OpenCode's `models.json` file ourselves (a torn
19
21
  * read produced impossible limits and a stale copy out-voted the live cap).
20
22
  */
23
+ describe("output-token reservation", () => {
24
+ beforeEach(() => setOutputReserveConfig(undefined));
25
+
26
+ test("keeps a pre-carved OpenAI catalog input limit unchanged", () => {
27
+ expect(
28
+ resolveLimit(
29
+ { context: 1_050_000, input: 922_000, output: 128_000 },
30
+ "openai",
31
+ "gpt-5.4",
32
+ ),
33
+ ).toBe(922_000);
34
+ });
35
+
36
+ test("input equal to context falls through to shared-window reservation", () => {
37
+ expect(
38
+ resolveLimit(
39
+ { context: 372_000, input: 372_000, output: 64_000 },
40
+ "openai",
41
+ "literal-config",
42
+ ),
43
+ ).toBe(308_000);
44
+ });
45
+
46
+ test("reserves output for Anthropic shared windows", () => {
47
+ expect(resolveLimit({ context: 1_000_000, output: 64_000 }, "anthropic", "claude")).toBe(
48
+ 936_000,
49
+ );
50
+ });
51
+
52
+ test("makes the reporter's 95% send safe under the shared provider wall", () => {
53
+ const usable = resolveLimit(
54
+ { context: 122_880, output: 16_384 },
55
+ "openai-compatible",
56
+ "reporter-model",
57
+ );
58
+ expect(usable).toBe(106_496);
59
+ expect((usable ?? 0) * 0.95 + 16_384).toBeLessThanOrEqual(131_072);
60
+ });
61
+
62
+ test("caps absurd catalog output at 25% of context", () => {
63
+ expect(
64
+ resolveLimit({ context: 100_000, output: 60_000 }, "anthropic", "absurd-output"),
65
+ ).toBe(75_000);
66
+ });
67
+
68
+ test("keeps proven separate-quota Gemini windows unchanged", () => {
69
+ expect(
70
+ resolveLimit({ context: 1_048_576, output: 65_536 }, "google", "gemini-2.5-pro"),
71
+ ).toBe(1_048_576);
72
+ expect(
73
+ resolveLimit(
74
+ { context: 1_048_576, output: 65_536 },
75
+ "google-antigravity",
76
+ "gemini-2.5-pro",
77
+ ),
78
+ ).toBe(1_048_576);
79
+ });
80
+
81
+ test("output_reserve overrides shared and separate quota defaults", () => {
82
+ expect(resolveLimit({ context: 100_000, output: 20_000 }, "anthropic", "claude", 0)).toBe(
83
+ 100_000,
84
+ );
85
+ const perModelReserve = { default: 4_000, "google/gemini": 8_000 };
86
+ expect(
87
+ resolveLimit({ context: 100_000, output: 20_000 }, "google", "gemini", perModelReserve),
88
+ ).toBe(92_000);
89
+ expect(
90
+ resolveLimit(
91
+ { context: 100_000, output: 20_000 },
92
+ "google-antigravity",
93
+ "gemini",
94
+ perModelReserve,
95
+ ),
96
+ ).toBe(92_000);
97
+ });
98
+
99
+ test("clamps reservation to both the 50% and 1024-token usable floors", () => {
100
+ expect(resolveLimit({ context: 100_000 }, "custom", "tiny", 90_000)).toBe(50_000);
101
+ expect(resolveLimit({ context: 1_200 }, "custom", "micro", 1_000)).toBe(1_024);
102
+ });
103
+ });
104
+
21
105
  describe("models-dev-cache (SDK-only)", () => {
22
106
  let tempDir: string;
23
107
  let originalXdgData: string | undefined;
@@ -32,6 +116,7 @@ describe("models-dev-cache (SDK-only)", () => {
32
116
  // touch the real ~/.local/share/cortexkit/magic-context cache.
33
117
  originalXdgData = process.env.XDG_DATA_HOME;
34
118
  process.env.XDG_DATA_HOME = tempDir;
119
+ setOutputReserveConfig(undefined);
35
120
  clearModelsDevCache();
36
121
  });
37
122
 
@@ -103,6 +188,32 @@ describe("models-dev-cache (SDK-only)", () => {
103
188
  expect(getSdkContextLimit("openai", "gpt-5.4-mini")).toBe(922000);
104
189
  });
105
190
 
191
+ test("narrows raw context with detected wire truth before reserving output", async () => {
192
+ await refreshModelLimitsFromApi(
193
+ makeClient([
194
+ {
195
+ id: "anthropic",
196
+ models: {
197
+ claude: { limit: { context: 200_000, output: 20_000 } },
198
+ },
199
+ },
200
+ ]),
201
+ );
202
+
203
+ expect(
204
+ getSdkContextLimit("anthropic", "claude", 120_000, {
205
+ detectedLimitProvenance: "combined",
206
+ }),
207
+ ).toBe(100_000);
208
+ // A provider-reported prompt ceiling is already pre-carved. Treating it
209
+ // as combined would subtract the 20K output allowance a second time.
210
+ expect(
211
+ getSdkContextLimit("anthropic", "claude", 120_000, {
212
+ detectedLimitProvenance: "prompt_only",
213
+ }),
214
+ ).toBe(120_000);
215
+ });
216
+
106
217
  test("matches a tagged ollama model against its tag-less SDK entry", async () => {
107
218
  await refreshModelLimitsFromApi(
108
219
  makeClient([
@@ -379,3 +490,63 @@ describe("models-dev-cache (SDK-only)", () => {
379
490
  expect(getSdkContextLimit("p", "m3")).toBe(200000);
380
491
  });
381
492
  });
493
+
494
+ describe("getSdkContextLimit prompt_only pre-carve arm", () => {
495
+ // The v0.34.0 re-verify found no behavioral test for the provenance
496
+ // ternary: a prompt_only detected limit must enter the pre-carved INPUT
497
+ // arm (no output subtraction), while combined detections narrow the raw
498
+ // context BEFORE reservation. A regression routing prompt_only through
499
+ // the context arm double-reserves and these fixtures go red.
500
+ beforeEach(() => clearModelsDevCache());
501
+ afterEach(() => clearModelsDevCache());
502
+ const seed = () =>
503
+ refreshModelLimitsFromApi({
504
+ config: {
505
+ providers: async () => ({
506
+ data: {
507
+ providers: [
508
+ {
509
+ id: "anthropic",
510
+ models: {
511
+ "prov-model": {
512
+ limit: { context: 200000, output: 64000 },
513
+ },
514
+ },
515
+ },
516
+ ],
517
+ },
518
+ }),
519
+ },
520
+ });
521
+
522
+ test("prompt_only detection routes through the input arm without double reservation", async () => {
523
+ await seed();
524
+ expect(
525
+ getSdkContextLimit("anthropic", "prov-model", 167000, {
526
+ detectedLimitProvenance: "prompt_only",
527
+ }),
528
+ ).toBe(167000);
529
+ });
530
+
531
+ test("combined detection narrows raw context before reservation", async () => {
532
+ await seed();
533
+ // min(200000, 131072) = 131072, then arm-2 reserve min(64000, 25%) = 32768.
534
+ expect(
535
+ getSdkContextLimit("anthropic", "prov-model", 131072, {
536
+ detectedLimitProvenance: "combined",
537
+ }),
538
+ ).toBe(131072 - 32768);
539
+ });
540
+
541
+ test("prompt_only with reservation none serves the prompt cap as the native denominator", async () => {
542
+ await seed();
543
+ // The input arm short-circuits before reservation: the native metric
544
+ // sees the provider-enforced prompt wall, not the raw window.
545
+ expect(
546
+ getSdkContextLimit("anthropic", "prov-model", 167000, {
547
+ detectedLimitProvenance: "prompt_only",
548
+ reservation: "none",
549
+ }),
550
+ ).toBe(167000);
551
+ });
552
+ });
@@ -27,9 +27,12 @@
27
27
 
28
28
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
29
29
  import { join } from "node:path";
30
+ import type { ContextLimitProvenance } from "./context-limit-provenance";
30
31
  import { getMagicContextStorageDir } from "./data-path";
31
32
  import { getHarness } from "./harness";
33
+ import { piModelRefToCanonical } from "./harness-provider-map";
32
34
  import { sessionLog } from "./logger";
35
+ import { shouldEnforcePrivateStoragePermissions } from "./storage-permissions";
33
36
 
34
37
  interface OpencodeClientLike {
35
38
  config: {
@@ -54,14 +57,35 @@ export function isSaneLimit(limit: number | undefined): limit is number {
54
57
  return typeof limit === "number" && limit >= MIN_SANE_LIMIT && limit <= MAX_SANE_LIMIT;
55
58
  }
56
59
 
60
+ export type OutputReserveConfig = number | { default: number; [modelKey: string]: number };
61
+
62
+ export interface ModelLimit {
63
+ context?: number;
64
+ input?: number;
65
+ output?: number;
66
+ }
67
+
57
68
  interface CachedModelMetadata {
69
+ /** Legacy resolved value, retained so pre-upgrade persisted caches remain readable. */
58
70
  limit?: number;
71
+ /** Raw combined context window. Reservation is applied only when the value is read. */
72
+ contextLimit?: number;
59
73
  /** Provider-enforced prompt cap. Undefined when only a combined context window is known. */
60
74
  inputLimit?: number;
75
+ /** Maximum generated tokens advertised by the provider/model catalog. */
76
+ outputLimit?: number;
61
77
  /** Provider metadata says the model accepts image input. Unknown is false. */
62
78
  vision?: boolean;
63
79
  }
64
80
 
81
+ // Proven-separate allowlist only. Unknown providers reserve by default because
82
+ // wasting some input capacity is safer than a shared-window hard rejection.
83
+ const SEPARATE_OUTPUT_QUOTA_PROVIDERS = new Set(["google", "google-antigravity"]);
84
+ const MIN_PLAUSIBLE_CONTEXT_LIMIT = 1024;
85
+ const OUTPUT_RESERVE_CAP_RATIO = 0.25;
86
+ let outputReserveConfig: OutputReserveConfig | undefined;
87
+ const reserveClampLogSeen = new Set<string>();
88
+
65
89
  /**
66
90
  * Authoritative source (OpenCode only): populated async from the SDK
67
91
  * `config.providers()`, which is OpenCode's fully-resolved config — models.dev +
@@ -96,17 +120,28 @@ function loadPersistedApiCacheOnce(): void {
96
120
  const raw = readFileSync(persistFilePath(), "utf-8");
97
121
  const obj = JSON.parse(raw) as Record<
98
122
  string,
99
- number | { limit?: number; inputLimit?: number; vision?: boolean }
123
+ | number
124
+ | {
125
+ limit?: number;
126
+ contextLimit?: number;
127
+ inputLimit?: number;
128
+ outputLimit?: number;
129
+ vision?: boolean;
130
+ }
100
131
  >;
101
132
  const map = new Map<string, CachedModelMetadata>();
102
133
  for (const [key, persisted] of Object.entries(obj)) {
103
134
  const limit = typeof persisted === "number" ? persisted : persisted.limit;
135
+ const contextLimit = typeof persisted === "number" ? undefined : persisted.contextLimit;
104
136
  const inputLimit = typeof persisted === "number" ? undefined : persisted.inputLimit;
137
+ const outputLimit = typeof persisted === "number" ? undefined : persisted.outputLimit;
105
138
  const vision = typeof persisted === "number" ? false : persisted.vision === true;
106
- if (isSaneLimit(limit)) {
139
+ if (isSaneLimit(contextLimit) || isSaneLimit(limit)) {
107
140
  map.set(key, {
108
- limit,
141
+ limit: isSaneLimit(limit) ? limit : undefined,
142
+ contextLimit: isSaneLimit(contextLimit) ? contextLimit : undefined,
109
143
  inputLimit: isSaneLimit(inputLimit) ? inputLimit : undefined,
144
+ outputLimit: isFinitePositive(outputLimit) ? outputLimit : undefined,
110
145
  vision,
111
146
  });
112
147
  }
@@ -133,7 +168,9 @@ function persistApiCache(): void {
133
168
  if (isSaneLimit(value.limit)) {
134
169
  obj[key] = {
135
170
  limit: value.limit,
171
+ contextLimit: isSaneLimit(value.contextLimit) ? value.contextLimit : undefined,
136
172
  inputLimit: isSaneLimit(value.inputLimit) ? value.inputLimit : undefined,
173
+ outputLimit: isFinitePositive(value.outputLimit) ? value.outputLimit : undefined,
137
174
  vision: value.vision === true,
138
175
  };
139
176
  }
@@ -143,29 +180,110 @@ function persistApiCache(): void {
143
180
  mkdirSync(dir, { recursive: true });
144
181
  const target = persistFilePath();
145
182
  const tmp = `${target}.${process.pid}.tmp`;
146
- writeFileSync(tmp, JSON.stringify(obj), { encoding: "utf-8", mode: 0o600 });
183
+ if (shouldEnforcePrivateStoragePermissions()) {
184
+ writeFileSync(tmp, JSON.stringify(obj), { encoding: "utf-8", mode: 0o600 });
185
+ } else {
186
+ writeFileSync(tmp, JSON.stringify(obj), { encoding: "utf-8" });
187
+ }
147
188
  renameSync(tmp, target);
148
189
  } catch {
149
190
  // best-effort — a failed persist only loses cold-start warmth, not correctness
150
191
  }
151
192
  }
152
193
 
194
+ function isFinitePositive(value: number | undefined): value is number {
195
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
196
+ }
197
+
198
+ function modelKeyLookupOrder(providerID: string, modelID: string): string[] {
199
+ const full = `${providerID}/${modelID}`;
200
+ const canonicalFull = piModelRefToCanonical(full);
201
+ const candidates = [full, canonicalFull, modelID];
202
+ const colon = modelID.lastIndexOf(":");
203
+ if (colon > 0) {
204
+ const bareModel = modelID.slice(0, colon);
205
+ const providerBare = `${providerID}/${bareModel}`;
206
+ candidates.push(providerBare, piModelRefToCanonical(providerBare), bareModel);
207
+ }
208
+ return [...new Set(candidates)];
209
+ }
210
+
211
+ function configuredOutputReserve(
212
+ config: OutputReserveConfig | undefined,
213
+ providerID: string,
214
+ modelID: string,
215
+ ): number | undefined {
216
+ if (typeof config === "number")
217
+ return Number.isFinite(config) && config >= 0 ? config : undefined;
218
+ if (!config) return undefined;
219
+ for (const candidate of modelKeyLookupOrder(providerID, modelID)) {
220
+ const value = config[candidate];
221
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
222
+ }
223
+ return Number.isFinite(config.default) && config.default >= 0 ? config.default : undefined;
224
+ }
225
+
226
+ function logReserveClampOnce(key: string, message: string): void {
227
+ if (reserveClampLogSeen.has(key)) return;
228
+ reserveClampLogSeen.add(key);
229
+ sessionLog("global", `models-dev-cache: ${message}`);
230
+ }
231
+
232
+ /** Set the user-tier reservation override shared by every resolved-limit consumer. */
233
+ export function setOutputReserveConfig(config: OutputReserveConfig | undefined): void {
234
+ outputReserveConfig = config;
235
+ }
236
+
153
237
  /**
154
- * Resolve the effective pressure limit for a model's `limit` object.
238
+ * Resolve the usable prompt budget from raw provider metadata.
155
239
  *
156
- * Prefers `limit.input` (max prompt tokens the provider will accept) over
157
- * `limit.context` (total window including output). For GitHub Copilot and
158
- * several proxy providers, `context` is the marketing number (input + output
159
- * combined), and sending a prompt sized against `context` gets rejected.
160
- * OpenCode's own `session/overflow.ts` uses `input ?? context` for the same
161
- * reason — the denominator that drives overflow/pressure must be the number
162
- * the provider actually enforces on input.
240
+ * A genuinely smaller input cap is already pre-carved and wins unchanged. All
241
+ * other providers reserve generated tokens from the shared context window by
242
+ * default, except the small allowlist whose APIs document a separate output
243
+ * quota. `output_reserve` overrides that shared/separate decision, including 0
244
+ * to disable reservation. Reservation can never leave less than half the raw
245
+ * context window or the module's 1024-token plausibility floor.
163
246
  */
164
- function resolveLimit(limit: { context?: number; input?: number } | undefined): number | undefined {
247
+ export function resolveLimit(
248
+ limit: ModelLimit | undefined,
249
+ providerID: string,
250
+ modelID: string,
251
+ reserveConfig: OutputReserveConfig | undefined = outputReserveConfig,
252
+ ): number | undefined {
165
253
  if (!limit) return undefined;
166
- if (typeof limit.input === "number" && limit.input > 0) return limit.input;
167
- if (typeof limit.context === "number" && limit.context > 0) return limit.context;
168
- return undefined;
254
+ const context = isFinitePositive(limit.context) ? limit.context : undefined;
255
+ const input = isFinitePositive(limit.input) ? limit.input : undefined;
256
+ if (input !== undefined && (context === undefined || input < context)) return input;
257
+ if (context === undefined) return undefined;
258
+
259
+ const configuredReserve = configuredOutputReserve(reserveConfig, providerID, modelID);
260
+ let reserve: number;
261
+ if (configuredReserve !== undefined) {
262
+ reserve = configuredReserve;
263
+ } else if (SEPARATE_OUTPUT_QUOTA_PROVIDERS.has(providerID)) {
264
+ reserve = 0;
265
+ } else {
266
+ const output = isFinitePositive(limit.output) ? limit.output : 0;
267
+ const cap = context * OUTPUT_RESERVE_CAP_RATIO;
268
+ reserve = Math.min(output, cap);
269
+ if (output > cap) {
270
+ logReserveClampOnce(
271
+ `cap|${providerID}/${modelID}|${context}|${output}`,
272
+ `output reserve capped at 25% for ${providerID}/${modelID}: ${output} → ${cap}`,
273
+ );
274
+ }
275
+ }
276
+
277
+ const floor = Math.max(MIN_PLAUSIBLE_CONTEXT_LIMIT, context * 0.5);
278
+ const maxReserve = Math.max(0, context - floor);
279
+ if (reserve > maxReserve) {
280
+ logReserveClampOnce(
281
+ `floor|${providerID}/${modelID}|${context}|${reserve}`,
282
+ `output reserve clamped for ${providerID}/${modelID}: ${reserve} → ${maxReserve} (usable floor ${floor})`,
283
+ );
284
+ reserve = maxReserve;
285
+ }
286
+ return Math.floor(context - reserve);
169
287
  }
170
288
 
171
289
  function setCachedModelMetadata(
@@ -173,7 +291,7 @@ function setCachedModelMetadata(
173
291
  key: string,
174
292
  model:
175
293
  | {
176
- limit?: { context?: number; input?: number };
294
+ limit?: ModelLimit;
177
295
  experimental?: { modes?: Record<string, unknown> };
178
296
  capabilities?: unknown;
179
297
  modalities?: unknown;
@@ -182,15 +300,19 @@ function setCachedModelMetadata(
182
300
  }
183
301
  | undefined,
184
302
  ): void {
185
- const limit = resolveLimit(model?.limit);
303
+ const contextLimit = model?.limit?.context;
186
304
  const inputLimit = model?.limit?.input;
305
+ const outputLimit = model?.limit?.output;
306
+ const rawLimit = isSaneLimit(contextLimit)
307
+ ? contextLimit
308
+ : isSaneLimit(inputLimit)
309
+ ? inputLimit
310
+ : undefined;
187
311
 
188
- // Only cache plausible limits. A value outside [20k, 3M] is garbage (torn
189
- // read / unconfigured default) and must never enter the cache or get
190
- // persisted — see isSaneLimit.
191
- if (!isSaneLimit(limit)) {
192
- return;
193
- }
312
+ // Validate the raw provider metadata before reservation. A legitimate 20K
313
+ // context may resolve below MIN_SANE_LIMIT after carving output, but it must
314
+ // still remain cached; the 50%/1024 usable floor protects the result.
315
+ if (rawLimit === undefined) return;
194
316
 
195
317
  const values = [model?.capabilities, model?.modalities, model?.input, model?.attachment];
196
318
  const vision = values.some(
@@ -203,8 +325,12 @@ function setCachedModelMetadata(
203
325
  .includes("vision"),
204
326
  );
205
327
  const value: CachedModelMetadata = {
206
- limit,
328
+ // Keep a sane raw fallback for old persisted-cache readers. Runtime
329
+ // resolution below uses context/input/output so user overrides stay live.
330
+ limit: rawLimit,
331
+ contextLimit: isSaneLimit(contextLimit) ? contextLimit : undefined,
207
332
  inputLimit: isSaneLimit(inputLimit) ? inputLimit : undefined,
333
+ outputLimit: isFinitePositive(outputLimit) ? outputLimit : undefined,
208
334
  vision,
209
335
  };
210
336
  cache.set(key, value);
@@ -307,7 +433,7 @@ async function refreshModelLimitsOnce(client: OpencodeClientLike): Promise<boole
307
433
  models?: Record<
308
434
  string,
309
435
  {
310
- limit?: { context?: number; input?: number };
436
+ limit?: ModelLimit;
311
437
  experimental?: { modes?: Record<string, unknown> };
312
438
  }
313
439
  >;
@@ -359,18 +485,51 @@ async function refreshModelLimitsOnce(client: OpencodeClientLike): Promise<boole
359
485
  *
360
486
  * Resolution:
361
487
  * 1. Seed `apiCache` from the persisted last-known-good file once (cold start).
362
- * 2. Return the SDK value (sane by construction — only [20k,3M] is cached).
488
+ * 2. Resolve the sane raw SDK metadata into its output-reserved usable value.
363
489
  * 3. `undefined` when the SDK hasn't reported this model yet → the caller
364
490
  * defaults / retries (the startup warm retries when OpenCode isn't ready).
365
491
  *
366
492
  * OpenCode-only: Pi never warms `apiCache` (it resolves from its own
367
- * `ctx.getModel().contextWindow`), so for Pi this returns `undefined` and Pi's
493
+ * `ctx.model.contextWindow`), so for Pi this returns `undefined` and Pi's
368
494
  * own resolution path is used.
369
495
  */
370
- export function getSdkContextLimit(providerID: string, modelID: string): number | undefined {
496
+ export function getSdkContextLimit(
497
+ providerID: string,
498
+ modelID: string,
499
+ detectedContextLimit?: number,
500
+ options?: {
501
+ reservation?: "default" | "none";
502
+ detectedLimitProvenance?: ContextLimitProvenance;
503
+ },
504
+ ): number | undefined {
371
505
  loadPersistedApiCacheOnce();
372
- const fromApi = lookupLimitWithTagFallback(apiCache, providerID, modelID);
373
- return isSaneLimit(fromApi) ? fromApi : undefined;
506
+ const metadata = lookupMetadataWithTagFallback(apiCache, providerID, modelID);
507
+ if (!metadata) return undefined;
508
+ const rawContext = metadata.contextLimit ?? metadata.limit;
509
+ const promptOnlyDetected =
510
+ options?.detectedLimitProvenance === "prompt_only" && isFinitePositive(detectedContextLimit)
511
+ ? detectedContextLimit
512
+ : undefined;
513
+ const context =
514
+ promptOnlyDetected === undefined &&
515
+ isFinitePositive(detectedContextLimit) &&
516
+ isFinitePositive(rawContext)
517
+ ? Math.min(rawContext, detectedContextLimit)
518
+ : promptOnlyDetected === undefined && isFinitePositive(detectedContextLimit)
519
+ ? detectedContextLimit
520
+ : rawContext;
521
+ const inputCandidates = [metadata.inputLimit, promptOnlyDetected].filter(isFinitePositive);
522
+ const input = inputCandidates.length > 0 ? Math.min(...inputCandidates) : undefined;
523
+ return resolveLimit(
524
+ {
525
+ context,
526
+ input,
527
+ output: metadata.outputLimit,
528
+ },
529
+ providerID,
530
+ modelID,
531
+ options?.reservation === "none" ? 0 : undefined,
532
+ );
374
533
  }
375
534
 
376
535
  /**
@@ -411,20 +570,18 @@ export function getSdkInputLimit(providerID: string, modelID: string): number |
411
570
  * Strategy: exact match first (never collapses a legitimately-tagged model),
412
571
  * then retry once with the last `:tag` segment stripped.
413
572
  */
414
- function lookupLimitWithTagFallback(
573
+ function lookupMetadataWithTagFallback(
415
574
  cache: Map<string, CachedModelMetadata> | null,
416
575
  providerID: string,
417
576
  modelID: string,
418
- ): number | undefined {
577
+ ): CachedModelMetadata | undefined {
419
578
  if (!cache) return undefined;
420
- const exact = cache.get(`${providerID}/${modelID}`)?.limit;
421
- if (typeof exact === "number") return exact;
579
+ const exact = cache.get(`${providerID}/${modelID}`);
580
+ if (exact) return exact;
422
581
 
423
582
  const colonIdx = modelID.lastIndexOf(":");
424
583
  if (colonIdx > 0) {
425
- const baseModel = modelID.slice(0, colonIdx);
426
- const fallback = cache.get(`${providerID}/${baseModel}`)?.limit;
427
- if (typeof fallback === "number") return fallback;
584
+ return cache.get(`${providerID}/${modelID.slice(0, colonIdx)}`);
428
585
  }
429
586
  return undefined;
430
587
  }
@@ -19,6 +19,7 @@ import {
19
19
  registerNotificationSink,
20
20
  } from "./rpc-notifications";
21
21
  import { isPidAlive, parseRpcPortFile, rpcPortDir, rpcPortFilePath } from "./rpc-utils";
22
+ import { shouldEnforcePrivateStoragePermissions } from "./storage-permissions";
22
23
 
23
24
  type RpcHandler = (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
24
25
 
@@ -152,16 +153,20 @@ export class MagicContextRpcServer {
152
153
  try {
153
154
  this.warnIfOtherLiveInstance();
154
155
  const dir = dirname(this.portFilePath);
155
- // The port file holds the per-process bearer token that gates
156
- // side-effecting RPC endpoints and the push channel. Under the default
157
- // umask 0o022 a plain write lands at 0o644 in a 0o755 dir —
158
- // world-readable, so any local UID could read the token. Restrict both:
159
- // dir 0o700, file 0o600.
160
- mkdirSync(dir, { recursive: true, mode: 0o700 });
161
- try {
162
- chmodSync(dir, 0o700);
163
- } catch {
164
- // best-effort
156
+ // The port file carries the RPC bearer token. The normal policy keeps
157
+ // it owner-only; a trusted-group deployment explicitly delegates every
158
+ // storage mode to its operator, so this path must not chmod or supply a
159
+ // restrictive creation mode in that case.
160
+ const enforcePrivatePermissions = shouldEnforcePrivateStoragePermissions();
161
+ if (enforcePrivatePermissions) {
162
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
163
+ try {
164
+ chmodSync(dir, 0o700);
165
+ } catch {
166
+ // Continue RPC startup when directory tightening is rejected.
167
+ }
168
+ } else {
169
+ mkdirSync(dir, { recursive: true });
165
170
  }
166
171
  const tmpPath = `${this.portFilePath}.tmp`;
167
172
  // A stale tmp from a crashed write could exist with loose perms;
@@ -171,9 +176,9 @@ export class MagicContextRpcServer {
171
176
  } catch {
172
177
  // best-effort
173
178
  }
174
- // Synchronous write so the renameSync below sees a fully-written file
175
- // (a 0o600 mode keeps the bearer token out of world-readable reach;
176
- // renameSync preserves the tmp's mode for the final path).
179
+ // Synchronous write so the renameSync below sees a fully-written file.
180
+ // The private mode keeps the bearer token out of other local accounts;
181
+ // externally managed storage intentionally leaves its mode to the umask.
177
182
  writeFileSync(
178
183
  tmpPath,
179
184
  JSON.stringify({
@@ -183,13 +188,17 @@ export class MagicContextRpcServer {
183
188
  token: this.token,
184
189
  instance_id: this.instanceId,
185
190
  }),
186
- { encoding: "utf-8", mode: 0o600 },
191
+ enforcePrivatePermissions
192
+ ? { encoding: "utf-8", mode: 0o600 }
193
+ : { encoding: "utf-8" },
187
194
  );
188
195
  renameSync(tmpPath, this.portFilePath);
189
- try {
190
- chmodSync(this.portFilePath, 0o600);
191
- } catch {
192
- // best-effort
196
+ if (enforcePrivatePermissions) {
197
+ try {
198
+ chmodSync(this.portFilePath, 0o600);
199
+ } catch {
200
+ // Continue RPC startup when port-file tightening is rejected.
201
+ }
193
202
  }
194
203
  log(`[rpc] server listening on 127.0.0.1:${this.port}`);
195
204
  } catch (err) {