@cortexkit/opencode-magic-context 0.33.1 → 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.
- package/dist/config/agent-disable.d.ts +22 -0
- package/dist/config/agent-disable.d.ts.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +7 -0
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +82 -2
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/config/transform-mode.d.ts +2 -0
- package/dist/config/transform-mode.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-n48c783v.js → conflict-warning-hook-fvpfvt0w.js} +1 -1
- package/dist/{derive-budgets-e9j09sjr.js → derive-budgets-9crp0cvd.js} +1 -1
- package/dist/features/builtin-commands/commands.d.ts +1 -1
- package/dist/features/builtin-commands/commands.d.ts.map +1 -1
- package/dist/features/magic-context/compaction-marker.d.ts +57 -0
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/fail-closed-block.d.ts +10 -0
- package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/storage-git-commits.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts +7 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
- package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity.d.ts +2 -6
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
- package/dist/features/magic-context/mural/render-mural.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts +8 -1
- package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/schema-fence-probe.d.ts +28 -0
- package/dist/features/magic-context/schema-fence-probe.d.ts.map +1 -0
- package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +35 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +38 -4
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +1 -1
- package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +1 -1
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
- package/dist/hooks/magic-context/boundary-execution.d.ts +1 -1
- package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
- package/dist/hooks/magic-context/cache-busting-signals.d.ts +1 -0
- package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
- package/dist/hooks/magic-context/child-session-spawn.d.ts +26 -0
- package/dist/hooks/magic-context/child-session-spawn.d.ts.map +1 -0
- package/dist/hooks/magic-context/command-handler.d.ts +2 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-off-transition.d.ts +96 -0
- package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -0
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -0
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts +12 -5
- package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +47 -1
- package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts +2 -2
- package/dist/hooks/magic-context/event-handler.d.ts +7 -0
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts +13 -21
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts +1 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +11 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +9 -0
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +3 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/send-session-notification.d.ts +20 -1
- package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -0
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +30 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +27 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-ey1bc14z.js → index-b563sga5.js} +7 -264
- package/dist/{index-y1c9tcm3.js → index-cn960ffd.js} +101 -20
- package/dist/{index-xyaekybw.js → index-dtas91s5.js} +6 -4
- package/dist/index-fcyvznch.js +279 -0
- package/dist/{index-63dq2g4d.js → index-ry8rh8cv.js} +138 -24
- package/dist/{index-c2gahrks.js → index-s0mdn690.js} +1599 -892
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1292 -623
- package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +11 -0
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts +10 -0
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +3 -7
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/tool-registry.d.ts +8 -0
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-2yb2hfh2.js → recomp-orchestrator-8e82vmvb.js} +6 -4
- package/dist/send-session-notification-6njqewzk.js +20 -0
- package/dist/shared/conflict-detector.d.ts +38 -1
- package/dist/shared/conflict-detector.d.ts.map +1 -1
- package/dist/shared/conflict-fixer.d.ts +18 -1
- package/dist/shared/conflict-fixer.d.ts.map +1 -1
- package/dist/shared/context-limit-provenance.d.ts +3 -0
- package/dist/shared/context-limit-provenance.d.ts.map +1 -0
- package/dist/shared/escalation-bands.d.ts +9 -0
- package/dist/shared/escalation-bands.d.ts.map +1 -0
- package/dist/shared/format-threshold.d.ts +1 -1
- package/dist/shared/models-dev-cache.d.ts +29 -3
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +46 -1
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/storage-permissions.d.ts +5 -0
- package/dist/shared/storage-permissions.d.ts.map +1 -0
- package/dist/{storage-meta-session-td6bwtky.js → storage-meta-session-xt41zbw4.js} +2 -1
- package/dist/tui/compaction-off.d.ts +13 -0
- package/dist/tui/compaction-off.d.ts.map +1 -0
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/dist/tui-compiled/compaction-off.d.ts +13 -0
- package/dist/tui-compiled/compaction-off.d.ts.map +1 -0
- package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
- package/package.json +7 -2
- package/src/agents/dreamer.ts +68 -0
- package/src/agents/hidden-agent-registrations.ts +331 -0
- package/src/agents/historian.ts +3 -0
- package/src/agents/language-directive.test.ts +138 -0
- package/src/agents/language-directive.ts +121 -0
- package/src/agents/magic-context-prompt.language.test.ts +35 -0
- package/src/agents/magic-context-prompt.test.ts +292 -0
- package/src/agents/magic-context-prompt.ts +182 -0
- package/src/agents/permissions.test.ts +252 -0
- package/src/agents/permissions.ts +186 -0
- package/src/agents/sidekick.ts +1 -0
- package/src/agents/smart-note-compiler.ts +1 -0
- package/src/config/agent-disable.ts +106 -0
- package/src/config/compaction-accessor-guard.test.ts +82 -0
- package/src/config/compaction-backcompat.test.ts +52 -0
- package/src/config/index.test.ts +904 -0
- package/src/config/index.ts +601 -0
- package/src/config/migrate-config-location.test.ts +332 -0
- package/src/config/migrate-config-location.ts +635 -0
- package/src/config/migrate-dreamer-v2.test.ts +287 -0
- package/src/config/migrate-dreamer-v2.ts +336 -0
- package/src/config/migrate-experimental.ts +159 -0
- package/src/config/project-security.test.ts +366 -0
- package/src/config/project-security.ts +588 -0
- package/src/config/prune-config-leaf.test.ts +78 -0
- package/src/config/prune-config-leaf.ts +68 -0
- package/src/config/removed-config-keys.test.ts +116 -0
- package/src/config/schema/agent-overrides.ts +45 -0
- package/src/config/schema/magic-context.test.ts +358 -0
- package/src/config/schema/magic-context.ts +1002 -0
- package/src/config/transform-mode.test.ts +62 -0
- package/src/config/transform-mode.ts +34 -0
- package/src/config/variable.test.ts +323 -0
- package/src/config/variable.ts +192 -0
- package/src/features/magic-context/defaults.ts +1 -0
- package/src/features/magic-context/dreamer/cron.ts +243 -0
- package/src/features/magic-context/dreamer/task-registry.ts +104 -0
- package/src/shared/conflict-detector.test.ts +121 -0
- package/src/shared/conflict-detector.ts +50 -3
- package/src/shared/conflict-fixer.test.ts +309 -0
- package/src/shared/conflict-fixer.ts +82 -5
- package/src/shared/context-limit-provenance.ts +6 -0
- package/src/shared/escalation-bands.test.ts +23 -0
- package/src/shared/escalation-bands.ts +18 -0
- package/src/shared/format-threshold.ts +1 -1
- package/src/shared/models-dev-cache.test.ts +171 -0
- package/src/shared/models-dev-cache.ts +195 -38
- package/src/shared/rpc-server.ts +27 -18
- package/src/shared/rpc-types.ts +46 -1
- package/src/shared/storage-permissions.ts +20 -0
- package/src/tui/compaction-off.test.ts +56 -0
- package/src/tui/compaction-off.ts +29 -0
- package/src/tui/data/context-db.ts +6 -0
- package/src/tui/index.tsx +113 -65
- package/src/tui/slots/sidebar-content.tsx +139 -83
- package/src/tui-compiled/compaction-off.ts +29 -0
- package/src/tui-compiled/data/context-db.ts +6 -0
- package/src/tui-compiled/index.tsx +558 -455
- package/src/tui-compiled/slots/sidebar-content.tsx +237 -161
- package/dist/send-session-notification-tghq9gnh.js +0 -11
- /package/dist/{index-v7fmc1s2.js → index-ygcng9t8.js} +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
238
|
+
* Resolve the usable prompt budget from raw provider metadata.
|
|
155
239
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
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(
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
return
|
|
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?:
|
|
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
|
|
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
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
if (
|
|
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
|
-
|
|
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?:
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
|
373
|
-
|
|
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
|
|
573
|
+
function lookupMetadataWithTagFallback(
|
|
415
574
|
cache: Map<string, CachedModelMetadata> | null,
|
|
416
575
|
providerID: string,
|
|
417
576
|
modelID: string,
|
|
418
|
-
):
|
|
577
|
+
): CachedModelMetadata | undefined {
|
|
419
578
|
if (!cache) return undefined;
|
|
420
|
-
const exact = cache.get(`${providerID}/${modelID}`)
|
|
421
|
-
if (
|
|
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
|
-
|
|
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
|
}
|
package/src/shared/rpc-server.ts
CHANGED
|
@@ -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
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
//
|
|
176
|
-
//
|
|
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
|
-
|
|
191
|
+
enforcePrivatePermissions
|
|
192
|
+
? { encoding: "utf-8", mode: 0o600 }
|
|
193
|
+
: { encoding: "utf-8" },
|
|
187
194
|
);
|
|
188
195
|
renameSync(tmpPath, this.portFilePath);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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) {
|