@sema-agent/core 7.18.0 → 7.19.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 +93 -0
- package/dist/brain/anthropic.js +6 -3
- package/dist/brain/open-responses.js +10 -4
- package/dist/brain/openai.js +5 -2
- package/dist/brain/reasoning.d.ts +13 -13
- package/dist/brain/reasoning.js +2 -2
- package/dist/brain/stream-engine.js +2 -1
- package/dist/brain/stream-shared.d.ts +3 -0
- package/dist/brain/stream-shared.js +1 -0
- package/dist/core/ask-origin.d.ts +16 -0
- package/dist/core/ask-origin.js +8 -0
- package/dist/core/auto-mode-arming.d.ts +6 -0
- package/dist/core/auto-mode-arming.js +5 -5
- package/dist/core/auto-mode-defaults.d.ts +43 -15
- package/dist/core/auto-mode-defaults.js +4 -1
- package/dist/core/auto-mode.d.ts +43 -14
- package/dist/core/auto-mode.js +79 -10
- package/dist/core/checkpoint-store.d.ts +7 -0
- package/dist/core/checkpoint-store.js +2 -2
- package/dist/core/engine-notice.d.ts +30 -0
- package/dist/core/gate-fold.js +5 -1
- package/dist/core/gate-lanes.js +7 -7
- package/dist/core/gate-outcome.d.ts +25 -0
- package/dist/core/gate-outcome.js +7 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +6 -0
- package/dist/core/hooks.d.ts +1 -1
- package/dist/core/read-root-candidate.d.ts +12 -0
- package/dist/core/read-root-candidate.js +24 -0
- package/dist/core/roles.d.ts +9 -1
- package/dist/core/roles.js +4 -0
- package/dist/core/runner/classifier-ladder.d.ts +149 -11
- package/dist/core/runner/classifier-ladder.js +159 -10
- package/dist/core/runner/contracts.d.ts +2 -0
- package/dist/core/runner/denial-limit-arms.js +2 -2
- package/dist/core/runner/prepare-ask-lane.js +1 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +20 -31
- package/dist/core/runner/prepare-config-doors.d.ts +6 -0
- package/dist/core/runner/prepare-config-doors.js +8 -10
- package/dist/core/runner/prepare-project-context.d.ts +5 -1
- package/dist/core/runner/prepare-project-context.js +3 -2
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/resume-apply.js +3 -3
- package/dist/core/runner/run-leg.js +10 -4
- package/dist/core/runner/run-turn-boundary.js +12 -4
- package/dist/core/runner/synthetic-tools.d.ts +67 -29
- package/dist/core/runner/synthetic-tools.js +51 -22
- package/dist/core/runner/turn-attachments.d.ts +43 -12
- package/dist/core/runner/turn-attachments.js +58 -14
- package/dist/core/sensitive-path-policy.d.ts +58 -17
- package/dist/core/sensitive-path-policy.js +33 -5
- package/dist/core/skill-listing-budget.d.ts +112 -0
- package/dist/core/skill-listing-budget.js +58 -0
- package/dist/core/task-spec.d.ts +26 -0
- package/dist/core/tool-policy.d.ts +19 -4
- package/dist/core/tool-policy.js +6 -7
- package/dist/core/tool-spec.d.ts +37 -4
- package/dist/core/types.d.ts +1 -1
- package/dist/core/write-protect.d.ts +8 -3
- package/dist/core/write-protect.js +8 -5
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +27 -0
- package/dist/tools/fs/bash-readonly-classifier.js +59 -6
- package/dist/tools/fs/fs-bash.js +7 -3
- package/dist/tools/fs/read-deny.d.ts +4 -3
- package/dist/tools/fs/safety.d.ts +15 -0
- package/dist/tools/fs/safety.js +7 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +15 -5
- package/wire-changes/core@7.18.0.json +1273 -0
- package/wire-changes/core@7.19.0.json +457 -0
package/dist/core/auto-mode.d.ts
CHANGED
|
@@ -10,6 +10,18 @@ export declare const AUTO_MODE_UNAVAILABLE_CAUSES: readonly ["error", "timeout"]
|
|
|
10
10
|
export type AutoModeUnavailableCause = (typeof AUTO_MODE_UNAVAILABLE_CAUSES)[number];
|
|
11
11
|
/** Whether a value is a member of the unavailable-cause set — the screen a row reader applies to a stored word. */
|
|
12
12
|
export declare function isAutoModeUnavailableCause(v: unknown): v is AutoModeUnavailableCause;
|
|
13
|
+
/**
|
|
14
|
+
* HOW a reply failed to carry a verdict — the closed set behind `parse_error.form`. `missing` = the reader found
|
|
15
|
+
* NO answer (empty text, prose, an answer only inside a thinking block); `conflict` = it found TWO distinct
|
|
16
|
+
* `<block>` answers (raw or thinking-stripped). The split exists for the classify leg's re-ask: a missing
|
|
17
|
+
* answer is asked for again (CC 2.1.259 `Vrr` re-asks an unparseable reply), a conflict is not — it carries a
|
|
18
|
+
* `yes`, and a second attempt that answered `no` would erase it. Not on any wire face; the trace frame and the
|
|
19
|
+
* gate record keep the one word `parse_error`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const AUTO_MODE_PARSE_FAILURES: readonly ["missing", "conflict"];
|
|
22
|
+
export type AutoModeParseFailure = (typeof AUTO_MODE_PARSE_FAILURES)[number];
|
|
23
|
+
/** Whether a value is a member of the parse-failure set. */
|
|
24
|
+
export declare function isAutoModeParseFailure(v: unknown): v is AutoModeParseFailure;
|
|
13
25
|
/**
|
|
14
26
|
* WHY a classification moved DOWN the candidate ladder — the closed set behind
|
|
15
27
|
* {@link AutoModeClassifierRound}'s `fallback.cause`, i.e. the ways a candidate produces NO RULING
|
|
@@ -52,10 +64,13 @@ export type AutoModeVerdict = ({
|
|
|
52
64
|
cause: AutoModeUnavailableCause;
|
|
53
65
|
} & AutoModeRoundAttributed)
|
|
54
66
|
/** The model responded but not in the `<block>…` contract shape. Handled as a BLOCK with the CC
|
|
55
|
-
* parse-failure sentence ({@link
|
|
67
|
+
* parse-failure sentence ({@link classifierParseFailureDenyMessage}), counted by the denial limit. `form`
|
|
68
|
+
* says WHICH way the reply failed ({@link AutoModeParseFailure}): the classify leg re-asks a `missing`
|
|
69
|
+
* reply once and never a `conflict`. */
|
|
56
70
|
| ({
|
|
57
71
|
kind: "parse_error";
|
|
58
72
|
raw: string;
|
|
73
|
+
form: AutoModeParseFailure;
|
|
59
74
|
} & AutoModeRoundAttributed);
|
|
60
75
|
/** The member every verdict arm shares: WHICH MODEL this classification asked and which one answered. */
|
|
61
76
|
export interface AutoModeRoundAttributed {
|
|
@@ -121,23 +136,35 @@ export declare function rungDidNotRule(v: AutoModeVerdict): AutoModeFallbackCaus
|
|
|
121
136
|
* NOT move keeps the CC text byte for byte.
|
|
122
137
|
*/
|
|
123
138
|
export declare function classifierUnavailableDenyMessage(toolName: string, cause: AutoModeUnavailableCause, attribution?: AutoModeClassifierRound): string;
|
|
124
|
-
/** The model-facing deny text for a `parse_error` verdict — CC 2.1.250 `Ure` over the R3t stem: the classifier
|
|
125
|
-
* answered, but not with a verdict, and auto mode blocks what it could not evaluate. */
|
|
126
|
-
export declare const CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE = "Auto mode could not evaluate this action and is blocking it for safety \u2014 the classifier's reply carried no verdict.";
|
|
127
139
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
140
|
+
* The model-facing deny text for a `parse_error` verdict — CC 2.1.259 `i7t` stem ("Auto mode could not evaluate
|
|
141
|
+
* this action and is blocking it for safety"; 2.1.250 `Ure`): the classifier answered, but not with a verdict,
|
|
142
|
+
* and auto mode blocks what it could not evaluate. With a candidate ladder wired the tail NAMES the model whose
|
|
143
|
+
* reply carried no verdict (`attribution.modelUsed`) — a declared departure from upstream, whose card names no
|
|
144
|
+
* model: the person reading this card is the one who can change the catalog, and on a single-model deployment
|
|
145
|
+
* the name is the whole diagnosis. Without attribution the sentence is unchanged. The stem is a stable prefix
|
|
146
|
+
* either way; one writer for both deny sites.
|
|
147
|
+
*/
|
|
148
|
+
export declare function classifierParseFailureDenyMessage(attribution?: AutoModeClassifierRound): string;
|
|
149
|
+
/**
|
|
150
|
+
* Parse the classifier's output contract — the CC 2.1.259 verdict reader (`MTt`, its thinking strip `QTt`, its
|
|
151
|
+
* reason reader `TTt`; unchanged from 2.1.250 `wCe`/`xCe`/`yCe`), plus ONE arm of sema's own:
|
|
130
152
|
* blocked: `<block>yes</block><category>Rule Name</category><reason>[Rule Name] …</reason>`
|
|
131
153
|
* allowed: `<block>no</block>`
|
|
132
154
|
*
|
|
133
155
|
* The verdict is the set of `<block>yes|no` hits, NOT a prefix match:
|
|
134
156
|
* 1. CONFLICT, read on the RAW text (thinking included): `<block>(yes|no)\b` collected over the whole
|
|
135
|
-
* response — more than one distinct answer ⇒ no verdict (`parse_error`). A scratch-space
|
|
136
|
-
* contradicting a final "no" is a contradiction, not an allow.
|
|
137
|
-
* 2. HITS, read on the text with
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
157
|
+
* response — more than one distinct answer ⇒ no verdict (`parse_error`, form `conflict`). A scratch-space
|
|
158
|
+
* "yes" contradicting a final "no" is a contradiction, not an allow.
|
|
159
|
+
* 2. HITS, read on the text with thinking blocks removed ({@link stripThinking}): `<block>(yes|no)\b` with
|
|
160
|
+
* the closing tag OPTIONAL, at ANY position. Two distinct answers ⇒ `conflict`; the one distinct answer ⇒
|
|
161
|
+
* `no` = allow, `yes` = block.
|
|
162
|
+
* 3. BARE ANSWER (sema, ruled 2026-09-15 — small open models drop the tag; recorded as a divergence; a caller
|
|
163
|
+
* that knows the reply is not a complete answer passes `bareAnswer: false` and this arm is closed): zero
|
|
164
|
+
* hits AND the whole thinking-stripped text is an answer by {@link bareAnswerOf} ⇒ that word is the
|
|
165
|
+
* verdict (a bare `yes` blocks with empty category/reason) — unless a `<block>` answer inside the
|
|
166
|
+
* thinking disagrees with it (`conflict`). Zero hits and anything else — prose, a question with its
|
|
167
|
+
* answer, a reply that only answered inside its thinking — ⇒ `parse_error`, form `missing`.
|
|
141
168
|
* So a preamble before the tag, prose after `<block>no`, a missing `</block>` and a reply that repeats
|
|
142
169
|
* its answer all read as the answer they carry; the tag must be spelled `<block>yes` / `<block>no` with
|
|
143
170
|
* no whitespace inside (the word-boundary form — `<block>yesterday` is not a hit) and case does not matter.
|
|
@@ -149,11 +176,13 @@ export declare const CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE = "Auto mode could no
|
|
|
149
176
|
* line from the tool name in that case. Downgrading a stated block to `parse_error` would lose the
|
|
150
177
|
* classifier's explicit verdict on the exact calls it flagged.
|
|
151
178
|
*
|
|
152
|
-
* `parse_error` is fail-closed: the gate handles it as a BLOCK (CC 2.1.
|
|
179
|
+
* `parse_error` is fail-closed: the gate handles it as a BLOCK (CC 2.1.259's own outcome for a reply with no
|
|
153
180
|
* verdict) that the denial limit counts like any other — never a silent allow, and never a question handed
|
|
154
181
|
* to a person on the classifier's behalf.
|
|
155
182
|
*/
|
|
156
|
-
export declare function parseAutoModeResponse(text: string
|
|
183
|
+
export declare function parseAutoModeResponse(text: string, options?: {
|
|
184
|
+
bareAnswer?: boolean;
|
|
185
|
+
}): AutoModeVerdict;
|
|
157
186
|
/** What the decider hands the deployment's `classify` hook (the assembled prompt is the hook's job
|
|
158
187
|
* when it owns assembly; when using {@link buildAutoModePrompt} the decider passes its output). */
|
|
159
188
|
export interface AutoModeClassifyInput {
|
package/dist/core/auto-mode.js
CHANGED
|
@@ -5,6 +5,11 @@ const AUTO_MODE_UNAVAILABLE_CAUSE_SET = new Set(AUTO_MODE_UNAVAILABLE_CAUSES);
|
|
|
5
5
|
export function isAutoModeUnavailableCause(v) {
|
|
6
6
|
return AUTO_MODE_UNAVAILABLE_CAUSE_SET.has(v);
|
|
7
7
|
}
|
|
8
|
+
export const AUTO_MODE_PARSE_FAILURES = ["missing", "conflict"];
|
|
9
|
+
const AUTO_MODE_PARSE_FAILURE_SET = new Set(AUTO_MODE_PARSE_FAILURES);
|
|
10
|
+
export function isAutoModeParseFailure(v) {
|
|
11
|
+
return AUTO_MODE_PARSE_FAILURE_SET.has(v);
|
|
12
|
+
}
|
|
8
13
|
export const AUTO_MODE_FALLBACK_CAUSES = ["error", "timeout", "parse_error"];
|
|
9
14
|
const AUTO_MODE_FALLBACK_CAUSE_SET = new Set(AUTO_MODE_FALLBACK_CAUSES);
|
|
10
15
|
export function isAutoModeFallbackCause(v) {
|
|
@@ -64,19 +69,83 @@ export function classifierUnavailableDenyMessage(toolName, cause, attribution) {
|
|
|
64
69
|
"If it keeps failing, continue with other tasks that don't require this action and come back to it later. " +
|
|
65
70
|
"Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used.");
|
|
66
71
|
}
|
|
67
|
-
export
|
|
68
|
-
|
|
72
|
+
export function classifierParseFailureDenyMessage(attribution) {
|
|
73
|
+
const stem = "Auto mode could not evaluate this action and is blocking it for safety";
|
|
74
|
+
return attribution === undefined ? `${stem} — the classifier's reply carried no verdict.` : `${stem} — the classifier model "${attribution.modelUsed}" replied without a verdict.`;
|
|
75
|
+
}
|
|
76
|
+
function stripThinking(text) {
|
|
77
|
+
return text.replace(/<(thinking|think)>[\s\S]*?<\/\1>/gi, "").replace(/<(thinking|think)>[\s\S]*$/i, "");
|
|
78
|
+
}
|
|
79
|
+
const BARE_ANSWER_WRAPPERS = [
|
|
80
|
+
["**", "**"],
|
|
81
|
+
["__", "__"],
|
|
82
|
+
["*", "*"],
|
|
83
|
+
["_", "_"],
|
|
84
|
+
["`", "`"],
|
|
85
|
+
['"', '"'],
|
|
86
|
+
["'", "'"],
|
|
87
|
+
["«", "»"],
|
|
88
|
+
];
|
|
89
|
+
function unwrapBareAnswer(text) {
|
|
90
|
+
let s = text;
|
|
91
|
+
for (;;) {
|
|
92
|
+
const before = s;
|
|
93
|
+
s = s.trim().replace(/[.!]+$/, "").trim();
|
|
94
|
+
for (const [open, close] of BARE_ANSWER_WRAPPERS) {
|
|
95
|
+
if (s.length > open.length + close.length && s.startsWith(open) && s.endsWith(close)) {
|
|
96
|
+
s = s.slice(open.length, s.length - close.length);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (s === before)
|
|
101
|
+
return s;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function bareAnswerOf(text) {
|
|
105
|
+
const visible = text.replace(/[\u200b-\u200d\ufeff]/g, "").trim();
|
|
106
|
+
if (visible.includes("```") || visible.includes("\n"))
|
|
107
|
+
return undefined;
|
|
108
|
+
let s = unwrapBareAnswer(visible);
|
|
109
|
+
const labelled = labelledAnswer(s);
|
|
110
|
+
if (labelled !== undefined)
|
|
111
|
+
s = unwrapBareAnswer(labelled);
|
|
112
|
+
const word = /^(yes|no)$/i.exec(s)?.[1]?.toLowerCase();
|
|
113
|
+
return word === "yes" || word === "no" ? word : undefined;
|
|
114
|
+
}
|
|
115
|
+
function labelledAnswer(s) {
|
|
116
|
+
const label = "(?:final[ \\t]+)?(?:block|answer|verdict|decision)";
|
|
117
|
+
for (const [open, close] of [["", ""], ...BARE_ANSWER_WRAPPERS]) {
|
|
118
|
+
const re = new RegExp(`^${escapeRe(open)}${label}${escapeRe(close)}[ \\t]*[:=][ \\t]*(.*)$`, "i");
|
|
119
|
+
const m = re.exec(s);
|
|
120
|
+
if (m !== null)
|
|
121
|
+
return m[1];
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
function escapeRe(s) {
|
|
126
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
127
|
+
}
|
|
128
|
+
export function parseAutoModeResponse(text, options) {
|
|
69
129
|
const rawAnswers = new Set([...text.matchAll(/<block>(yes|no)\b/gi)].map((m) => m[1].toLowerCase()));
|
|
70
130
|
if (rawAnswers.size > 1)
|
|
71
|
-
return { kind: "parse_error", raw: text };
|
|
72
|
-
const stripped = text
|
|
131
|
+
return { kind: "parse_error", raw: text, form: "conflict" };
|
|
132
|
+
const stripped = stripThinking(text);
|
|
73
133
|
const hits = [...stripped.matchAll(/<block>(yes|no)\b(<\/block>)?/gi)];
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
134
|
+
let answer;
|
|
135
|
+
if (hits.length > 0) {
|
|
136
|
+
const answers = new Set(hits.map((m) => m[1].toLowerCase()));
|
|
137
|
+
if (answers.size !== 1)
|
|
138
|
+
return { kind: "parse_error", raw: text, form: "conflict" };
|
|
139
|
+
answer = answers.has("yes") ? "yes" : "no";
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
answer = options?.bareAnswer === false ? undefined : bareAnswerOf(stripped);
|
|
143
|
+
if (answer === undefined)
|
|
144
|
+
return { kind: "parse_error", raw: text, form: "missing" };
|
|
145
|
+
if (rawAnswers.size > 0 && !rawAnswers.has(answer))
|
|
146
|
+
return { kind: "parse_error", raw: text, form: "conflict" };
|
|
147
|
+
}
|
|
148
|
+
if (answer === "no")
|
|
80
149
|
return { kind: "allow" };
|
|
81
150
|
const category = /<category>([\s\S]*?)<\/category>/i.exec(stripped)?.[1]?.trim() ?? "";
|
|
82
151
|
const reason = /<reason>([\s\S]*?)<\/reason>/.exec(stripped)?.[1]?.trim() ?? "";
|
|
@@ -369,6 +369,13 @@ export interface PendingSteerInput {
|
|
|
369
369
|
inputId?: string;
|
|
370
370
|
priority?: SystemInjectionPriority;
|
|
371
371
|
}
|
|
372
|
+
/** True ONLY for a PLAIN data record (not null / array / class-instance / proxy-with-odd-prototype) — the only
|
|
373
|
+
* shape we safely digest. Adversarial args (a proxy, a getter that throws) must never crash the mint, so this +
|
|
374
|
+
* {@link safeDataValue}/{@link safeKeys} make {@link buildRiskDescriptor} fully TOTAL. */
|
|
375
|
+
export declare function isPlainRecord(x: unknown): x is Record<string, unknown>;
|
|
376
|
+
/** Read `obj[key]` as a DATA property only — NEVER invokes a getter/accessor (so a throwing getter can't escape
|
|
377
|
+
* AND a non-deterministic getter can't vary the digest); a trapped descriptor read ⇒ undefined. */
|
|
378
|
+
export declare function safeDataValue(obj: Record<string, unknown>, key: string): unknown;
|
|
372
379
|
/**
|
|
373
380
|
* design/80 §D-E: build the DETERMINISTIC, REDACTED {@link RiskDescriptor} for an escalation checkpoint at
|
|
374
381
|
* MINT. Pure — a function ONLY of (`toolName`, `args`, the D-2 `safety` axis, the `shellGated` flag); it reads
|
|
@@ -73,7 +73,7 @@ export const MAX_PENDING_STEER_CHARS = 16_000;
|
|
|
73
73
|
export const PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES = 48_000;
|
|
74
74
|
export const MAX_PENDING_STEER_ENTRIES = Math.floor(PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES / MAX_PENDING_STEER_CHARS);
|
|
75
75
|
export const PENDING_STEER_FROZEN_FIELDS = ["text", "trusted", "actor", "seq", "inputId", "priority"];
|
|
76
|
-
function isPlainRecord(x) {
|
|
76
|
+
export function isPlainRecord(x) {
|
|
77
77
|
try {
|
|
78
78
|
if (x === null || typeof x !== "object" || Array.isArray(x))
|
|
79
79
|
return false;
|
|
@@ -84,7 +84,7 @@ function isPlainRecord(x) {
|
|
|
84
84
|
return false;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
function safeDataValue(obj, key) {
|
|
87
|
+
export function safeDataValue(obj, key) {
|
|
88
88
|
try {
|
|
89
89
|
const d = Object.getOwnPropertyDescriptor(obj, key);
|
|
90
90
|
return d && "value" in d ? d.value : undefined;
|
|
@@ -23,6 +23,22 @@ export interface EngineNotice {
|
|
|
23
23
|
* - `"config.tool_face_undeclared"` / `"config.tool_face_invalid"` (design/388 §2.7) — an MCP server's
|
|
24
24
|
* `toolFaces` overlay names a tool the listing does not carry / does not fit the tool's materialized
|
|
25
25
|
* schema (the tool mounts bare); once per leg per tool; `detail: { server, tool, problem?, sessionId, runId }`.
|
|
26
|
+
* - `"skills.listing_truncated"` — the `<skills>` disclosure block did not fit its byte budget, so
|
|
27
|
+
* at least one skill is listed without its description (`nameOnly`) or, in the pathological case,
|
|
28
|
+
* not listed at all (`dropped`); the `Skill` tool still serves every one of them. Audience
|
|
29
|
+
* `operator` — raising `TaskSpec.skillListingBudgetFraction`, lowering `skillListingMaxDescChars`
|
|
30
|
+
* or declaring fewer skills are all deployment acts. `detail: { budgetBytes,
|
|
31
|
+
* requestedBudgetBytes, descMaxChars, total, kept, nameOnly: [name], dropped: [name], sessionId }`
|
|
32
|
+
* — `requestedBudgetBytes` is what the declared fraction asked for BEFORE the delivery lane's
|
|
33
|
+
* structural clamp, and differs from `budgetBytes` only when that clamp bound, so a listing
|
|
34
|
+
* shortened by the lane is distinguishable from one shortened by the deployment's own fraction.
|
|
35
|
+
* Announced by the lane that DELIVERS a listing frame — the leg's first-frame producer, or the
|
|
36
|
+
* turn boundary that a drift (or a resume leg's initial listing) rides — once per delivered frame
|
|
37
|
+
* the budget actually degraded, and never for one that fit. Every count is read off that frame, so
|
|
38
|
+
* `total` is the whole roster on an initial listing and the newly announced set on a drift frame.
|
|
39
|
+
* A run whose deployment turned the listing off (`attachments.skillsListing: false`) emits nothing,
|
|
40
|
+
* since nothing is rendered. There is NO dedup key: each notice is one delivered frame`s cost, and
|
|
41
|
+
* a frame is delivered (and committed) once.
|
|
26
42
|
* - `"tool_result.offload_put_failed"` (#167) — a clear-with-offload persist's fire-and-forget put
|
|
27
43
|
* failed; THIS attempt stored nothing (the failure arm reports, it never re-inserts under the
|
|
28
44
|
* ref) — an earlier attempt of the same idempotent re-put may already have stored the row, so
|
|
@@ -118,6 +134,20 @@ export interface EngineNotice {
|
|
|
118
134
|
* `detail: { from, to, cause, sessionId?, runId? }` — `cause` is the `AutoModeFallbackCause` word, the
|
|
119
135
|
* two ids are the rungs. Dedup key: (code, from, cause) PER RUN; a second CAUSE on the same rung is
|
|
120
136
|
* its own line.
|
|
137
|
+
* - `"classifier.role_defaulted"` (`src/core/runner/classifier-ladder.ts`, the seat resolver) — no
|
|
138
|
+
* `classifier` role names a model, so another model STANDS IN for the seat. `detail: { model, reason,
|
|
139
|
+
* thinkingOffSpelled, sessionId?, runId? }` — `reason` ∈ `tier_below_main` (the nearest directly-bound
|
|
140
|
+
* tier below the run's main model) / `tier_table` (the static `flash` row, a main bound to no tier) /
|
|
141
|
+
* `role_fallback` (the `summarize → default` chain, no tiers) / `main_only` (the main model is the only
|
|
142
|
+
* model there is) / `role_unresolvable` (the role named a model the catalog cannot resolve; the main
|
|
143
|
+
* stands in); `thinkingOffSpelled` = whether that model's wire can carry a disable key. Minted
|
|
144
|
+
* AFTER the route pre-flight, so it names the seat actually used. Once per run.
|
|
145
|
+
* - `"classifier.contract_broken"` (same file, the round observer) — ONE model answered
|
|
146
|
+
* `AUTO_MODE_CLASSIFIER_CONTRACT_BROKEN_AFTER` CONSECUTIVE classifications without a verdict (each already
|
|
147
|
+
* re-asked once by the leg). `detail: { model, rung, consecutive, sessionId?, runId? }` — `rung` is the
|
|
148
|
+
* ladder position (0 the seat, 1 the model behind it), since two rungs may share an id. A ruling from
|
|
149
|
+
* that rung resets its count; an `unavailable` round neither counts nor resets. Once per run per rung,
|
|
150
|
+
* and by construction before the denial limit's third block hands the user its card.
|
|
121
151
|
* - `"route.base_url_changed_key_unchanged"` (key↔URL pairing) — `Runner.swapDeps` moved a
|
|
122
152
|
* same-name entry's `baseUrl` while its Model-visible credential half (auth-bearing headers)
|
|
123
153
|
* did not change: legal (a provider changing domains), but worth one loud line — if the
|
package/dist/core/gate-fold.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
2
2
|
import { PROBE_REASON_MAX, normalizeProbeCause } from "./checkpoint-store.js";
|
|
3
|
+
import { normalizeReadRootCandidate } from "./read-root-candidate.js";
|
|
3
4
|
import { traceHookCrash } from "./runner/gate-exit.js";
|
|
4
5
|
function withProbeTimeout(p, ms, signal) {
|
|
5
6
|
if (ms === undefined && signal === undefined)
|
|
@@ -80,6 +81,7 @@ export async function runGateFold(pass) {
|
|
|
80
81
|
let probeReason;
|
|
81
82
|
let probeCause;
|
|
82
83
|
let probeMandated;
|
|
84
|
+
let probeReadRootCandidate;
|
|
83
85
|
if (input.irreversibility === "maybe" && input.reversibilityProbe) {
|
|
84
86
|
let reversible = false;
|
|
85
87
|
const probeArgs = pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput;
|
|
@@ -105,6 +107,7 @@ export async function runGateFold(pass) {
|
|
|
105
107
|
probeCause = normalizeProbeCause(verdict?.cause);
|
|
106
108
|
if (verdict?.mandated === true)
|
|
107
109
|
probeMandated = true;
|
|
110
|
+
probeReadRootCandidate = normalizeReadRootCandidate(verdict?.readRootCandidate);
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
113
|
catch (err) {
|
|
@@ -123,6 +126,7 @@ export async function runGateFold(pass) {
|
|
|
123
126
|
...(probeReason !== undefined ? { probeReason } : {}),
|
|
124
127
|
...(probeCause !== undefined ? { probeCause } : {}),
|
|
125
128
|
...(probeMandated === true ? { probeMandated: true } : {}),
|
|
129
|
+
...(probeReadRootCandidate !== undefined ? { probeReadRootCandidate } : {}),
|
|
126
130
|
};
|
|
127
131
|
pass.tightenedBy = input.shellGated === true ? "shell_gate" : "safety";
|
|
128
132
|
}
|
|
@@ -139,7 +143,7 @@ export async function runGateFold(pass) {
|
|
|
139
143
|
pass.tightenedBy = "safety";
|
|
140
144
|
}
|
|
141
145
|
if (pass.decision.action === "allow" && input.writeProtectionCheck !== undefined) {
|
|
142
|
-
const writeProtectedHit = input.writeProtectionCheck(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, pass.req.face);
|
|
146
|
+
const writeProtectedHit = input.writeProtectionCheck(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, pass.req.face, input.trackedCwd?.());
|
|
143
147
|
if (writeProtectedHit !== null) {
|
|
144
148
|
pass.decision = {
|
|
145
149
|
action: "ask",
|
package/dist/core/gate-lanes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { attestClassifierDenyCause, classifierDenyCauseOf, decisionText, describeThrown } from "./tool-policy.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { classifierMayAnswer, persistedRuleMandateOf, probeBoundaryMandate, safetyAxisOf, stampAskOrigin } from "./ask-origin.js";
|
|
3
|
+
import { classifierParseFailureDenyMessage, classifierUnavailableDenyMessage, denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
|
|
4
4
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
5
5
|
import { isRuleBehavior } from "./permission-rule-model.js";
|
|
6
6
|
import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
@@ -346,7 +346,7 @@ export async function runGateLanes(pass) {
|
|
|
346
346
|
if (pass.decision.action === "ask") {
|
|
347
347
|
const { ruleStoreUnreadable: _selfDeclared, ...unstamped } = pass.decision;
|
|
348
348
|
const stamped = { ...unstamped, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}), ...(originFacts.ruleStore !== undefined ? { ruleStoreUnreadable: originFacts.ruleStore } : {}) };
|
|
349
|
-
pass.decision =
|
|
349
|
+
pass.decision = stampAskOrigin(stamped, originFacts);
|
|
350
350
|
}
|
|
351
351
|
if (allowLayerMayClear() && pass.decision.action === "ask" && persistedRuleMandate === undefined && pass.decision.matchedAskRule === undefined && pass.req.toolName === COMMAND_RULE_TOOL) {
|
|
352
352
|
const commandParam = catalogRuleFaceOf(COMMAND_RULE_TOOL)?.primaryParams[0] ?? "command";
|
|
@@ -387,7 +387,7 @@ export async function runGateLanes(pass) {
|
|
|
387
387
|
}, "unavailable", pass.req);
|
|
388
388
|
}
|
|
389
389
|
else {
|
|
390
|
-
const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") :
|
|
390
|
+
const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : classifierParseFailureDenyMessage(verdict.classifier);
|
|
391
391
|
const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
|
|
392
392
|
const tracked = input.autoMode.denialTracking?.recordBlock();
|
|
393
393
|
if (tracked?.limitReached === true) {
|
|
@@ -398,7 +398,7 @@ export async function runGateLanes(pass) {
|
|
|
398
398
|
requiresRealApproval: true,
|
|
399
399
|
denialLimitFallback: tracked.fallback,
|
|
400
400
|
};
|
|
401
|
-
pass.decision =
|
|
401
|
+
pass.decision = stampAskOrigin(fallbackAsk, originFacts);
|
|
402
402
|
}
|
|
403
403
|
else {
|
|
404
404
|
const denied = {
|
|
@@ -658,7 +658,7 @@ export async function runGateLanes(pass) {
|
|
|
658
658
|
const { ruleStoreUnreadable: _selfDeclaredOnEdit, ...recheckUnstamped } = recheck;
|
|
659
659
|
const editAskSnapshot = { ...recheckUnstamped, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
|
|
660
660
|
markedThisRound = undefined;
|
|
661
|
-
const editAskDecision = { ...editAskSnapshot, ...(editRuleStore !== undefined ? { ruleStoreUnreadable: editRuleStore } : {})
|
|
661
|
+
const editAskDecision = stampAskOrigin({ ...editAskSnapshot, ...(editRuleStore !== undefined ? { ruleStoreUnreadable: editRuleStore } : {}) }, askOriginFacts(editOrg.org, editRuleStore));
|
|
662
662
|
const editAskReq = { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
|
|
663
663
|
const rr = await (callSignal !== undefined ? resolveAsk(editAskDecision, editAskReq, callSignal) : resolveAsk(editAskDecision, editAskReq));
|
|
664
664
|
ledger.fromResolved(rr, editAskDecision, toolName);
|
|
@@ -677,7 +677,7 @@ export async function runGateLanes(pass) {
|
|
|
677
677
|
editRewrittenSinceHuman = false;
|
|
678
678
|
}
|
|
679
679
|
if (editDenied === undefined && editRewrittenSinceHuman && input.writeProtectionCheck !== undefined) {
|
|
680
|
-
const editHit = input.writeProtectionCheck(toolName, editArgs, pass.req.face);
|
|
680
|
+
const editHit = input.writeProtectionCheck(toolName, editArgs, pass.req.face, input.trackedCwd?.());
|
|
681
681
|
if (editHit !== null) {
|
|
682
682
|
editDenied = {
|
|
683
683
|
action: "deny",
|
|
@@ -93,6 +93,31 @@ export type Settlement = {
|
|
|
93
93
|
};
|
|
94
94
|
when: number;
|
|
95
95
|
};
|
|
96
|
+
/** The person arm of {@link Settlement} — the shape the two human words inhabit. Named so a site that
|
|
97
|
+
* both composes one and quotes its note can read the seat off the record instead of re-deriving it. */
|
|
98
|
+
export type PersonSettlement = Extract<Settlement, {
|
|
99
|
+
kind: "human_allowed" | "human_refused";
|
|
100
|
+
}>;
|
|
101
|
+
/**
|
|
102
|
+
* A decider's NOTE, normalised — the one rule for "did the decider say anything": empty text is a
|
|
103
|
+
* decider who attached nothing and reads as absent wherever the note travels. Whitespace-only text is
|
|
104
|
+
* not empty by this rule and stays a note: that half is PINNED CURRENT BEHAVIOUR, not a promise —
|
|
105
|
+
* nothing here requires a note to carry content, and narrowing it is a separate decision with its own
|
|
106
|
+
* consumers to warn.
|
|
107
|
+
*/
|
|
108
|
+
export declare function decidersNote(text: string | undefined): string | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* THE mint for a person's settlement: the synchronous resolver's human arms and the durable decide
|
|
111
|
+
* lane's person arm compose their record here, so one refusal plus one reason text cannot end as two
|
|
112
|
+
* different records depending on which path carried it.
|
|
113
|
+
*
|
|
114
|
+
* @contract settlement.note — the `note` seat is filled exactly when the decider attached a reason, and
|
|
115
|
+
* its absence is how the record says the refusal was bare (the runner reads that absence, and nothing
|
|
116
|
+
* else, as the issuing batch's control-flow boundary). Empty text folds to absence HERE, once, for
|
|
117
|
+
* every path that fills the seat ({@link decidersNote}); a site that hand-builds the record instead is
|
|
118
|
+
* a second answer to the same question, which is how one path came to halt a batch the other let run.
|
|
119
|
+
*/
|
|
120
|
+
export declare function settledByPerson(kind: PersonSettlement["kind"], approver: string | undefined, note?: string): PersonSettlement;
|
|
96
121
|
/** Which settlement kinds are REFUSALS (the ask ended as a deny). Total over the closed set — the fence
|
|
97
122
|
* below refuses a member without a row — so invariant I2 is a table lookup, not a list in a condition. */
|
|
98
123
|
export declare const SETTLEMENT_IS_REFUSAL: {
|
|
@@ -18,6 +18,13 @@ const SETTLEMENT_KIND_SET = new Set(SETTLEMENT_KINDS);
|
|
|
18
18
|
export function isSettlementKind(v) {
|
|
19
19
|
return SETTLEMENT_KIND_SET.has(v);
|
|
20
20
|
}
|
|
21
|
+
export function decidersNote(text) {
|
|
22
|
+
return text === "" ? undefined : text;
|
|
23
|
+
}
|
|
24
|
+
export function settledByPerson(kind, approver, note) {
|
|
25
|
+
const given = decidersNote(note);
|
|
26
|
+
return { kind, who: { party: "person", ...(approver !== undefined ? { approver } : {}) }, when: Date.now(), ...(given !== undefined ? { note: given } : {}) };
|
|
27
|
+
}
|
|
21
28
|
export const SETTLEMENT_IS_REFUSAL = {
|
|
22
29
|
human_allowed: false,
|
|
23
30
|
human_refused: true,
|
|
@@ -104,7 +104,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
104
104
|
* src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
|
|
105
105
|
* no longer minted.
|
|
106
106
|
*/
|
|
107
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
107
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "tool_result.offload_put_failed"];
|
|
108
108
|
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
109
109
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
110
110
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -119,6 +119,8 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
119
119
|
"peer.idle_subscription",
|
|
120
120
|
"classifier.denial_limit",
|
|
121
121
|
"classifier.fallback",
|
|
122
|
+
"classifier.role_defaulted",
|
|
123
|
+
"classifier.contract_broken",
|
|
122
124
|
"checkpoint.execution_outcome_unrecorded",
|
|
123
125
|
"delegation.transcript_integrity",
|
|
124
126
|
"delegation.ask_unresolvable",
|
|
@@ -151,6 +153,7 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
151
153
|
"task.interrupt_unconsumed",
|
|
152
154
|
"memory.capture_opted_out",
|
|
153
155
|
"memory.capture_optout_unpersisted",
|
|
156
|
+
"skills.listing_truncated",
|
|
154
157
|
"tool_result.offload_put_failed",
|
|
155
158
|
];
|
|
156
159
|
const NOTICE_AUDIENCE_TABLE = {
|
|
@@ -164,6 +167,8 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
164
167
|
"task.user_followup_undrained": "user",
|
|
165
168
|
"classifier.denial_limit": "user",
|
|
166
169
|
"classifier.fallback": "operator",
|
|
170
|
+
"classifier.role_defaulted": "operator",
|
|
171
|
+
"classifier.contract_broken": "operator",
|
|
167
172
|
"task.turn_interrupted": "user",
|
|
168
173
|
"steering.parked_input_blocked": "user",
|
|
169
174
|
"task.halt_unconsumed": "user",
|
|
@@ -209,6 +214,7 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
209
214
|
"route.fallback_to_primary": "operator",
|
|
210
215
|
"route.classifier_unroutable": "operator",
|
|
211
216
|
"route.base_url_changed_key_unchanged": "operator",
|
|
217
|
+
"skills.listing_truncated": "operator",
|
|
212
218
|
"tool_result.offload_put_failed": "operator",
|
|
213
219
|
};
|
|
214
220
|
export const NOTICE_AUDIENCE = NOTICE_AUDIENCE_TABLE;
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -1354,7 +1354,7 @@ export interface ToolGateInput {
|
|
|
1354
1354
|
* (the write-protection tighten below). Absent ⇒ the deployment replaced the table with `[]` (or
|
|
1355
1355
|
* the caller runs the gate without one) and the decision path is byte-identical.
|
|
1356
1356
|
*/
|
|
1357
|
-
writeProtectionCheck?: (toolName: string, args: unknown, face?: import("./tool-registry.js").ToolCallLike["face"]) => import("./write-protect.js").WriteProtectedHit | null;
|
|
1357
|
+
writeProtectionCheck?: (toolName: string, args: unknown, face?: import("./tool-registry.js").ToolCallLike["face"], cwd?: string) => import("./write-protect.js").WriteProtectedHit | null;
|
|
1358
1358
|
/**
|
|
1359
1359
|
* design/143 批2 (A, CC 2.1.207 auto mode): when present, a surviving `ask` is routed to the
|
|
1360
1360
|
* small-model policy CLASSIFIER before any human/durable resolution:
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The display bound on a grant candidate's directory: generous next to a real `PATH_MAX`, tight enough
|
|
2
|
+
* that the seat cannot become an unbounded prose channel onto an approval card. */
|
|
3
|
+
export declare const READ_ROOT_CANDIDATE_DIR_MAX = 1024;
|
|
4
|
+
/**
|
|
5
|
+
* The validator for a probe's {@link import("./tool-spec.js").ReversibilityVerdict.readRootCandidate}: the
|
|
6
|
+
* errand `normalizeProbeCause` does for the cause, fail-closed on the disclosure alone — a malformed value
|
|
7
|
+
* yields `undefined`, the ask stands, the card carries no offer. It REFUSES where the cause NEUTRALIZES,
|
|
8
|
+
* per `shell.read_boundary.grant_candidate_is_the_grant`: the cause is prose, so folding a separator out of
|
|
9
|
+
* it keeps it true, while this string IS the grant, and a card showing one spelling while the deployment
|
|
10
|
+
* applies another is the failure the seat prevents.
|
|
11
|
+
*/
|
|
12
|
+
export declare function normalizeReadRootCandidate(value: unknown): import("./tool-spec.js").ReadRootGrantCandidate | undefined;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isPlainRecord, safeDataValue } from "./checkpoint-store.js";
|
|
2
|
+
import { hasWin32AliasedComponent, isAbsolutePathForm, normalizeAbsPathLexically, pathFamilyOf, win32NamespaceScreen } from "../tools/fs/safety.js";
|
|
3
|
+
export const READ_ROOT_CANDIDATE_DIR_MAX = 1024;
|
|
4
|
+
export function normalizeReadRootCandidate(value) {
|
|
5
|
+
if (!isPlainRecord(value))
|
|
6
|
+
return undefined;
|
|
7
|
+
if (safeDataValue(value, "clearsThisAsk") !== true)
|
|
8
|
+
return undefined;
|
|
9
|
+
const dir = safeDataValue(value, "dir");
|
|
10
|
+
if (typeof dir !== "string" || dir.length === 0 || dir.length > READ_ROOT_CANDIDATE_DIR_MAX)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (/[\u0000-\u001f\u007f\u2028\u2029]/.test(dir))
|
|
13
|
+
return undefined;
|
|
14
|
+
if (!isAbsolutePathForm(dir))
|
|
15
|
+
return undefined;
|
|
16
|
+
const folded = normalizeAbsPathLexically(dir);
|
|
17
|
+
if (folded !== dir || normalizeAbsPathLexically(`${dir}/..`) === dir)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (hasWin32AliasedComponent(dir))
|
|
20
|
+
return undefined;
|
|
21
|
+
if (!win32NamespaceScreen(dir, pathFamilyOf({ root: dir })).ok)
|
|
22
|
+
return undefined;
|
|
23
|
+
return { dir, clearsThisAsk: true };
|
|
24
|
+
}
|
package/dist/core/roles.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "../internal/harness-types.js";
|
|
2
2
|
import type { Model } from "../internal/llm.js";
|
|
3
|
-
import type { ModelRef, ModelRole, ModelRoles } from "./types.js";
|
|
3
|
+
import type { ModelRef, ModelRole, ModelRoles, RoleSpec } from "./types.js";
|
|
4
4
|
export type { ModelRole, RoleSpec, ModelRoles } from "./types.js";
|
|
5
5
|
/** Resolve a string ModelRef against the catalog (a Model object passes through). */
|
|
6
6
|
export declare function resolveModel(ref: ModelRef, models?: Record<string, Model>): Model;
|
|
@@ -87,6 +87,14 @@ export declare function parseModelMention(text: string, allowedNames: Iterable<s
|
|
|
87
87
|
* (即使 catalog 恰有裸模型名 `pro`/`flash` 也不入档位路由——INERT 靠配置成立,不靠键名运气)。
|
|
88
88
|
*/
|
|
89
89
|
export declare const ROLE_TIER_DEFAULTS: Readonly<Partial<Record<ModelRole, string>>>;
|
|
90
|
+
/**
|
|
91
|
+
* Whether a RoleSpec NAMES a model — a string ref, a Model object, or a config object carrying `model` or
|
|
92
|
+
* `select` — as opposed to a preset-only object (`{ thinking }`, `{ systemPrompt }`) that shapes the role's
|
|
93
|
+
* request but leaves its model to the defaults. The classifier seat's own default (the tier below the run's
|
|
94
|
+
* main model, `classifier-ladder.ts`) applies exactly when this is false: a `{select}` keeps its selection, a
|
|
95
|
+
* preset-only role does not disable the relative pick. One reader, the same parts {@link specParts} reads.
|
|
96
|
+
*/
|
|
97
|
+
export declare function roleSpecNamesModel(spec: RoleSpec | undefined): boolean;
|
|
90
98
|
export interface ResolvedRole {
|
|
91
99
|
model: Model;
|
|
92
100
|
/** The effective thinking level: explicit `spec.thinking` wins, else the role's default. */
|
package/dist/core/roles.js
CHANGED
|
@@ -130,6 +130,10 @@ function specParts(s) {
|
|
|
130
130
|
const cfg = s;
|
|
131
131
|
return { model: cfg.model, select: cfg.select, thinking: cfg.thinking, systemPrompt: cfg.systemPrompt };
|
|
132
132
|
}
|
|
133
|
+
export function roleSpecNamesModel(spec) {
|
|
134
|
+
const parts = specParts(spec);
|
|
135
|
+
return parts.model !== undefined || parts.select !== undefined;
|
|
136
|
+
}
|
|
133
137
|
function toConfig(s) {
|
|
134
138
|
if (s == null) {
|
|
135
139
|
return undefined;
|