@sema-agent/core 5.58.0 → 5.60.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/CHANGELOG.md +88 -0
- package/dist/brain/anthropic.js +15 -5
- package/dist/brain/errors.d.ts +18 -1
- package/dist/brain/errors.js +7 -1
- package/dist/brain/input-too-long.d.ts +57 -0
- package/dist/brain/input-too-long.js +35 -0
- package/dist/brain/route-adjudicator.d.ts +8 -1
- package/dist/brain/route-adjudicator.js +8 -1
- package/dist/brain/stream-engine.js +9 -1
- package/dist/core/auto-compaction.js +2 -2
- package/dist/core/checkpoint-store.d.ts +116 -19
- package/dist/core/checkpoint-store.js +15 -8
- package/dist/core/context-edit.d.ts +243 -41
- package/dist/core/context-edit.js +247 -32
- package/dist/core/governance-codes.d.ts +37 -10
- package/dist/core/governance-codes.js +57 -1
- package/dist/core/locked-config.d.ts +36 -4
- package/dist/core/locked-config.js +34 -1
- package/dist/core/mcp.js +10 -6
- package/dist/core/memory-engine/consolidation-driver.d.ts +6 -2
- package/dist/core/memory-engine/consolidation-driver.js +54 -5
- package/dist/core/memory-engine/consolidation.d.ts +73 -1
- package/dist/core/memory-engine/consolidation.js +21 -1
- package/dist/core/memory-engine/content-origin.d.ts +24 -2
- package/dist/core/memory-engine/content-origin.js +6 -1
- package/dist/core/memory-engine/engine.d.ts +97 -8
- package/dist/core/memory-engine/engine.js +112 -20
- package/dist/core/memory-engine/file-backend.d.ts +13 -1
- package/dist/core/memory-engine/file-backend.js +3 -0
- package/dist/core/memory-engine/index.d.ts +4 -3
- package/dist/core/memory-engine/layout.js +20 -6
- package/dist/core/memory-engine/types.d.ts +17 -0
- package/dist/core/memory.d.ts +10 -0
- package/dist/core/park-selfcheck.js +1 -0
- package/dist/core/permission-rule-consent.js +9 -5
- package/dist/core/permission-rule-model.d.ts +42 -1
- package/dist/core/permission-rule-model.js +12 -0
- package/dist/core/runner/prepare-config-doors.d.ts +22 -1
- package/dist/core/runner/prepare-config-doors.js +36 -0
- package/dist/core/runner/prepare-task.d.ts +28 -1
- package/dist/core/runner/prepare-task.js +109 -11
- package/dist/core/runner/runtask.js +45 -8
- package/dist/core/store-contracts/checkpoint-store-contract.js +32 -0
- package/dist/core/tool-policy.d.ts +74 -0
- package/dist/core/tool-policy.js +80 -1
- package/dist/core/tools.js +1 -1
- package/dist/core/trace.d.ts +36 -0
- package/dist/core/types.d.ts +172 -22
- package/dist/core/types.js +4 -3
- package/dist/core/untrusted-text.d.ts +11 -0
- package/dist/core/untrusted-text.js +1 -0
- package/dist/engine/llm/types.d.ts +21 -2
- package/dist/engine/loop/agent-loop.js +7 -1
- package/dist/engine/loop/types.d.ts +4 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/tools/fs/fs-bash.js +1 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +1771 -1
|
@@ -9,7 +9,7 @@ import { formatMemoryAge } from "../memory-recall.js";
|
|
|
9
9
|
import { committedDistilledOf, committedOriginOf, computeEntryRev, distilledEquals, hasDistilledFormExtra, hasOriginFormExtra, isValidEntryId, originEquals, parseEntryFile, serializeEntryFile, stripModelWrittenDistilled, stripModelWrittenOrigin } from "./frontmatter.js";
|
|
10
10
|
import { MEMORY_PROVENANCE_RECALL_SENTENCE, memoryExposureIndexRow, parseMemoryExposureIndexRow } from "./provenance-wording.js";
|
|
11
11
|
import { isInstructionEntry } from "./header-hints.js";
|
|
12
|
-
import { ConsolidationRefusedError, CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS, CONSOLIDATION_SEAT_FREEZE_GRACE_MS, consolidationTypeEligible, deriveProductSlug, deriveSupersededSet, isValidConsolidationPlanId, listConsolidationPlanIds, productAddPatch, quarantineCorruptPlan, readConsolidationGateFile, readConsolidationPlan, readIntentCredentials, recordActiveSetBaseline, recordConsolidationSession, recordIntentCredentials, screenConsolidationOptions, supersessionFuseCeiling, updateConsolidationGate, writeConsolidationPlan, } from "./consolidation.js";
|
|
12
|
+
import { ConsolidationRefusedError, CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS, CONSOLIDATION_SEAT_FREEZE_GRACE_MS, consolidationTypeEligible, deriveProductSlug, deriveSupersededSet, isValidConsolidationPlanId, listConsolidationPlanIds, productAddPatch, quarantineCorruptPlan, readConsolidationGateFile, readConsolidationPlan, readIntentCredentials, recordActiveSetBaseline, recordConsolidationSession, recordIntentCredentials, scanRefusalCeiling, screenConsolidationOptions, supersessionFuseCeiling, updateConsolidationGate, writeConsolidationPlan, } from "./consolidation.js";
|
|
13
13
|
import { classifySessionSettlements, clearHoldResolution, closeSessionAccount, disposeHold, effectiveSettlements, expireOverdueSettlements, foreignDanglingSessionAccounts, markHoldReleased, openInstructionHold, openSessionAccount, readHoldCustody, readHolds, reconcileHolds, replayExternalSettlementEffects, resolveHoldRecord, resolveSessionAccountRecord, resolveSettlementRecord, sessionSettlements, sessionUnattributedSet, } from "./delegation-settlement.js";
|
|
14
14
|
import { noReplaceRestore } from "./delegation-settlement.js";
|
|
15
15
|
import { markOriginClearanceTombstoned, openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
|
|
@@ -2656,6 +2656,9 @@ export class MemoryEngine {
|
|
|
2656
2656
|
else if (p.inputs.length > screened.maxInputsPerProduct)
|
|
2657
2657
|
reasons.push(`product #${i}: ${p.inputs.length} inputs > maxInputsPerProduct ${screened.maxInputsPerProduct}`);
|
|
2658
2658
|
}
|
|
2659
|
+
if (proposal.products.length === 0 && intents.length === 0) {
|
|
2660
|
+
reasons.push("empty plan: zero products and zero directed intents — a plan with nothing to apply must not settle as a completed run (the vacuous completion would stamp the eligible set into the fingerprint and blind the incremental face)");
|
|
2661
|
+
}
|
|
2659
2662
|
if (reasons.length > 0)
|
|
2660
2663
|
throw new ConsolidationRefusedError("memory.consolidation_refused", "memory consolidation plan refused (structural)", reasons);
|
|
2661
2664
|
const planId = uuidv7();
|
|
@@ -2737,21 +2740,7 @@ export class MemoryEngine {
|
|
|
2737
2740
|
}
|
|
2738
2741
|
}
|
|
2739
2742
|
const activeSetSize = headers.filter((h) => !superseded.has(h.id) && !exclusions.has(h.id)).length;
|
|
2740
|
-
const edgeTargets = new Set();
|
|
2741
|
-
for (const p of proposal.products)
|
|
2742
|
-
for (const i2 of p.inputs)
|
|
2743
|
-
if (i2.supersede === true)
|
|
2744
|
-
edgeTargets.add(i2.id);
|
|
2745
2743
|
const ceiling = supersessionFuseCeiling(activeSetSize, screened);
|
|
2746
|
-
if (edgeTargets.size > ceiling) {
|
|
2747
|
-
wholeReasons.push(`supersession fuse: ${edgeTargets.size} distinct targets > ceiling ${ceiling} (active set ${activeSetSize}, ratio ${screened.supersedeRatioCap}, floor ${screened.supersedeAbsoluteFloor}, N-1 unconditional)`);
|
|
2748
|
-
try {
|
|
2749
|
-
enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at, items: [`memory consolidation plan for scope ${inlineUntrusted(scope, 80)} REFUSED by the supersession fuse (${edgeTargets.size} targets over the ${ceiling} ceiling)`] });
|
|
2750
|
-
}
|
|
2751
|
-
catch (err) {
|
|
2752
|
-
this.discloseAnnounceFailure("consolidation fuse enqueue", err);
|
|
2753
|
-
}
|
|
2754
|
-
}
|
|
2755
2744
|
let visibleMarked = snapshot.marked;
|
|
2756
2745
|
if (!visibleMarked) {
|
|
2757
2746
|
const inputIds = [...new Set(proposal.products.flatMap((p) => p.inputs.map((i2) => i2.id)))];
|
|
@@ -2761,12 +2750,18 @@ export class MemoryEngine {
|
|
|
2761
2750
|
const foldedOrigin = visibleMarked ? { taint: "external", cause: "derived", at } : undefined;
|
|
2762
2751
|
for (let i = 0; i < proposal.products.length; i++) {
|
|
2763
2752
|
const p = proposal.products[i];
|
|
2764
|
-
if (
|
|
2765
|
-
|
|
2753
|
+
if (!isInstructionEntry({ ...(p.type !== undefined ? { type: p.type } : {}) }))
|
|
2754
|
+
continue;
|
|
2755
|
+
if (visibleMarked) {
|
|
2756
|
+
wholeReasons.push(`product #${i}: instruction-form product (type ${JSON.stringify(p.type)}) refused — the run's visible set contains marked content (the laundering hard gate, G7/G26; no option opens this arm)`);
|
|
2757
|
+
}
|
|
2758
|
+
else if (!screened.allowInstructionProducts) {
|
|
2759
|
+
wholeReasons.push(`product #${i}: instruction-form product (type ${JSON.stringify(p.type)}) refused — instruction-form consolidation products are refused unconditionally by default (a consolidation must not mint privileged entries out of ordinary notes); the host escape hatch is consolidation.allowInstructionProducts: true`);
|
|
2766
2760
|
}
|
|
2767
2761
|
}
|
|
2768
2762
|
const assembled = [];
|
|
2769
2763
|
const refusedProducts = [];
|
|
2764
|
+
const scanRefused = [];
|
|
2770
2765
|
for (let i = 0; i < proposal.products.length; i++) {
|
|
2771
2766
|
const p = proposal.products[i];
|
|
2772
2767
|
const drift = [];
|
|
@@ -2813,11 +2808,31 @@ export class MemoryEngine {
|
|
|
2813
2808
|
const nameFinding = scanMemoryFileName(`${slug}.md`);
|
|
2814
2809
|
const findings = [...(nameFinding !== undefined ? [nameFinding] : []), ...scanMemoryWrite(serializeEntryFile(entry), { maxBytes: this.perFileBytes })];
|
|
2815
2810
|
if (findings.length > 0) {
|
|
2816
|
-
|
|
2811
|
+
const reason = `scan gate (${findings.map((f) => `${f.code}: ${f.reason}`).join("; ")})`;
|
|
2812
|
+
refusedProducts.push({ index: i, reason });
|
|
2813
|
+
scanRefused.push({ index: i, reason });
|
|
2817
2814
|
continue;
|
|
2818
2815
|
}
|
|
2819
2816
|
assembled.push(entry);
|
|
2820
2817
|
}
|
|
2818
|
+
const edgeTargets = new Set();
|
|
2819
|
+
for (const e of assembled)
|
|
2820
|
+
for (const i2 of e.frontmatter.distilled?.inputs ?? [])
|
|
2821
|
+
if (i2.superseded === true)
|
|
2822
|
+
edgeTargets.add(i2.id);
|
|
2823
|
+
if (edgeTargets.size > ceiling) {
|
|
2824
|
+
wholeReasons.push(`supersession fuse: ${edgeTargets.size} distinct targets > ceiling ${ceiling} (active set ${activeSetSize}, ratio ${screened.supersedeRatioCap}, floor ${screened.supersedeAbsoluteFloor}, N-1 unconditional)`);
|
|
2825
|
+
try {
|
|
2826
|
+
enqueueMemoryAnnouncement(this.controlDir, { kind: "gate", at, items: [`memory consolidation plan for scope ${inlineUntrusted(scope, 80)} REFUSED by the supersession fuse (${edgeTargets.size} targets over the ${ceiling} ceiling)`] });
|
|
2827
|
+
}
|
|
2828
|
+
catch (err) {
|
|
2829
|
+
this.discloseAnnounceFailure("consolidation fuse enqueue", err);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
const scanCeiling = scanRefusalCeiling(proposal.products.length, screened);
|
|
2833
|
+
if (scanRefused.length > scanCeiling) {
|
|
2834
|
+
wholeReasons.push(`scan-refusal fuse: ${scanRefused.length} of ${proposal.products.length} product(s) failed the write scan > ceiling ${scanCeiling} (ratio ${screened.scanRefusalRatioCap}, per-plan max ${screened.scanRefusalMaxPerPlan}) — ${scanRefused.map((r) => `#${r.index}: ${r.reason}`).join(" | ")}`);
|
|
2835
|
+
}
|
|
2821
2836
|
const directed = [];
|
|
2822
2837
|
const intents = proposal.intents ?? [];
|
|
2823
2838
|
const targetIds = intents.map((it) => it.entryId);
|
|
@@ -2875,6 +2890,9 @@ export class MemoryEngine {
|
|
|
2875
2890
|
directed.push({ intentRequestId: intent.requestId, op: "update", id: intent.entryId, baseRev: committed.rev, plannedPostRev: terminal.rev, entry: terminal, state: "pending" });
|
|
2876
2891
|
}
|
|
2877
2892
|
}
|
|
2893
|
+
if (wholeReasons.length === 0 && assembled.length === 0 && directed.length === 0) {
|
|
2894
|
+
wholeReasons.push(`empty plan after freeze: every product was refused (${refusedProducts.map((r) => `#${r.index}: ${r.reason}`).join(" | ")}) — a plan with nothing left to apply must not settle as a completed run (the vacuous completion would stamp the eligible set into the fingerprint and blind the incremental face)`);
|
|
2895
|
+
}
|
|
2878
2896
|
if (wholeReasons.length > 0)
|
|
2879
2897
|
throw new ConsolidationRefusedError("memory.consolidation_refused", "memory consolidation plan refused", wholeReasons);
|
|
2880
2898
|
try {
|
|
@@ -2888,8 +2906,9 @@ export class MemoryEngine {
|
|
|
2888
2906
|
throw err;
|
|
2889
2907
|
throw new ConsolidationRefusedError("memory.consolidation_gate_corrupt", `memory consolidation refused: the gate store could not be re-read at the freeze boundary (fail-closed): ${err instanceof Error ? err.message : String(err)}`);
|
|
2890
2908
|
}
|
|
2909
|
+
const freezeRefusedInputIds = [...new Set(refusedProducts.flatMap((r) => (proposal.products[r.index]?.inputs ?? []).map((i2) => i2.id)))].sort();
|
|
2891
2910
|
const plan = {
|
|
2892
|
-
v: 1,
|
|
2911
|
+
v: freezeRefusedInputIds.length > 0 ? 2 : 1,
|
|
2893
2912
|
planId,
|
|
2894
2913
|
scope,
|
|
2895
2914
|
requestId: input.requestId,
|
|
@@ -2900,6 +2919,7 @@ export class MemoryEngine {
|
|
|
2900
2919
|
...(foldedOrigin !== undefined ? { foldedOrigin } : {}),
|
|
2901
2920
|
products: assembled,
|
|
2902
2921
|
productStates: Object.fromEntries(assembled.map((e) => [e.id, "pending"])),
|
|
2922
|
+
...(freezeRefusedInputIds.length > 0 ? { freezeRefusedInputIds } : {}),
|
|
2903
2923
|
directed,
|
|
2904
2924
|
intents: intents.map((it) => ({ requestId: it.requestId, state: "pending" })),
|
|
2905
2925
|
state: "open",
|
|
@@ -3099,6 +3119,7 @@ export class MemoryEngine {
|
|
|
3099
3119
|
}
|
|
3100
3120
|
writeConsolidationPlan(this.controlDir, plan);
|
|
3101
3121
|
const currentlyEligible = await this.settleFingerprintFilter(plan.scope);
|
|
3122
|
+
const freezeRefused = new Set(plan.freezeRefusedInputIds ?? []);
|
|
3102
3123
|
updateConsolidationGate(this.controlDir, (file) => {
|
|
3103
3124
|
const row = file.scopes[plan.scope];
|
|
3104
3125
|
if (row?.openPlanId !== plan.planId)
|
|
@@ -3111,6 +3132,8 @@ export class MemoryEngine {
|
|
|
3111
3132
|
if (currentlyEligible !== undefined) {
|
|
3112
3133
|
const fp = {};
|
|
3113
3134
|
for (const [id, rev] of Object.entries(row.snapshot.eligible)) {
|
|
3135
|
+
if (freezeRefused.has(id))
|
|
3136
|
+
continue;
|
|
3114
3137
|
if (!currentlyEligible(id, rev))
|
|
3115
3138
|
continue;
|
|
3116
3139
|
fp[id] = { rev };
|
|
@@ -3234,6 +3257,7 @@ export class MemoryEngine {
|
|
|
3234
3257
|
}
|
|
3235
3258
|
async settleGateForTerminalPlan(plan) {
|
|
3236
3259
|
const currentlyEligible = await this.settleFingerprintFilter(plan.scope);
|
|
3260
|
+
const freezeRefused = new Set(plan.freezeRefusedInputIds ?? []);
|
|
3237
3261
|
updateConsolidationGate(this.controlDir, (file) => {
|
|
3238
3262
|
const row = file.scopes[plan.scope];
|
|
3239
3263
|
if (row?.openPlanId !== plan.planId)
|
|
@@ -3247,6 +3271,8 @@ export class MemoryEngine {
|
|
|
3247
3271
|
if (currentlyEligible !== undefined) {
|
|
3248
3272
|
const fp = {};
|
|
3249
3273
|
for (const [id, rev] of Object.entries(row.snapshot.eligible)) {
|
|
3274
|
+
if (freezeRefused.has(id))
|
|
3275
|
+
continue;
|
|
3250
3276
|
if (!currentlyEligible(id, rev))
|
|
3251
3277
|
continue;
|
|
3252
3278
|
fp[id] = { rev };
|
|
@@ -3348,6 +3374,24 @@ export class MemoryEngine {
|
|
|
3348
3374
|
throw new ConsolidationRefusedError("memory.consolidation_gate_corrupt", `memory consolidation retry refused: the intent-credential account cannot be trusted (fail-closed): ${err instanceof Error ? err.message : String(err)}`);
|
|
3349
3375
|
}
|
|
3350
3376
|
const pendingIntents = plan.intents.filter((i) => i.state === "pending" && standingCredentials[i.requestId] === undefined).map((i) => i.requestId);
|
|
3377
|
+
if (pendingIntents.length === 0) {
|
|
3378
|
+
throw new ConsolidationRefusedError("memory.consolidation_refused", `memory consolidation retry refused: plan ${planId} has nothing to re-judge — ` +
|
|
3379
|
+
(plan.intents.length === 0
|
|
3380
|
+
? "it carries no intents (a conflicted product never rides a retry)"
|
|
3381
|
+
: "every unfulfilled intent already carries a credential (consumed by an earlier retry or a racing plan)") +
|
|
3382
|
+
` — a plan with nothing to apply must not settle as a completed run (the vacuous completion would stamp the eligible set into the fingerprint and blind the incremental face); resolve with "discard" to release the seat.`);
|
|
3383
|
+
}
|
|
3384
|
+
const conflictProductInputIds = [];
|
|
3385
|
+
for (const p of plan.products) {
|
|
3386
|
+
if (plan.productStates[p.id] !== "conflict")
|
|
3387
|
+
continue;
|
|
3388
|
+
const inputs = p.frontmatter?.distilled?.inputs;
|
|
3389
|
+
if (!Array.isArray(inputs) || inputs.some((i) => typeof i?.["id"] !== "string")) {
|
|
3390
|
+
throw new ConsolidationRefusedError("memory.consolidation_refused", `memory consolidation retry refused: conflicted product ${p.id} carries no readable distilled attribution rows (every frozen product is minted with them — this is control-plane damage); the plan stays parked, resolve with "discard" (the bytes remain on the plan file for host review).`);
|
|
3391
|
+
}
|
|
3392
|
+
for (const i of inputs)
|
|
3393
|
+
conflictProductInputIds.push(i.id);
|
|
3394
|
+
}
|
|
3351
3395
|
const face = this.committedAuditFace();
|
|
3352
3396
|
const originalIntents = await this.reconstructIntentsForRetry(plan, pendingIntents, face);
|
|
3353
3397
|
const retryPlanId = uuidv7();
|
|
@@ -3361,8 +3405,9 @@ export class MemoryEngine {
|
|
|
3361
3405
|
file.scopes[plan.scope] = row;
|
|
3362
3406
|
return { next: file, result: undefined };
|
|
3363
3407
|
});
|
|
3408
|
+
const inheritedRefusals = [...new Set([...(plan.freezeRefusedInputIds ?? []), ...conflictProductInputIds])].sort();
|
|
3364
3409
|
const retryPlan = {
|
|
3365
|
-
v: 1,
|
|
3410
|
+
v: inheritedRefusals.length > 0 ? 2 : 1,
|
|
3366
3411
|
planId: retryPlanId,
|
|
3367
3412
|
scope: plan.scope,
|
|
3368
3413
|
requestId: opts.requestId,
|
|
@@ -3371,6 +3416,7 @@ export class MemoryEngine {
|
|
|
3371
3416
|
visibleMarked: false,
|
|
3372
3417
|
products: [],
|
|
3373
3418
|
productStates: {},
|
|
3419
|
+
...(inheritedRefusals.length > 0 ? { freezeRefusedInputIds: inheritedRefusals } : {}),
|
|
3374
3420
|
directed: originalIntents.directed,
|
|
3375
3421
|
intents: pendingIntents.map((r) => ({ requestId: r, state: "pending" })),
|
|
3376
3422
|
state: "open",
|
|
@@ -3450,6 +3496,35 @@ export class MemoryEngine {
|
|
|
3450
3496
|
}
|
|
3451
3497
|
return out;
|
|
3452
3498
|
}
|
|
3499
|
+
readConsolidationPlanFoldEvidence(planId) {
|
|
3500
|
+
const read = readConsolidationPlan(this.controlDir, planId);
|
|
3501
|
+
if (read.state === "absent")
|
|
3502
|
+
return { state: "absent" };
|
|
3503
|
+
if (read.state === "corrupt")
|
|
3504
|
+
return { state: "corrupt", detail: read.detail };
|
|
3505
|
+
const p = read.plan;
|
|
3506
|
+
const targets = new Set();
|
|
3507
|
+
for (const prod of p.products) {
|
|
3508
|
+
if (p.productStates[prod.id] !== "applied")
|
|
3509
|
+
continue;
|
|
3510
|
+
const inputs = prod.frontmatter?.distilled?.inputs;
|
|
3511
|
+
const anchors = { scope: p.scope, requestId: p.requestId };
|
|
3512
|
+
if (inputs === undefined)
|
|
3513
|
+
return { state: "corrupt", detail: `applied product ${prod.id} carries no distilled.inputs (every frozen product is minted with its attribution rows)`, ...anchors };
|
|
3514
|
+
if (!Array.isArray(inputs))
|
|
3515
|
+
return { state: "corrupt", detail: `applied product ${prod.id} carries a non-array distilled.inputs`, ...anchors };
|
|
3516
|
+
for (const i of inputs) {
|
|
3517
|
+
if (typeof i !== "object" || i === null)
|
|
3518
|
+
return { state: "corrupt", detail: `applied product ${prod.id} carries a malformed distilled input row`, ...anchors };
|
|
3519
|
+
if (i.superseded !== true)
|
|
3520
|
+
continue;
|
|
3521
|
+
if (typeof i.id !== "string")
|
|
3522
|
+
return { state: "corrupt", detail: `applied product ${prod.id} carries a supersession edge without a string target id`, ...anchors };
|
|
3523
|
+
targets.add(i.id);
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
return { state: "ok", scope: p.scope, requestId: p.requestId, planState: p.state, supersededTargets: [...targets].sort() };
|
|
3527
|
+
}
|
|
3453
3528
|
readConsolidationGate(scope) {
|
|
3454
3529
|
try {
|
|
3455
3530
|
return readConsolidationGateFile(this.controlDir).scopes[scope];
|
|
@@ -3458,6 +3533,16 @@ export class MemoryEngine {
|
|
|
3458
3533
|
return undefined;
|
|
3459
3534
|
}
|
|
3460
3535
|
}
|
|
3536
|
+
readConsolidationGateState(scope) {
|
|
3537
|
+
let row;
|
|
3538
|
+
try {
|
|
3539
|
+
row = readConsolidationGateFile(this.controlDir).scopes[scope];
|
|
3540
|
+
}
|
|
3541
|
+
catch (err) {
|
|
3542
|
+
return { state: "corrupt", detail: err instanceof Error ? err.message : String(err) };
|
|
3543
|
+
}
|
|
3544
|
+
return row !== undefined ? { state: "present", row } : { state: "absent" };
|
|
3545
|
+
}
|
|
3461
3546
|
async readConsolidationFoldState(scope) {
|
|
3462
3547
|
const face = this.committedAuditFace();
|
|
3463
3548
|
const headers = await face.listHeaders([scope]);
|
|
@@ -3478,6 +3563,13 @@ export class MemoryEngine {
|
|
|
3478
3563
|
get controlPlaneRoot() {
|
|
3479
3564
|
return this.controlDir;
|
|
3480
3565
|
}
|
|
3566
|
+
async listMemoryScopes() {
|
|
3567
|
+
const b = this.backend;
|
|
3568
|
+
if (typeof b.listScopes !== "function") {
|
|
3569
|
+
return { supported: false, reason: "the backend does not implement scope enumeration (listScopes) — an absent capability must never read as an empty scope list" };
|
|
3570
|
+
}
|
|
3571
|
+
return await b.listScopes();
|
|
3572
|
+
}
|
|
3481
3573
|
committedAuditFace() {
|
|
3482
3574
|
const b = this.backend;
|
|
3483
3575
|
return b.restrictedAdoptionView?.({ audit: false }) ?? b.retrievalView?.() ?? this.backend;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { scopeDirName, type ChallengeEvent, type MemoryPartitionIncidentSink } from "./layout.js";
|
|
2
2
|
import type { MemoryBundleImportPlan, MemoryImportReport } from "./export-bundle.js";
|
|
3
|
-
import type { HarvestRejection, MemoryBackend, MemoryEntry, MemoryEntryHeader, NotePatch, PatchReport, ScoredMemoryEntry } from "./types.js";
|
|
3
|
+
import type { HarvestRejection, MemoryBackend, MemoryEntry, MemoryEntryHeader, MemoryScopeEnumeration, NotePatch, PatchReport, ScoredMemoryEntry } from "./types.js";
|
|
4
4
|
/** Files/dirs the entry scan never treats as entries: the derived index, dotfiles (`.hydrate`,
|
|
5
5
|
* transaction staging files), and anything not `.md`. */
|
|
6
6
|
export declare const MEMORY_INDEX_FILENAME = "MEMORY.md";
|
|
@@ -756,6 +756,18 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
756
756
|
* while a live holder exists ({@link txnInFlight}), which now covers adoption holds too.
|
|
757
757
|
*/
|
|
758
758
|
private readScopeAdopting;
|
|
759
|
+
/**
|
|
760
|
+
* design/376 slice ③ (#437③) — the File leg's scope-enumeration capability face: every scope
|
|
761
|
+
* this store has hosted, derived from the store root's durable scope registry (the control
|
|
762
|
+
* plane's `scopes.json` — the one source that can invert a scope's sanitized directory label
|
|
763
|
+
* back to its name; a listing of raw subdirectories could not). Side-effect-free committed
|
|
764
|
+
* read; a corrupt/unreadable registry THROWS its fail-closed refusal — enumeration over an
|
|
765
|
+
* untrustworthy registry must never answer an empty (= clean-looking) list. Honest residual,
|
|
766
|
+
* disclosed: the registry records scopes as they are first touched THROUGH this control plane,
|
|
767
|
+
* so a hand-assembled tree's never-touched scope dirs appear here only once a caller first
|
|
768
|
+
* names them — store-level fact, session-roster-free either way.
|
|
769
|
+
*/
|
|
770
|
+
listScopes(): MemoryScopeEnumeration;
|
|
759
771
|
listHeaders(scopes: readonly string[]): Promise<MemoryEntryHeader[]>;
|
|
760
772
|
private listHeadersFrom;
|
|
761
773
|
getByIds(ids: readonly string[]): Promise<MemoryEntry[]>;
|
|
@@ -1780,6 +1780,9 @@ export class FileMemoryEngineBackend {
|
|
|
1780
1780
|
lock.release();
|
|
1781
1781
|
}
|
|
1782
1782
|
}
|
|
1783
|
+
listScopes() {
|
|
1784
|
+
return { supported: true, scopes: Object.keys(registeredScopes(this.controlPlaneRoot)).sort() };
|
|
1785
|
+
}
|
|
1783
1786
|
async listHeaders(scopes) {
|
|
1784
1787
|
return this.listHeadersFrom(scopes, (s) => this.readScopeAdopting(s));
|
|
1785
1788
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, memoryRecallDisciplineSegment, entryFileHeadCarriesOrigin, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, type EntryProvenanceAccount, memoryConsolidationRecommendedNotice, memoryConsolidationCommittedNotice, memoryConsolidationConflictNotice, memoryConsolidationIncompleteNotice, memoryConsolidationRefusedNotice, type ConsolidationCommitReceipt, type ConsolidationReconcileReport, type ConsolidationResolveReceipt, type ConsolidationPlanSummary, } from "./engine.js";
|
|
1
|
+
export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, memoryRecallDisciplineSegment, entryFileHeadCarriesOrigin, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, type EntryProvenanceAccount, memoryConsolidationRecommendedNotice, memoryConsolidationCommittedNotice, memoryConsolidationConflictNotice, memoryConsolidationIncompleteNotice, memoryConsolidationRefusedNotice, type ConsolidationCommitReceipt, type ConsolidationReconcileReport, type ConsolidationResolveReceipt, type ConsolidationPlanSummary, type ConsolidationPlanFoldEvidence, } from "./engine.js";
|
|
2
2
|
export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type CleanMemorySearchHit, type ExposedMemorySearchHit, type MemoryGetDetails, } from "./tools.js";
|
|
3
3
|
export { MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, MEMORY_PROVENANCE_RECALL_SENTENCE, MEMORY_PROVENANCE_SEARCH_SENTENCE, memoryExposureIndexRow, parseMemoryExposureIndexRow, } from "./provenance-wording.js";
|
|
4
4
|
export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
|
|
@@ -8,11 +8,12 @@ export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlane
|
|
|
8
8
|
export { readV2HeaderHints, isInstructionEntry, type V2HeaderHints } from "./header-hints.js";
|
|
9
9
|
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, committedOriginOf, originEquals, ambiguousOriginRepresentation, type ParsedEntryFile } from "./frontmatter.js";
|
|
10
10
|
export { committedDistilledOf, distilledEquals } from "./frontmatter.js";
|
|
11
|
-
export { CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type MemoryConsolidationOptions, CONSOLIDATION_RUN_STOP_REASONS, type ConsolidationRunStopReason, } from "./consolidation.js";
|
|
11
|
+
export { CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, type ConsolidationGateRead, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type MemoryConsolidationOptions, CONSOLIDATION_RUN_STOP_REASONS, type ConsolidationRunStopReason, } from "./consolidation.js";
|
|
12
12
|
export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintLlmConsolidationPlan, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, type ConsolidationDistillFn, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationDriveResult, type ConsolidationFoldState, type DistillerCandidate, type DistillerChatAnswer, type DistillerChatFn, type DistillerChatRequest, type FuseSchedule, type LlmConsolidationPlan, type LlmConsolidationPlanProduct, type LlmDistillerContract, type MintLlmConsolidationPlanResult, type PlanParseRepairs, type SanitizedLlmGroups, } from "./distiller.js";
|
|
13
13
|
export { CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, type ConsolidationDriverEngine, type ConsolidationDriverRunRow, type ConsolidationRunReceipt, type RunMemoryConsolidationOptions, } from "./consolidation-driver.js";
|
|
14
|
+
export type { OriginClearanceRow, OriginClearanceEvent } from "./origin-clearance.js";
|
|
14
15
|
export { MEMORY_ORIGIN_CAUSES } from "./types.js";
|
|
15
|
-
export type { MemoryBackend, MemoryEntry, MemoryEntryFrontmatter, MemoryEntryOrigin, MemoryOriginCause, MemoryEntryDistilled, MemoryEntryDistilledInput, MemoryEntryHeader, ScoredMemoryEntry, NotePatch, PatchReport, MaterializedFile, MemorySessionHandle, HarvestReport, HarvestRejection, HarvestRejectionCode, MemoryAnnouncement, ScanFinding, } from "./types.js";
|
|
16
|
+
export type { MemoryBackend, MemoryEntry, MemoryEntryFrontmatter, MemoryEntryOrigin, MemoryOriginCause, MemoryScopeEnumeration, MemoryEntryDistilled, MemoryEntryDistilledInput, MemoryEntryHeader, ScoredMemoryEntry, NotePatch, PatchReport, MaterializedFile, MemorySessionHandle, HarvestReport, HarvestRejection, HarvestRejectionCode, MemoryAnnouncement, ScanFinding, } from "./types.js";
|
|
16
17
|
export { memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, } from "./memory-backend-contract.js";
|
|
17
18
|
export { SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, type ParsedScopeKey, } from "./scope-contract.js";
|
|
18
19
|
export { migrateScope, type MigrateScopeReport } from "./migrate.js";
|
|
@@ -303,21 +303,35 @@ export function dirCaseFolds(dir) {
|
|
|
303
303
|
}
|
|
304
304
|
function readScopesRecord(controlDir) {
|
|
305
305
|
const path = join(controlDir, SCOPES_FILE);
|
|
306
|
+
const journalPath = `${path}.journal`;
|
|
306
307
|
let raw;
|
|
308
|
+
let sourcePath = journalPath;
|
|
307
309
|
try {
|
|
308
|
-
raw = readFileSync(
|
|
310
|
+
raw = readFileSync(journalPath, "utf8");
|
|
309
311
|
}
|
|
310
312
|
catch (err) {
|
|
311
|
-
if (err.code
|
|
312
|
-
|
|
313
|
-
|
|
313
|
+
if (err.code !== "ENOENT") {
|
|
314
|
+
throw new ControlPlaneCorruptError(`scope registry journal unreadable (${err.code ?? "io error"}) — a committed next state may exist that cannot be proven: ${journalPath}`, { cause: err });
|
|
315
|
+
}
|
|
316
|
+
raw = undefined;
|
|
317
|
+
}
|
|
318
|
+
if (raw === undefined) {
|
|
319
|
+
sourcePath = path;
|
|
320
|
+
try {
|
|
321
|
+
raw = readFileSync(path, "utf8");
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
if (err.code === "ENOENT")
|
|
325
|
+
return {};
|
|
326
|
+
throw new ControlPlaneCorruptError(`scope registry unreadable (${err.code ?? "io error"}): ${path}`, { cause: err });
|
|
327
|
+
}
|
|
314
328
|
}
|
|
315
329
|
let parsed;
|
|
316
330
|
try {
|
|
317
331
|
parsed = JSON.parse(raw);
|
|
318
332
|
}
|
|
319
333
|
catch {
|
|
320
|
-
throw new ControlPlaneCorruptError(`scope registry is unparseable: ${
|
|
334
|
+
throw new ControlPlaneCorruptError(`scope registry is unparseable: ${sourcePath}`);
|
|
321
335
|
}
|
|
322
336
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
323
337
|
throw new ControlPlaneCorruptError(`scope registry has the wrong shape: ${path}`);
|
|
@@ -351,7 +365,7 @@ function lockedScopesUpdate(controlDir, fn) {
|
|
|
351
365
|
const lock = `${file}.lock`;
|
|
352
366
|
const token = acquireSidecarLock(lock, { onDeadline: "throw" });
|
|
353
367
|
try {
|
|
354
|
-
|
|
368
|
+
rollForwardStrictSidecar(file, journal);
|
|
355
369
|
const rec = readScopesRecord(controlDir);
|
|
356
370
|
const { next, result } = fn(rec);
|
|
357
371
|
if (next !== undefined) {
|
|
@@ -246,6 +246,23 @@ export interface NotePatch {
|
|
|
246
246
|
* Ignored on `update`/`delete`. */
|
|
247
247
|
guard?: "absent";
|
|
248
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* design/376 slice ③ (#437③) — a backend's scope-enumeration answer, DISCRIMINATED on purpose:
|
|
251
|
+
* "this backend cannot enumerate" and "this store hosts no scopes" must never share a spelling
|
|
252
|
+
* (an unsupported backend read as `[]` would make every audit sweep over it silently claim a
|
|
253
|
+
* clean store). The capability itself is an OPTIONAL backend face (`listScopes`, probed like the
|
|
254
|
+
* File class's audit faces): a backend without it is answered `supported: false` by
|
|
255
|
+
* `MemoryEngine.listMemoryScopes`, never an empty array. The File leg derives from the store
|
|
256
|
+
* root's durable scope registry; the SQL twins' leg is a store-contracts extension (separate
|
|
257
|
+
* ticket) — until it lands they simply do not implement the face.
|
|
258
|
+
*/
|
|
259
|
+
export type MemoryScopeEnumeration = {
|
|
260
|
+
supported: true;
|
|
261
|
+
scopes: string[];
|
|
262
|
+
} | {
|
|
263
|
+
supported: false;
|
|
264
|
+
reason: string;
|
|
265
|
+
};
|
|
249
266
|
/** Result of {@link MemoryBackend.applyPatches} — applied ops + per-id CAS conflicts (reported, not thrown). */
|
|
250
267
|
export interface PatchReport {
|
|
251
268
|
applied: Array<{
|
package/dist/core/memory.d.ts
CHANGED
|
@@ -477,6 +477,16 @@ export interface NormalizedMemorySpec {
|
|
|
477
477
|
* of the fail-closed `"external"` default. The explicit-config channel for a deployment whose custom
|
|
478
478
|
* tools predate the {@link import("./types.js").ToolSpec.contentOrigin} declaration; it never
|
|
479
479
|
* overrides a tool's OWN declaration. Normalized: trimmed, deduped, empties dropped.
|
|
480
|
+
*
|
|
481
|
+
* NOT the channel for MCP servers (design/378). A per-name entry here keys on the MINTED name — the
|
|
482
|
+
* host has to predict the namespacing and charset normalization — covers nothing a mid-task refresh
|
|
483
|
+
* adds, and says "exempt this undeclared tool" where the fact is "this whole server is mine".
|
|
484
|
+
* {@link import("./types.js").McpServerSpec.contentOrigin} states that fact at the entry, and being
|
|
485
|
+
* a DECLARATION it wins over this allowlist in both directions: a declared server's tools leave this
|
|
486
|
+
* list's reach entirely, an explicit `"external"` declaration included (that is the one way a
|
|
487
|
+
* deployment can pin a server's tools BEYOND this exemption). This list is not retired — it remains
|
|
488
|
+
* the in-register channel for undeclared HOST tools and for a deployment not ready to touch its
|
|
489
|
+
* server entries.
|
|
480
490
|
*/
|
|
481
491
|
trustedTools?: string[];
|
|
482
492
|
/**
|
|
@@ -70,6 +70,7 @@ function syntheticCheckpoint(scope) {
|
|
|
70
70
|
toolName: "SelfCheck",
|
|
71
71
|
args: { probe: true, nested: { depth: 2, list: [1, 2, 3] } },
|
|
72
72
|
boundInputHash: boundInputHashOf({ probe: true, nested: { depth: 2, list: [1, 2, 3] } }),
|
|
73
|
+
hasBidiControls: true,
|
|
73
74
|
batchToolCallIds: [`${scope}:call`],
|
|
74
75
|
completedCallIds: [],
|
|
75
76
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, } from "./permission-rule-model.js";
|
|
2
|
+
import { escapeForDisclosure, hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, } from "./permission-rule-model.js";
|
|
3
3
|
import { errText, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
|
|
4
4
|
export class InMemoryRuleApprovalRecordStore {
|
|
5
5
|
rows = new Map();
|
|
@@ -115,7 +115,7 @@ function approvalRecordDamageOf(rec) {
|
|
|
115
115
|
const seen = new Set();
|
|
116
116
|
for (const member of members) {
|
|
117
117
|
if (!Number.isInteger(member) || member < 0 || rec.candidates[member] === undefined) {
|
|
118
|
-
return `offer ${at} references candidate ${member}, which the record does not carry`;
|
|
118
|
+
return `offer ${at} references candidate ${escapeForDisclosure(member)}, which the record does not carry`;
|
|
119
119
|
}
|
|
120
120
|
const cand = rec.candidates[member];
|
|
121
121
|
if (typeof cand !== "object" || cand === null || typeof cand.rule !== "string") {
|
|
@@ -138,10 +138,14 @@ function approvalRecordDamageOf(rec) {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
else if (offer.segments !== undefined || offer.uncoveredSegments !== undefined) {
|
|
141
|
-
return `${rec.kind} batch offer ${at} must not carry segment metadata — there is no compound command behind it`;
|
|
141
|
+
return `${escapeForDisclosure(rec.kind)} batch offer ${at} must not carry segment metadata — there is no compound command behind it`;
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
for (const [at] of rec.candidates.entries()) {
|
|
146
|
+
if (!claimed.has(at))
|
|
147
|
+
return `candidate ${at} belongs to no offer — every candidate row is claimed by exactly one offer`;
|
|
148
|
+
}
|
|
145
149
|
return undefined;
|
|
146
150
|
}
|
|
147
151
|
export function ruleOffersOfRecord(rec) {
|
|
@@ -325,7 +329,7 @@ function checkEditedRuleText(text, command) {
|
|
|
325
329
|
if (!ruleAdmitsCommand(parsed.rule, command)) {
|
|
326
330
|
return {
|
|
327
331
|
ok: false,
|
|
328
|
-
message: `the edited rule "${parsed.rule.rule}" does not admit the command that was decided ("${command}") — a card's edit may widen how much the rule covers, never move it to a different grant`,
|
|
332
|
+
message: `the edited rule "${parsed.rule.rule}" does not admit the command that was decided ("${escapeForDisclosure(command)}") — a card's edit may widen how much the rule covers, never move it to a different grant`,
|
|
329
333
|
};
|
|
330
334
|
}
|
|
331
335
|
return { ok: true, canonicalRule: parsed.rule.rule };
|
|
@@ -342,7 +346,7 @@ export function precheckEditedRuleText(text, command) {
|
|
|
342
346
|
throw e;
|
|
343
347
|
}
|
|
344
348
|
if (hasUnrenderableCharacters(command)) {
|
|
345
|
-
const e = new Error("precheckEditedRuleText takes a command this lane can read — one carrying control or format characters is
|
|
349
|
+
const e = new Error("precheckEditedRuleText takes a command this lane can read — one carrying control or format characters is refused at this preflight face (likelier a caller wiring fault than a card's command; the in-engine confirmation arm still judges such a card, with the command escaped on its refusal line)");
|
|
346
350
|
e.code = "config.invalid_argument";
|
|
347
351
|
throw e;
|
|
348
352
|
}
|
|
@@ -282,6 +282,40 @@ export declare const SUGGESTION_LEXICON: readonly string[];
|
|
|
282
282
|
* through, so an ordinary rule text reads normally. The result is length-bounded.
|
|
283
283
|
*/
|
|
284
284
|
export declare function escapeForDisclosure(value: unknown): string;
|
|
285
|
+
/**
|
|
286
|
+
* Render raw, untrusted COMMAND text for a display surface — the minimal safe baseline for the seats
|
|
287
|
+
* that carry post-rewrite command bytes verbatim ({@link SegmentRuleSuggestion.segment} is the
|
|
288
|
+
* motivating one; a card's own `command`/`args` echo has the same hazard).
|
|
289
|
+
*
|
|
290
|
+
* WHAT IT DOES, and why the two character families are not handled the same way:
|
|
291
|
+
* · `\p{Cf}` (the bidirectional controls U+061C/U+200E/U+200F/U+202A–U+202E/U+2066–U+2069 and the
|
|
292
|
+
* zero-width formatters ZWSP/ZWNJ/ZWJ/WORD JOINER/SOFT HYPHEN/BOM) is REMOVED. These occupy no
|
|
293
|
+
* width, so deleting them is what makes the rendered line agree with what a reader believes they
|
|
294
|
+
* are reading — a `rm -rf x` that DISPLAYS as something benign is exactly the deception the seat
|
|
295
|
+
* would otherwise hand on. This is the same family
|
|
296
|
+
* {@link import("./tool-policy.js").AskRequest.hasBidiControls} raises its warning bit for; the
|
|
297
|
+
* bit says "the raw bytes carry them", this function is what a surface renders instead.
|
|
298
|
+
* · Controls and line/paragraph separators (C0 incl. TAB and NEWLINE, DEL, C1, U+2028/U+2029) are
|
|
299
|
+
* FOLDED TO A SPACE with runs collapsed, not removed: they occupy width (or a whole line), and
|
|
300
|
+
* deleting them would glue two tokens into one word that reads as a different command. TAB is not
|
|
301
|
+
* excepted here — a one-line display value has no use for one, and a rule text's foldable-tab
|
|
302
|
+
* exception belongs to the VALIDATOR class, not to this one.
|
|
303
|
+
* The result is then capped by code point (never splitting a surrogate pair) with a `…` marker, and
|
|
304
|
+
* carries the shared authority-envelope neutralization every untrusted inline value gets — this text
|
|
305
|
+
* can land on a model-visible line as easily as on a human one.
|
|
306
|
+
*
|
|
307
|
+
* WHAT IT IS NOT. A DISPLAY baseline, never an identity channel and never adjudication input: the
|
|
308
|
+
* transform is LOSSY and NOT injective (two distinct commands can render identically — that is the
|
|
309
|
+
* price of a readable line). Correlate, match, store and adjudicate on the RAW bytes, which the
|
|
310
|
+
* carrying seats keep unchanged: this function neither changes what those seats contain nor what any
|
|
311
|
+
* of them means. A caller that must instead tell two hostile texts APART wants
|
|
312
|
+
* {@link escapeForDisclosure} (which escapes rather than folds, so nothing collides) or
|
|
313
|
+
* {@link hasUnrenderableCharacters} (refuse instead of render).
|
|
314
|
+
*
|
|
315
|
+
* @param text the raw untrusted text (a `String(x)` is applied to anything that is not one).
|
|
316
|
+
* @param maxLen code-point cap for the rendered result; defaults to the disclosure-line budget.
|
|
317
|
+
*/
|
|
318
|
+
export declare function renderUntrustedCommandText(text: unknown, maxLen?: number): string;
|
|
285
319
|
/**
|
|
286
320
|
* Does this text carry a character of the class {@link escapeForDisclosure} exists for — the class
|
|
287
321
|
* {@link CONTROL_CHARS_RE} names? Asked by a caller that must REFUSE such a value rather than render
|
|
@@ -488,7 +522,14 @@ export interface SegmentRuleSuggestion {
|
|
|
488
522
|
readonly command: string;
|
|
489
523
|
/** The (folded) segment this suggestion came from. Raw post-rewrite command bytes — a substring
|
|
490
524
|
* of the same call's `args`/`command` values riding beside it on every surface that carries
|
|
491
|
-
* this seat — so render it with contextual escaping, exactly like those.
|
|
525
|
+
* this seat — so render it with contextual escaping, exactly like those.
|
|
526
|
+
*
|
|
527
|
+
* RAW is the contract here and does not change: this seat carries the bytes, because the bytes are
|
|
528
|
+
* what a rule would be minted from and what a correlation keys on. The rendering side of that
|
|
529
|
+
* contract is {@link renderUntrustedCommandText} — the shipped display baseline (bidi and
|
|
530
|
+
* zero-width controls removed, controls/line separators folded, length capped, envelope-neutralized)
|
|
531
|
+
* a surface can adopt instead of writing its own, since raw command bytes displayed unprocessed can
|
|
532
|
+
* show a reader something other than what they authorize. */
|
|
492
533
|
readonly segment: string;
|
|
493
534
|
}
|
|
494
535
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parsePermissionRule } from "./permission-rules.js";
|
|
2
2
|
import { parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
3
|
+
import { inlineUntrusted } from "./untrusted-text.js";
|
|
3
4
|
export const MAX_RULE_TEXT_CHARS = 512;
|
|
4
5
|
export const BARE_INTERPRETER_NAMES = new Set([
|
|
5
6
|
"node", "deno", "bun", "python", "python2", "python3", "perl", "ruby", "php", "osascript",
|
|
@@ -162,6 +163,17 @@ export function escapeForDisclosure(value) {
|
|
|
162
163
|
});
|
|
163
164
|
return escaped.length <= DISCLOSED_RULE_TEXT_MAX_CHARS ? escaped : `${escaped.slice(0, DISCLOSED_RULE_TEXT_MAX_CHARS)}…`;
|
|
164
165
|
}
|
|
166
|
+
const DISPLAY_STRIP_FORMAT_RE = /\p{Cf}/gu;
|
|
167
|
+
export function renderUntrustedCommandText(text, maxLen = DISCLOSED_RULE_TEXT_MAX_CHARS) {
|
|
168
|
+
let raw;
|
|
169
|
+
try {
|
|
170
|
+
raw = typeof text === "string" ? text : String(text);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return "<unprintable>";
|
|
174
|
+
}
|
|
175
|
+
return inlineUntrusted(raw.replace(DISPLAY_STRIP_FORMAT_RE, ""), maxLen);
|
|
176
|
+
}
|
|
165
177
|
export function hasUnrenderableCharacters(text) {
|
|
166
178
|
return CONTROL_CHARS_RE.test(text);
|
|
167
179
|
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
|
|
23
23
|
import type { Model } from "../../internal/llm.js";
|
|
24
24
|
import type { ThinkingLevel } from "../../internal/harness.js";
|
|
25
|
+
import type { ContextEditMachine } from "../context-edit.js";
|
|
25
26
|
import { type LockedPreflight } from "../locked-config.js";
|
|
26
27
|
import { type ResolvedRole } from "../roles.js";
|
|
27
28
|
import type { SessionStore } from "../session.js";
|
|
@@ -119,6 +120,13 @@ export interface PrepareConfigDoorsResult {
|
|
|
119
120
|
/** owned — the resolved interaction posture (spec > spawning run > deployment); the
|
|
120
121
|
* AskUserQuestion mount and the child-ctx injection read this same value. */
|
|
121
122
|
resolvedInteractionPosture: "interactive" | "headless" | undefined;
|
|
123
|
+
/** owned — the validated/resolved microCompact knob (design/374; {@link resolveMicroCompactKnob}
|
|
124
|
+
* for the door + placement reasoning). The per-run state builder consumes exactly these values —
|
|
125
|
+
* the deps bag is never re-read after this door. */
|
|
126
|
+
microCompactKnob: {
|
|
127
|
+
machine: ContextEditMachine;
|
|
128
|
+
clearOnRejection: boolean;
|
|
129
|
+
};
|
|
122
130
|
/** owned — the resolved role; its `systemPrompt` seat is still read at prompt-input time. */
|
|
123
131
|
resolvedRole: ResolvedRole;
|
|
124
132
|
/** owned — the resolved main model (`resolvedRole.model`, re-exposed as the name every later
|
|
@@ -163,5 +171,18 @@ export interface PrepareConfigDoorsResult {
|
|
|
163
171
|
* the resolver's off states). */
|
|
164
172
|
brainCallGuardrailMs: number | undefined;
|
|
165
173
|
}
|
|
166
|
-
/**
|
|
174
|
+
/** design/374 — the microCompact knob door (坏值响亮度默认律, the sibling knob doors' form:
|
|
175
|
+
* interactionPosture / toolModelGate / staleToolResultOffload). Placement is load-bearing (review
|
|
176
|
+
* r3): it runs in THIS pre-first-await config-doors phase so a malformed declaration refuses
|
|
177
|
+
* BEFORE any resource-bearing preparation — session acquisition, execution-env creation, the MCP
|
|
178
|
+
* dial — whose external effects a late refusal's teardown cannot undo. SINGLE READ per seat, and
|
|
179
|
+
* the CONTAINER is judged first (reviews r1/r2): an accessor-backed bag answering a legal value at
|
|
180
|
+
* one read and junk at another must be caught by whichever read this door takes, and a
|
|
181
|
+
* `microCompact: "cc"` / null / number / array bag makes both property reads answer undefined —
|
|
182
|
+
* it must refuse exactly like a malformed field, never fold to the defaults. The RESOLVED values
|
|
183
|
+
* returned here are immutable and are the state builder's ONLY source — nothing re-reads the bag. */
|
|
184
|
+
export declare function resolveMicroCompactKnob(bag: RunnerDeps["microCompact"]): {
|
|
185
|
+
machine: ContextEditMachine;
|
|
186
|
+
clearOnRejection: boolean;
|
|
187
|
+
};
|
|
167
188
|
export declare function prepareConfigDoors(input: PrepareConfigDoorsInput): PrepareConfigDoorsResult;
|