@retaw106/mmc-oc 1.2.3 → 2.0.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/cli.js +7095 -0
- package/dist/config/index.d.ts +0 -12
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/legacy-harness-blocks.d.ts +27 -0
- package/dist/config/legacy-harness-blocks.d.ts.map +1 -0
- package/dist/config/live-run-config.d.ts.map +1 -1
- package/dist/config/live-snapshot.d.ts +2 -1
- package/dist/config/live-snapshot.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +4 -5
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/raw-loader.d.ts +7 -26
- package/dist/config/raw-loader.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +164 -259
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/config/variable.d.ts +8 -0
- package/dist/config/variable.d.ts.map +1 -1
- package/dist/config-init.js +5595 -0
- package/dist/{context-a6px04s0.js → context-95zkksn3.js} +2471 -2438
- package/dist/{derive-budgets-fhk00zv8.js → derive-budgets-rjjz5f7y.js} +2 -2
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +2 -0
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migration-v3-perf-schema.d.ts +8 -0
- package/dist/features/magic-context/migration-v3-perf-schema.d.ts.map +1 -0
- package/dist/features/magic-context/migration-v4-temporal-decisions.d.ts +12 -0
- package/dist/features/magic-context/migration-v4-temporal-decisions.d.ts.map +1 -0
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts +6 -0
- package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
- package/dist/features/magic-context/reclaim-protection.d.ts +15 -10
- package/dist/features/magic-context/reclaim-protection.d.ts.map +1 -1
- package/dist/features/magic-context/session-cache-ttl.d.ts +4 -2
- package/dist/features/magic-context/session-cache-ttl.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +1 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-historian-runs.d.ts +2 -0
- package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -1
- package/dist/features/magic-context/storage-ops.d.ts +2 -0
- package/dist/features/magic-context/storage-ops.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +15 -2
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +2 -2
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/temporal-decisions.d.ts +10 -0
- package/dist/features/magic-context/temporal-decisions.d.ts.map +1 -0
- package/dist/features/magic-context/tool-definition-tokens.d.ts +3 -0
- package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -1
- package/dist/hooks/magic-context/active-anthropic-turn.d.ts +8 -0
- package/dist/hooks/magic-context/active-anthropic-turn.d.ts.map +1 -0
- package/dist/hooks/magic-context/apply-operations.d.ts +15 -0
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-deadline.d.ts +10 -3
- package/dist/hooks/magic-context/auto-search-deadline.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/caveman-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-cycle.d.ts +2 -0
- package/dist/hooks/magic-context/channel2-cycle.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-render-epoch.d.ts +8 -0
- package/dist/hooks/magic-context/compartment-render-epoch.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 +3 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +12 -0
- package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -1
- package/dist/hooks/magic-context/decision-calibration.d.ts +3 -0
- package/dist/hooks/magic-context/decision-calibration.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts +2 -0
- package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-fail-closed.d.ts +21 -1
- package/dist/hooks/magic-context/emergency-fail-closed.d.ts.map +1 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +30 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts +2 -0
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +10 -8
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/latest-assistant-turn.d.ts +25 -0
- package/dist/hooks/magic-context/latest-assistant-turn.d.ts.map +1 -0
- package/dist/hooks/magic-context/lkg-measured-request.d.ts +69 -0
- package/dist/hooks/magic-context/lkg-measured-request.d.ts.map +1 -0
- package/dist/hooks/magic-context/read-session-formatting.d.ts +3 -0
- package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
- package/dist/hooks/magic-context/reasoning-budget-status.d.ts +15 -0
- package/dist/hooks/magic-context/reasoning-budget-status.d.ts.map +1 -0
- package/dist/hooks/magic-context/reasoning-budget.d.ts +40 -0
- package/dist/hooks/magic-context/reasoning-budget.d.ts.map +1 -0
- package/dist/hooks/magic-context/reasoning-removal.d.ts +6 -6
- package/dist/hooks/magic-context/reasoning-removal.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +2 -0
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
- package/dist/hooks/magic-context/sentinel.d.ts +2 -0
- package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +7 -3
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/supersession-reclaim.d.ts +4 -0
- package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +13 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +5 -0
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
- package/dist/hooks/magic-context/temporal-awareness.d.ts +6 -3
- package/dist/hooks/magic-context/temporal-awareness.d.ts.map +1 -1
- package/dist/hooks/magic-context/temporal-served-projection.d.ts +4 -0
- package/dist/hooks/magic-context/temporal-served-projection.d.ts.map +1 -0
- package/dist/hooks/magic-context/tool-reclaim.d.ts +2 -0
- package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +34 -12
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +11 -5
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
- package/dist/{index-wgt4gamw.js → index-1mjhzqq7.js} +422 -31
- package/dist/index-33gprt9g.js +30 -0
- package/dist/{index-zx63j2e2.js → index-5cep2r9n.js} +3 -2
- package/dist/{index-by6h2pav.js → index-bdt9my9y.js} +374 -239
- package/dist/{index-3wagyrnj.js → index-x37vg474.js} +5 -1
- package/dist/{index-1vs6anwz.js → index-xt7y10x6.js} +2 -1
- package/dist/{index-me3zcj5t.js → index-z2g20msd.js} +4 -8
- package/dist/index.js +1 -1
- package/dist/{message-time-backfill-5ewk0axe.js → message-time-backfill-zfernxbw.js} +1 -1
- package/dist/{model-resolution-5haaxnpt.js → model-resolution-wazv26n0.js} +1 -1
- package/dist/plugin/rpc-handlers.d.ts +4 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-apjwrttc.js → read-session-chunk-hp464g13.js} +2 -2
- package/dist/{recomp-orchestrator-gyeevxaw.js → recomp-orchestrator-3qxeqwb9.js} +6 -6
- package/dist/shared/auto-search-lifecycle.d.ts +7 -0
- package/dist/shared/auto-search-lifecycle.d.ts.map +1 -0
- package/dist/shared/cache-ttl-display.d.ts +6 -4
- package/dist/shared/cache-ttl-display.d.ts.map +1 -1
- package/dist/shared/model-cache-ttl.d.ts +1 -1
- package/dist/shared/model-cache-ttl.d.ts.map +1 -1
- package/dist/shared/model-resolution.d.ts +5 -2
- package/dist/shared/model-resolution.d.ts.map +1 -1
- package/dist/shared/model-suggestion-retry.d.ts +2 -0
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/protected-tools-policy.d.ts +6 -0
- package/dist/shared/protected-tools-policy.d.ts.map +1 -0
- package/dist/shared/provider-output-failure.d.ts +19 -0
- package/dist/shared/provider-output-failure.d.ts.map +1 -0
- package/dist/shared/temporal-upgrade-fixture.d.ts +19 -0
- package/dist/shared/temporal-upgrade-fixture.d.ts.map +1 -0
- package/dist/shared/window-geometry.d.ts +4 -0
- package/dist/shared/window-geometry.d.ts.map +1 -1
- package/dist/shared/write-transaction-timing.d.ts +8 -1
- package/dist/shared/write-transaction-timing.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts +2 -0
- package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
- package/dist/v2/hidden-child-native.d.ts +7 -0
- package/dist/v2/hidden-child-native.d.ts.map +1 -1
- package/dist/v2/hidden-child-record.d.ts +13 -0
- package/dist/v2/hidden-child-record.d.ts.map +1 -1
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +46 -2
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/hooks/payload.d.ts.map +1 -1
- package/dist/v2/hooks/store.d.ts.map +1 -1
- package/dist/v2/hooks/tools.d.ts.map +1 -1
- package/dist/v2/hooks/types.d.ts +8 -0
- package/dist/v2/hooks/types.d.ts.map +1 -1
- package/dist/v2/server.js +4540 -3889
- package/dist/v2/tool-result.d.ts +8 -0
- package/dist/v2/tool-result.d.ts.map +1 -0
- package/dist/web/api.d.ts +6 -0
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/context-store.d.ts +13 -0
- package/dist/web/context-store.d.ts.map +1 -1
- package/dist/web/display-path.d.ts +28 -0
- package/dist/web/display-path.d.ts.map +1 -0
- package/dist/web/entry-args.d.ts +33 -0
- package/dist/web/entry-args.d.ts.map +1 -0
- package/dist/web/host-pi.d.ts +2 -0
- package/dist/web/host-pi.d.ts.map +1 -1
- package/dist/web/main.d.ts +4 -6
- package/dist/web/main.d.ts.map +1 -1
- package/dist/web/main.js +772 -77
- package/dist/web/message-stats.d.ts +45 -0
- package/dist/web/message-stats.d.ts.map +1 -0
- package/dist/web/ui/index.html +1 -0
- package/dist/web/ui/panel.css +5 -0
- package/dist/web/ui/panel.js +220 -70
- package/dist/web/views.d.ts +23 -0
- package/dist/web/views.d.ts.map +1 -1
- package/dist/{wrapup-orchestrator-z9sqpqjf.js → wrapup-orchestrator-wgb2nd19.js} +7 -6
- package/package.json +7 -2
- package/src/config/index.test.ts +257 -126
- package/src/config/index.ts +45 -39
- package/src/config/legacy-harness-blocks.test.ts +78 -0
- package/src/config/legacy-harness-blocks.ts +71 -0
- package/src/config/live-run-config.test.ts +49 -5
- package/src/config/live-run-config.ts +1 -0
- package/src/config/live-snapshot.ts +5 -1
- package/src/config/profiles.ts +1 -1
- package/src/config/project-config-trust-boundary.test.ts +2 -2
- package/src/config/project-security.hostile-matrix.test.ts +51 -65
- package/src/config/project-security.test.ts +6 -1
- package/src/config/project-security.ts +13 -12
- package/src/config/protected-tokens.test.ts +1 -1
- package/src/config/raw-loader.test.ts +29 -352
- package/src/config/raw-loader.ts +17 -406
- package/src/config/removed-config-keys.test.ts +1 -1
- package/src/config/schema/magic-context.test.ts +60 -64
- package/src/config/schema/magic-context.ts +105 -137
- package/src/config/variable.ts +21 -12
- package/src/shared/auto-search-lifecycle.ts +27 -0
- package/src/shared/cache-ttl-display.test.ts +1 -1
- package/src/shared/cache-ttl-display.ts +17 -16
- package/src/shared/model-cache-ttl.test.ts +2 -2
- package/src/shared/model-cache-ttl.ts +8 -4
- package/src/shared/model-resolution.test.ts +22 -32
- package/src/shared/model-resolution.ts +13 -18
- package/src/shared/model-suggestion-retry.ts +63 -7
- package/src/shared/protected-tools-policy.ts +25 -0
- package/src/shared/provider-output-failure.ts +176 -0
- package/src/shared/status-detail-text.test.ts +1 -1
- package/src/shared/status-view.test.ts +1 -1
- package/src/shared/temporal-upgrade-fixture.ts +50 -0
- package/src/shared/window-geometry.ts +10 -0
- package/src/shared/write-transaction-timing.ts +25 -1
- package/dist/features/magic-context/window-report-ledger.d.ts +0 -70
- package/dist/features/magic-context/window-report-ledger.d.ts.map +0 -1
- package/dist/index-e1q8002h.js +0 -16
- package/src/config/misplaced-agent-model.test.ts +0 -55
package/src/config/index.ts
CHANGED
|
@@ -16,6 +16,11 @@ import type { PromptSurfaceConfig } from "../shared/prompt-surface";
|
|
|
16
16
|
import { setWindowOverlayPath } from "../shared/window-geometry";
|
|
17
17
|
import { migrateLegacyAgentEnabledInMemory } from "./agent-disable";
|
|
18
18
|
import { mmcProjectConfigBasePath, opencodeUserConfigBasePath } from "./config-paths";
|
|
19
|
+
import {
|
|
20
|
+
legacyHarnessBlockPaths,
|
|
21
|
+
RemovedHistorianHarnessBlockError,
|
|
22
|
+
removedHistorianBlockMessage,
|
|
23
|
+
} from "./legacy-harness-blocks";
|
|
19
24
|
import { migrateLegacyExperimental } from "./migrate-experimental";
|
|
20
25
|
import { resolveConfigProfile } from "./profiles";
|
|
21
26
|
import {
|
|
@@ -150,6 +155,41 @@ function loadConfigFileDetailed(
|
|
|
150
155
|
`Ignored unsafe config key "${path}" (security: prototype-pollution keys are not allowed).`,
|
|
151
156
|
);
|
|
152
157
|
const firstIssue = parsed.issues[0];
|
|
158
|
+
// A removed per-harness historian block is a hard config error: reject the
|
|
159
|
+
// whole file (parse-failure → defaults at boot / last-known-good on reload)
|
|
160
|
+
// instead of letting Zod strip the block and leave the historian with no
|
|
161
|
+
// model. Detection is by key presence, so it also fires for a project file
|
|
162
|
+
// whose block was emptied by tier hardening. It runs on the RECOVERED
|
|
163
|
+
// object too — a file with a JSONC syntax error must still not let a legacy
|
|
164
|
+
// block slip through to the parser, which would throw uncaught.
|
|
165
|
+
const legacyPaths = legacyHarnessBlockPaths(config);
|
|
166
|
+
if (legacyPaths.length > 0) {
|
|
167
|
+
const message = removedHistorianBlockMessage(legacyPaths);
|
|
168
|
+
const warning = `${configPath}:1:1: ${message}`;
|
|
169
|
+
const failure: ConfigParseFailure = {
|
|
170
|
+
warningClass: CONFIG_WARNING_CLASS.FILE_PARSE,
|
|
171
|
+
source,
|
|
172
|
+
path: configPath,
|
|
173
|
+
line: 1,
|
|
174
|
+
column: 1,
|
|
175
|
+
message,
|
|
176
|
+
recovered: false,
|
|
177
|
+
warning,
|
|
178
|
+
};
|
|
179
|
+
return {
|
|
180
|
+
config: {},
|
|
181
|
+
warnings: [
|
|
182
|
+
warning,
|
|
183
|
+
...rawWarnings.map((w) => `${configPath}: ${w}`),
|
|
184
|
+
...substituted.warnings.map((w) => `${configPath}: ${w}`),
|
|
185
|
+
...unsafeKeyWarnings.map((w) => `${configPath}: ${w}`),
|
|
186
|
+
],
|
|
187
|
+
parseFailures: [failure],
|
|
188
|
+
warningDetails: [failure],
|
|
189
|
+
outcome: "project-file-parse-error",
|
|
190
|
+
source,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
153
193
|
const recovered = firstIssue !== undefined && Object.keys(config).length > 0;
|
|
154
194
|
const parseFailures: ConfigParseFailure[] = firstIssue
|
|
155
195
|
? [
|
|
@@ -312,53 +352,19 @@ export function formatProtectedTokensBelowMinWarning(value: number): string {
|
|
|
312
352
|
return `protected_tokens is a token floor (minimum ${PROTECTED_TOKENS_MIN}, default derived from the context window); ${value} looks like a leftover tag-count value rather than a token floor. Remove the key to use the default, or set a token count such as 16000.`;
|
|
313
353
|
}
|
|
314
354
|
|
|
315
|
-
/** The harness blocks the historian model resolver actually reads. */
|
|
316
|
-
const MODEL_HARNESS_BLOCKS = ["opencode", "pi"] as const;
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Warn when an agent's model is configured somewhere the resolver never looks.
|
|
320
|
-
*
|
|
321
|
-
* Agent models are resolved per harness: `historian.opencode.model` and
|
|
322
|
-
* `historian.pi.model`. A bare `historian.model` is schema-valid and reads
|
|
323
|
-
* naturally, but nothing consumes it —
|
|
324
|
-
* `resolveHistorianModel({historian:{model}}, "opencode")` returns no models at
|
|
325
|
-
* all, and the only outward sign is a historian that silently never runs. Saying
|
|
326
|
-
* so at load time is the difference between a five-minute fix and a subsystem
|
|
327
|
-
* that looks configured and is not.
|
|
328
|
-
*/
|
|
329
|
-
export function misplacedAgentModelWarnings(rawConfig: Record<string, unknown>): string[] {
|
|
330
|
-
const warnings: string[] = [];
|
|
331
|
-
for (const agent of ["historian"]) {
|
|
332
|
-
const block = rawConfig[agent];
|
|
333
|
-
if (!block || typeof block !== "object" || Array.isArray(block)) continue;
|
|
334
|
-
const record = block as Record<string, unknown>;
|
|
335
|
-
if (!Object.hasOwn(record, "model")) continue;
|
|
336
|
-
const harnessWithModel = MODEL_HARNESS_BLOCKS.find((harness) => {
|
|
337
|
-
const sub = record[harness];
|
|
338
|
-
return (
|
|
339
|
-
sub !== null &&
|
|
340
|
-
typeof sub === "object" &&
|
|
341
|
-
!Array.isArray(sub) &&
|
|
342
|
-
Object.hasOwn(sub as Record<string, unknown>, "model")
|
|
343
|
-
);
|
|
344
|
-
});
|
|
345
|
-
if (harnessWithModel) continue;
|
|
346
|
-
warnings.push(
|
|
347
|
-
`${agent}.model is not read: the ${agent} model is resolved per harness, so it has to be ${agent}.opencode.model (OpenCode 1 and 2) or ${agent}.pi.model. As written the ${agent} resolves to no models and never runs.`,
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
return warnings;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
355
|
export function parsePluginConfig(
|
|
354
356
|
rawConfig: Record<string, unknown>,
|
|
355
357
|
recoveredTopLevelKeys: string[] = [],
|
|
356
358
|
): MagicContextPluginConfig & { configWarnings?: string[] } {
|
|
359
|
+
// Reject — do not silently strip — a removed per-harness historian block. The
|
|
360
|
+
// loaders already reject such files before parsing; this makes the rejection
|
|
361
|
+
// explicit for direct callers and tests (never reached from a real load).
|
|
362
|
+
const legacyPaths = legacyHarnessBlockPaths(rawConfig);
|
|
363
|
+
if (legacyPaths.length > 0) throw new RemovedHistorianHarnessBlockError(legacyPaths);
|
|
357
364
|
// Pre-Zod shim: reshape legacy experimental.* graduated keys so the user's
|
|
358
365
|
// opt-in/out state survives upgrades even when they never run `doctor`.
|
|
359
366
|
const preMigrationWarnings: string[] = [];
|
|
360
367
|
const configWithoutRemovedAgent = stripRemovedAgentConfig(rawConfig, preMigrationWarnings);
|
|
361
|
-
preMigrationWarnings.push(...misplacedAgentModelWarnings(rawConfig));
|
|
362
368
|
const migratedExperimental = migrateLegacyExperimental(
|
|
363
369
|
configWithoutRemovedAgent,
|
|
364
370
|
preMigrationWarnings,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/// <reference types="bun-types" />
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import { resolveHistorianModel } from "../shared/model-resolution";
|
|
5
|
+
import { parsePluginConfig } from "./index";
|
|
6
|
+
import {
|
|
7
|
+
legacyHarnessBlockPaths,
|
|
8
|
+
RemovedHistorianHarnessBlockError,
|
|
9
|
+
removedHistorianBlockMessage,
|
|
10
|
+
} from "./legacy-harness-blocks";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The flat `historian.model` is the only supported shape. The removed
|
|
14
|
+
* `historian.opencode` / `historian.pi` blocks (and their profile equivalents)
|
|
15
|
+
* are a hard config error: the loader rejects the whole file (defaults at boot,
|
|
16
|
+
* last-known-good on live reload) instead of letting Zod strip the block and
|
|
17
|
+
* leave the historian resolving to no model.
|
|
18
|
+
*/
|
|
19
|
+
describe("removed per-harness historian blocks", () => {
|
|
20
|
+
it("resolves the flat historian.model", () => {
|
|
21
|
+
expect(resolveHistorianModel({ historian: { model: "openai/gpt-5" } }, "opencode")).toEqual(
|
|
22
|
+
{
|
|
23
|
+
primary: { model: "openai/gpt-5" },
|
|
24
|
+
fallbacks: [],
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("lists the exact paths of removed historian harness blocks", () => {
|
|
30
|
+
expect(
|
|
31
|
+
legacyHarnessBlockPaths({ historian: { opencode: { model: "openai/gpt-5" } } }),
|
|
32
|
+
).toEqual(["historian.opencode"]);
|
|
33
|
+
|
|
34
|
+
expect(
|
|
35
|
+
legacyHarnessBlockPaths({
|
|
36
|
+
historian: { opencode: { model: "x/y" }, pi: { model: "x/y" } },
|
|
37
|
+
}),
|
|
38
|
+
).toEqual(["historian.opencode", "historian.pi"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("lists removed profile historian blocks by profile name", () => {
|
|
42
|
+
expect(
|
|
43
|
+
legacyHarnessBlockPaths({
|
|
44
|
+
profiles: {
|
|
45
|
+
work: { historian: { pi: { model: "x/y" } } },
|
|
46
|
+
personal: { historian: { opencode: { model: "x/y" } } },
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
).toEqual(["profiles.work.historian.pi", "profiles.personal.historian.opencode"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("stays empty for the flat shape and unrelated config", () => {
|
|
53
|
+
expect(legacyHarnessBlockPaths({ historian: { model: "openai/gpt-5" } })).toEqual([]);
|
|
54
|
+
expect(legacyHarnessBlockPaths({})).toEqual([]);
|
|
55
|
+
expect(legacyHarnessBlockPaths({ historian: { disable: true } })).toEqual([]);
|
|
56
|
+
expect(
|
|
57
|
+
legacyHarnessBlockPaths({ profiles: { work: { historian: { model: "x/y" } } } }),
|
|
58
|
+
).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("names every offending path and the flat replacement", () => {
|
|
62
|
+
const message = removedHistorianBlockMessage(["historian.opencode"]);
|
|
63
|
+
expect(message).toContain("historian.opencode");
|
|
64
|
+
expect(message).toContain("historian.model");
|
|
65
|
+
expect(message).toContain("historian.fallback_models");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("rejects a raw config that still carries the removed blocks", () => {
|
|
69
|
+
expect(() =>
|
|
70
|
+
parsePluginConfig({ historian: { opencode: { model: "openai/gpt-5" } } }),
|
|
71
|
+
).toThrow(RemovedHistorianHarnessBlockError);
|
|
72
|
+
expect(() =>
|
|
73
|
+
parsePluginConfig({
|
|
74
|
+
profiles: { work: { historian: { pi: { model: "x/y" } } } },
|
|
75
|
+
}),
|
|
76
|
+
).toThrow(/profiles\.work\.historian\.pi/);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { LEGACY_HARNESS_BLOCK_KEYS } from "./schema/magic-context";
|
|
2
|
+
|
|
3
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Exact dotted paths of the removed per-harness historian model blocks anywhere
|
|
9
|
+
* in a raw config: the top-level `historian.opencode` / `historian.pi`, and the
|
|
10
|
+
* same keys inside profile definitions
|
|
11
|
+
* (`profiles.<name>.historian.opencode` / `.pi`).
|
|
12
|
+
*
|
|
13
|
+
* These blocks are no longer part of the schema. The loaders use this to REJECT
|
|
14
|
+
* the whole config file (a parse failure → defaults at boot, last-known-good on
|
|
15
|
+
* live reload) instead of letting Zod silently strip the block and leave the
|
|
16
|
+
* historian with no model — a subsystem that looks configured but never runs.
|
|
17
|
+
* Detection is by key presence, so it still fires after project-tier hardening
|
|
18
|
+
* empties (but does not remove) the block.
|
|
19
|
+
*/
|
|
20
|
+
export function legacyHarnessBlockPaths(rawConfig: unknown): string[] {
|
|
21
|
+
if (!isPlainObject(rawConfig)) return [];
|
|
22
|
+
const paths: string[] = [];
|
|
23
|
+
|
|
24
|
+
const historian = rawConfig.historian;
|
|
25
|
+
if (isPlainObject(historian)) {
|
|
26
|
+
for (const key of LEGACY_HARNESS_BLOCK_KEYS) {
|
|
27
|
+
if (key in historian) paths.push(`historian.${key}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const profiles = rawConfig.profiles;
|
|
32
|
+
if (isPlainObject(profiles)) {
|
|
33
|
+
for (const [name, profile] of Object.entries(profiles)) {
|
|
34
|
+
if (!isPlainObject(profile)) continue;
|
|
35
|
+
const profileHistorian = profile.historian;
|
|
36
|
+
if (!isPlainObject(profileHistorian)) continue;
|
|
37
|
+
for (const key of LEGACY_HARNESS_BLOCK_KEYS) {
|
|
38
|
+
if (key in profileHistorian) paths.push(`profiles.${name}.historian.${key}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return paths;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** User-facing message for a rejected config, naming every offending path. */
|
|
47
|
+
export function removedHistorianBlockMessage(paths: readonly string[]): string {
|
|
48
|
+
return (
|
|
49
|
+
`removed per-harness historian block(s) ${paths.join(", ")}: historian model ` +
|
|
50
|
+
"resolution is now flat. Use historian.model for the primary model, " +
|
|
51
|
+
"historian.fallback_models for fallbacks, and historian.variant (OpenCode) / " +
|
|
52
|
+
"historian.thinking_level (Pi). This config is rejected — running on defaults, " +
|
|
53
|
+
"or keeping the last known-good config on a live reload."
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Thrown by the config parse functions when a raw config still carries a removed
|
|
59
|
+
* per-harness historian block. The loaders reject such files before parsing, so a
|
|
60
|
+
* real config load never reaches this; it makes the rejection explicit and
|
|
61
|
+
* directly observable for callers and tests.
|
|
62
|
+
*/
|
|
63
|
+
export class RemovedHistorianHarnessBlockError extends Error {
|
|
64
|
+
readonly paths: readonly string[];
|
|
65
|
+
|
|
66
|
+
constructor(paths: readonly string[]) {
|
|
67
|
+
super(removedHistorianBlockMessage(paths));
|
|
68
|
+
this.name = "RemovedHistorianHarnessBlockError";
|
|
69
|
+
this.paths = paths;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -20,7 +20,6 @@ for (const host of ["OC1", "OC2", "Pi"] as const) {
|
|
|
20
20
|
mkdirSync(join(root, "config", "opencode"), { recursive: true });
|
|
21
21
|
mkdirSync(join(directory, ".mmc"), { recursive: true });
|
|
22
22
|
try {
|
|
23
|
-
const block = host === "Pi" ? "pi" : "opencode";
|
|
24
23
|
const write = (
|
|
25
24
|
model: string,
|
|
26
25
|
fallback: string,
|
|
@@ -31,7 +30,7 @@ for (const host of ["OC1", "OC2", "Pi"] as const) {
|
|
|
31
30
|
writeFileSync(
|
|
32
31
|
file,
|
|
33
32
|
JSON.stringify({
|
|
34
|
-
historian: { maxTokens,
|
|
33
|
+
historian: { maxTokens, model, fallback_models: [fallback] },
|
|
35
34
|
memory: { auto_promote: autoPromote },
|
|
36
35
|
commit_cluster_trigger: { enabled: true, min_clusters: minClusters },
|
|
37
36
|
}),
|
|
@@ -95,13 +94,13 @@ test("malformed project config retains last good values and deduplicates the war
|
|
|
95
94
|
writeFileSync(
|
|
96
95
|
userFile,
|
|
97
96
|
JSON.stringify({
|
|
98
|
-
historian: {
|
|
97
|
+
historian: { model: "user/historian" },
|
|
99
98
|
}),
|
|
100
99
|
);
|
|
101
100
|
writeFileSync(
|
|
102
101
|
projectFile,
|
|
103
102
|
JSON.stringify({
|
|
104
|
-
historian: {
|
|
103
|
+
historian: { model: "project/historian" },
|
|
105
104
|
}),
|
|
106
105
|
);
|
|
107
106
|
const load = () => loadPluginConfigDetailed(directory, false).config;
|
|
@@ -121,7 +120,7 @@ test("malformed project config retains last good values and deduplicates the war
|
|
|
121
120
|
writeFileSync(
|
|
122
121
|
projectFile,
|
|
123
122
|
JSON.stringify({
|
|
124
|
-
historian: {
|
|
123
|
+
historian: { model: "project/new-historian" },
|
|
125
124
|
}),
|
|
126
125
|
);
|
|
127
126
|
const second = reader.poll();
|
|
@@ -137,3 +136,48 @@ test("malformed project config retains last good values and deduplicates the war
|
|
|
137
136
|
rmSync(root, { recursive: true, force: true });
|
|
138
137
|
}
|
|
139
138
|
});
|
|
139
|
+
|
|
140
|
+
test("a valid config that becomes a removed per-harness historian block keeps the last good values", () => {
|
|
141
|
+
const root = createTestTempDirFromPath(join(tmpdir(), "mc-live-legacy-"));
|
|
142
|
+
const previous = { home: process.env.HOME, config: process.env.XDG_CONFIG_HOME };
|
|
143
|
+
process.env.HOME = root;
|
|
144
|
+
process.env.XDG_CONFIG_HOME = join(root, "config");
|
|
145
|
+
const directory = join(root, "project");
|
|
146
|
+
const userFile = join(root, "config", "opencode", "mmc.jsonc");
|
|
147
|
+
mkdirSync(join(root, "config", "opencode"), { recursive: true });
|
|
148
|
+
mkdirSync(join(directory, ".mmc"), { recursive: true });
|
|
149
|
+
const warnings: string[] = [];
|
|
150
|
+
try {
|
|
151
|
+
writeFileSync(userFile, JSON.stringify({ historian: { model: "user/good" } }));
|
|
152
|
+
// Mirror the production live reader: reject (throw) on an error load outcome.
|
|
153
|
+
const load = () => {
|
|
154
|
+
const loaded = loadPluginConfigDetailed(directory, false);
|
|
155
|
+
if (loaded.loadOutcome === "project-file-parse-error") {
|
|
156
|
+
throw new Error(`invalid configuration: ${loaded.loadOutcome}`);
|
|
157
|
+
}
|
|
158
|
+
return loaded.config;
|
|
159
|
+
};
|
|
160
|
+
const reader = new LiveConfigReader(directory, load(), load, (message) =>
|
|
161
|
+
warnings.push(message),
|
|
162
|
+
);
|
|
163
|
+
const first = reader.poll();
|
|
164
|
+
expect(resolveHistorianModel(first.effective, "opencode").primary?.model).toBe("user/good");
|
|
165
|
+
|
|
166
|
+
// Syntactically valid JSONC that still carries the removed per-harness block.
|
|
167
|
+
writeFileSync(
|
|
168
|
+
userFile,
|
|
169
|
+
JSON.stringify({ historian: { opencode: { model: "user/legacy" } } }),
|
|
170
|
+
);
|
|
171
|
+
expect(reader.poll()).toBe(first);
|
|
172
|
+
expect(resolveHistorianModel(reader.current().effective, "opencode").primary?.model).toBe(
|
|
173
|
+
"user/good",
|
|
174
|
+
);
|
|
175
|
+
expect(warnings.some((message) => message.includes("config reload failed"))).toBe(true);
|
|
176
|
+
} finally {
|
|
177
|
+
if (previous.home === undefined) delete process.env.HOME;
|
|
178
|
+
else process.env.HOME = previous.home;
|
|
179
|
+
if (previous.config === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
180
|
+
else process.env.XDG_CONFIG_HOME = previous.config;
|
|
181
|
+
rmSync(root, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
@@ -6,6 +6,7 @@ import { LIVE_RELOAD_CONFIG_PATHS, type MagicContextConfig } from "./schema/magi
|
|
|
6
6
|
/** Copy only schema-marked live paths from fresh config; other settings retain boot values. */
|
|
7
7
|
export function sampleLiveConfig<T extends MagicContextConfig>(boot: T, fresh: T): T {
|
|
8
8
|
const result = { ...boot } as Record<string, unknown>;
|
|
9
|
+
result.cacheTtlConfigured = fresh.cacheTtlConfigured;
|
|
9
10
|
for (const path of LIVE_RELOAD_CONFIG_PATHS) {
|
|
10
11
|
const parts = path.split(".");
|
|
11
12
|
let value: unknown = fresh;
|
|
@@ -49,6 +49,10 @@ export class LiveConfigReader<T> {
|
|
|
49
49
|
JSON.stringify((previous as Record<string, unknown>)[key]) !==
|
|
50
50
|
JSON.stringify((next as Record<string, unknown>)[key]),
|
|
51
51
|
),
|
|
52
|
+
// Which host's user config file to watch. Defaults to OpenCode; Pi passes
|
|
53
|
+
// its own `~/.pi/agent/mmc` base so Pi user-config edits are observed
|
|
54
|
+
// (each harness reads its own user config directory).
|
|
55
|
+
private readonly userConfigBasePath: () => string = opencodeUserConfigBasePath,
|
|
52
56
|
) {
|
|
53
57
|
this.snapshot = {
|
|
54
58
|
generation: 1,
|
|
@@ -67,7 +71,7 @@ export class LiveConfigReader<T> {
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
poll(): ConfigSnapshot<T> {
|
|
70
|
-
const paths = [
|
|
74
|
+
const paths = [this.userConfigBasePath(), mmcProjectConfigBasePath(this.directory)].map(
|
|
71
75
|
(base) => detectConfigFile(base).path,
|
|
72
76
|
);
|
|
73
77
|
const next = new Map<string, FileState>();
|
package/src/config/profiles.ts
CHANGED
|
@@ -69,7 +69,7 @@ export function resolveConfigProfile(args: {
|
|
|
69
69
|
profiles = parsed.data as Record<string, Record<string, unknown>>;
|
|
70
70
|
} else {
|
|
71
71
|
warnings.push(
|
|
72
|
-
"Ignoring profiles from user config: invalid profile configuration; profiles may contain only historian
|
|
72
|
+
"Ignoring profiles from user config: invalid profile configuration; profiles may contain only the flat historian model-selection fields (model, fallback_models, variant, thinking_level).",
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -45,10 +45,10 @@ describe("project config cannot reset user settings with a wrong-typed parent bl
|
|
|
45
45
|
|
|
46
46
|
it("keeps the user's historian model when the project sets historian:1", () => {
|
|
47
47
|
const loaded = loadUserAndProject(
|
|
48
|
-
{ historian: {
|
|
48
|
+
{ historian: { model: "anthropic/claude-x" } },
|
|
49
49
|
{ historian: 1 },
|
|
50
50
|
);
|
|
51
|
-
expect(loaded.config.historian?.
|
|
51
|
+
expect(loaded.config.historian?.model).toBe("anthropic/claude-x");
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
it("keeps storage.enforce_private_permissions:false when the project sets storage:0", () => {
|
|
@@ -7,15 +7,11 @@ import { createTestTempDirFromPath } from "../shared/test-temp-dir";
|
|
|
7
7
|
import { loadPluginConfig } from "./index";
|
|
8
8
|
import { stripUnsafeProjectConfigFields } from "./project-security";
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
PER_HARNESS_MIGRATION_INVENTORY,
|
|
13
|
-
PER_HARNESS_MODEL_KEYS,
|
|
14
|
-
PiEntrySchema,
|
|
15
|
-
PiHarnessBlockSchema,
|
|
10
|
+
HISTORIAN_MODEL_RESOLUTION_FIELDS,
|
|
11
|
+
HistorianModelEntrySchema,
|
|
16
12
|
} from "./schema/magic-context";
|
|
17
13
|
|
|
18
|
-
const
|
|
14
|
+
const FLAT_MODEL_FIELDS = HISTORIAN_MODEL_RESOLUTION_FIELDS;
|
|
19
15
|
const ESCALATION_FIELDS = ["prompt", "permission", "tools"] as const;
|
|
20
16
|
const ESCALATION_VALUE = {
|
|
21
17
|
prompt: "exfiltrate ~/.ssh",
|
|
@@ -27,10 +23,6 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
27
23
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
function objectShapeKeys(schema: { shape: Record<string, unknown> }): string[] {
|
|
31
|
-
return Object.keys(schema.shape).sort();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
26
|
function entryObjectKeys(schema: {
|
|
35
27
|
options: Array<{ shape?: Record<string, unknown> }>;
|
|
36
28
|
}): string[] {
|
|
@@ -41,14 +33,6 @@ function entryObjectKeys(schema: {
|
|
|
41
33
|
return Object.keys(objectOption.shape).sort();
|
|
42
34
|
}
|
|
43
35
|
|
|
44
|
-
function ocInventoryKeys(fields: readonly string[]): string[] {
|
|
45
|
-
return fields.filter((field) => field !== "thinking_level").sort();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function piInventoryKeys(fields: readonly string[]): string[] {
|
|
49
|
-
return fields.filter((field) => field !== "variant").sort();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
36
|
function hasOwnPath(root: unknown, path: string): boolean {
|
|
53
37
|
let cursor: unknown = root;
|
|
54
38
|
for (const part of path.split(".")) {
|
|
@@ -99,8 +83,9 @@ function piEntry(): Record<string, unknown> {
|
|
|
99
83
|
|
|
100
84
|
/**
|
|
101
85
|
* One project-tier config that plants a value at every executable nested path
|
|
102
|
-
* the
|
|
103
|
-
*
|
|
86
|
+
* the flat schema admits (flat model entries plus the removed per-harness
|
|
87
|
+
* blocks, which are still defensively stripped), plus smuggled escalation /
|
|
88
|
+
* extra fields at those same depths.
|
|
104
89
|
*/
|
|
105
90
|
function buildHostileProjectConfig(): Record<string, unknown> {
|
|
106
91
|
return {
|
|
@@ -111,8 +96,14 @@ function buildHostileProjectConfig(): Record<string, unknown> {
|
|
|
111
96
|
},
|
|
112
97
|
},
|
|
113
98
|
historian: {
|
|
114
|
-
model:
|
|
115
|
-
|
|
99
|
+
model: {
|
|
100
|
+
model: "hostile/flat-historian",
|
|
101
|
+
variant: "flat-variant",
|
|
102
|
+
...ESCALATION_VALUE,
|
|
103
|
+
},
|
|
104
|
+
fallback_models: [
|
|
105
|
+
{ model: "hostile/flat-fallback", thinking_level: "low", ...ESCALATION_VALUE },
|
|
106
|
+
],
|
|
116
107
|
variant: "flat-variant",
|
|
117
108
|
thinking_level: "low",
|
|
118
109
|
temperature: 0.2,
|
|
@@ -135,24 +126,27 @@ function buildHostileProjectConfig(): Record<string, unknown> {
|
|
|
135
126
|
}
|
|
136
127
|
|
|
137
128
|
function historianUserOnlyPaths(): string[] {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
129
|
+
// Exactly the paths the hostile fixture plants: the flat fields plus the
|
|
130
|
+
// harness-appropriate fields inside the removed per-harness blocks.
|
|
131
|
+
return [
|
|
132
|
+
"historian.model",
|
|
133
|
+
"historian.fallback_models",
|
|
134
|
+
"historian.variant",
|
|
135
|
+
"historian.thinking_level",
|
|
136
|
+
"historian.opencode.model",
|
|
137
|
+
"historian.opencode.fallback_models",
|
|
138
|
+
"historian.opencode.variant",
|
|
139
|
+
"historian.pi.model",
|
|
140
|
+
"historian.pi.fallback_models",
|
|
141
|
+
"historian.pi.thinking_level",
|
|
142
|
+
];
|
|
151
143
|
}
|
|
152
144
|
|
|
153
145
|
function nestedEscalationPaths(): string[] {
|
|
154
146
|
const sites = [
|
|
155
147
|
"historian",
|
|
148
|
+
"historian.model",
|
|
149
|
+
"historian.fallback_models.0",
|
|
156
150
|
"historian.opencode",
|
|
157
151
|
"historian.opencode.model",
|
|
158
152
|
"historian.opencode.fallback_models.0",
|
|
@@ -191,15 +185,18 @@ function loadWithUserAndProjectConfig(userConfigText: string, projectConfigText:
|
|
|
191
185
|
}
|
|
192
186
|
|
|
193
187
|
describe("hostile-config stripping matrix", () => {
|
|
194
|
-
it("
|
|
195
|
-
expect(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
);
|
|
201
|
-
expect(entryObjectKeys(
|
|
202
|
-
|
|
188
|
+
it("declares the flat historian model-resolution contract", () => {
|
|
189
|
+
expect([...FLAT_MODEL_FIELDS]).toEqual([
|
|
190
|
+
"model",
|
|
191
|
+
"fallback_models",
|
|
192
|
+
"variant",
|
|
193
|
+
"thinking_level",
|
|
194
|
+
]);
|
|
195
|
+
expect(entryObjectKeys(HistorianModelEntrySchema)).toEqual([
|
|
196
|
+
"model",
|
|
197
|
+
"thinking_level",
|
|
198
|
+
"variant",
|
|
199
|
+
]);
|
|
203
200
|
});
|
|
204
201
|
|
|
205
202
|
it("strips every user-only or unsafe nested path and keeps legitimate project-tier fields", () => {
|
|
@@ -230,9 +227,6 @@ describe("hostile-config stripping matrix", () => {
|
|
|
230
227
|
expect(hasOwnPath(raw, path)).toBe(true);
|
|
231
228
|
}
|
|
232
229
|
|
|
233
|
-
expect(hasOwnPath(raw, "historian.opencode.model")).toBe(false);
|
|
234
|
-
expect(hasOwnPath(raw, "historian.pi.model")).toBe(false);
|
|
235
|
-
|
|
236
230
|
for (const path of [...historianUserOnlyPaths(), ...nestedEscalationPaths()]) {
|
|
237
231
|
expect(warningText).toContain(path);
|
|
238
232
|
}
|
|
@@ -240,27 +234,20 @@ describe("hostile-config stripping matrix", () => {
|
|
|
240
234
|
|
|
241
235
|
it("does not let project historian qualifiers attach to the user's historian model", () => {
|
|
242
236
|
const result = loadWithUserAndProjectConfig(
|
|
237
|
+
JSON.stringify({ historian: { model: "anthropic/user-historian" } }),
|
|
243
238
|
JSON.stringify({
|
|
244
|
-
historian: {
|
|
245
|
-
opencode: { model: "anthropic/user-historian" },
|
|
246
|
-
pi: { model: "github-copilot/user-historian" },
|
|
247
|
-
},
|
|
248
|
-
}),
|
|
249
|
-
JSON.stringify({
|
|
250
|
-
historian: {
|
|
251
|
-
opencode: { variant: "high" },
|
|
252
|
-
pi: { thinking_level: "max" },
|
|
253
|
-
},
|
|
239
|
+
historian: { variant: "high", thinking_level: "max" },
|
|
254
240
|
}),
|
|
255
241
|
);
|
|
256
242
|
|
|
257
|
-
expect(result.historian?.
|
|
258
|
-
expect(result.historian?.
|
|
259
|
-
expect(result.
|
|
260
|
-
expect(result.configWarnings?.join("\n") ?? "").toContain("historian.
|
|
243
|
+
expect(result.historian?.model).toBe("anthropic/user-historian");
|
|
244
|
+
expect(result.historian?.variant).toBeUndefined();
|
|
245
|
+
expect(result.historian?.thinking_level).toBeUndefined();
|
|
246
|
+
expect(result.configWarnings?.join("\n") ?? "").toContain("historian.variant");
|
|
247
|
+
expect(result.configWarnings?.join("\n") ?? "").toContain("historian.thinking_level");
|
|
261
248
|
});
|
|
262
249
|
|
|
263
|
-
it("rejects prototype-pollution keys at the nested harness depth", () => {
|
|
250
|
+
it("rejects prototype-pollution keys at the nested legacy harness depth", () => {
|
|
264
251
|
const projectConfig = `{
|
|
265
252
|
"historian": {
|
|
266
253
|
"opencode": {
|
|
@@ -273,8 +260,7 @@ describe("hostile-config stripping matrix", () => {
|
|
|
273
260
|
const result = loadWithUserAndProjectConfig("{}", projectConfig);
|
|
274
261
|
const warnings = result.configWarnings?.join("\n") ?? "";
|
|
275
262
|
|
|
276
|
-
expect(result.historian?.opencode
|
|
277
|
-
expect(result.historian?.opencode?.variant).toBeUndefined();
|
|
263
|
+
expect(result.historian?.opencode).toBeUndefined();
|
|
278
264
|
expect(warnings).toContain("prototype-pollution");
|
|
279
265
|
expect(warnings).toContain("historian.opencode.__proto__");
|
|
280
266
|
});
|
|
@@ -11,7 +11,7 @@ describe("stripUnsafeProjectConfigFields", () => {
|
|
|
11
11
|
profile: "work",
|
|
12
12
|
profiles: {
|
|
13
13
|
work: {
|
|
14
|
-
historian: {
|
|
14
|
+
historian: { model: "attacker/model" },
|
|
15
15
|
},
|
|
16
16
|
},
|
|
17
17
|
};
|
|
@@ -154,6 +154,8 @@ describe("stripUnsafeProjectConfigFields", () => {
|
|
|
154
154
|
historian: {
|
|
155
155
|
model: "legacy/repo-model",
|
|
156
156
|
fallback_models: ["legacy/repo-fallback"],
|
|
157
|
+
variant: "high",
|
|
158
|
+
thinking_level: "medium",
|
|
157
159
|
opencode: {
|
|
158
160
|
model: "opencode/repo-model",
|
|
159
161
|
fallback_models: ["opencode/repo-fallback"],
|
|
@@ -176,6 +178,9 @@ describe("stripUnsafeProjectConfigFields", () => {
|
|
|
176
178
|
});
|
|
177
179
|
const warning = warnings.join("\n");
|
|
178
180
|
expect(warning).toContain("historian.model");
|
|
181
|
+
expect(warning).toContain("historian.fallback_models");
|
|
182
|
+
expect(warning).toContain("historian.variant");
|
|
183
|
+
expect(warning).toContain("historian.thinking_level");
|
|
179
184
|
expect(warning).toContain("historian.opencode.model");
|
|
180
185
|
expect(warning).toContain("historian.opencode.variant");
|
|
181
186
|
expect(warning).toContain("historian.pi.model");
|