@sema-agent/core 7.42.1 → 7.44.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 +173 -0
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +3 -1
- package/dist/agents/subagent.js +5 -3
- package/dist/agents/teacher.js +1 -1
- package/dist/agents/verify.js +2 -0
- package/dist/brain/errors.d.ts +23 -2
- package/dist/brain/errors.js +26 -2
- package/dist/brain/stream-engine.d.ts +2 -1
- package/dist/brain/stream-engine.js +10 -9
- package/dist/core/allow-entry.d.ts +50 -31
- package/dist/core/allow-entry.js +100 -37
- package/dist/core/ask-origin.d.ts +16 -5
- package/dist/core/ask-origin.js +11 -3
- package/dist/core/ask-unresolvable-notice.d.ts +3 -2
- package/dist/core/ask-unresolvable-notice.js +1 -1
- package/dist/core/auto-mode.d.ts +5 -4
- package/dist/core/auto-mode.js +3 -1
- package/dist/core/checkpoint-store.d.ts +44 -20
- package/dist/core/checkpoint-store.js +10 -16
- package/dist/core/child-clamp-seats.d.ts +15 -1
- package/dist/core/child-clamp-seats.js +6 -2
- package/dist/core/classifier-unavailable-arm.d.ts +42 -23
- package/dist/core/classifier-unavailable-arm.js +14 -3
- package/dist/core/decision-receipt.js +2 -0
- package/dist/core/engine-notice.d.ts +16 -14
- package/dist/core/engine-notice.js +4 -3
- package/dist/core/gate-lanes.js +37 -29
- package/dist/core/governance-codes.d.ts +4 -3
- package/dist/core/governance-codes.js +1 -0
- package/dist/core/hooks.d.ts +6 -1
- package/dist/core/hooks.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +2 -2
- package/dist/core/memory-engine/engine.js +7 -14
- package/dist/core/memory-engine/file-backend.d.ts +2 -2
- package/dist/core/memory-engine/file-backend.js +5 -7
- package/dist/core/memory-engine/layout.d.ts +37 -4
- package/dist/core/memory-engine/layout.js +34 -5
- package/dist/core/mode-policy.d.ts +5 -4
- package/dist/core/mode-policy.js +4 -0
- package/dist/core/permission-rule-consent.d.ts +3 -2
- package/dist/core/permission-rule-consent.js +25 -20
- package/dist/core/permission-rule-model.d.ts +47 -7
- package/dist/core/permission-rule-model.js +80 -6
- package/dist/core/permission-rules.d.ts +8 -2
- package/dist/core/permission-rules.js +26 -9
- package/dist/core/runner/classifier-ladder.d.ts +10 -8
- package/dist/core/runner/classifier-ladder.js +3 -1
- package/dist/core/runner/contracts.d.ts +1 -16
- package/dist/core/runner/denial-limit-arms.d.ts +25 -8
- package/dist/core/runner/denial-limit-arms.js +24 -10
- package/dist/core/runner/gate-exit.js +2 -1
- package/dist/core/runner/permission-rule-lanes.d.ts +2 -2
- package/dist/core/runner/permission-rule-lanes.js +8 -2
- package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
- package/dist/core/runner/prepare-ask-lane.js +2 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -3
- package/dist/core/runner/prepare-caps-and-workflow.js +3 -2
- package/dist/core/runner/prepare-config-doors.d.ts +3 -0
- package/dist/core/runner/prepare-config-doors.js +6 -5
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-inherited-gate.d.ts +7 -5
- package/dist/core/runner/prepare-inherited-gate.js +5 -3
- package/dist/core/runner/prepare-park-ask.d.ts +4 -4
- package/dist/core/runner/prepare-park-ask.js +2 -4
- package/dist/core/runner/prepare-policy-chain.d.ts +7 -1
- package/dist/core/runner/prepare-policy-chain.js +59 -38
- package/dist/core/runner/prepare-project-context.js +6 -2
- package/dist/core/runner/prepare-task.js +6 -6
- package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -5
- package/dist/core/runner/prepare-wiring-manifest.js +8 -9
- package/dist/core/runner/resume-checkpoint-screen.js +1 -1
- package/dist/core/runner/runtask.js +1 -5
- package/dist/core/runner/steer-admission.d.ts +10 -11
- package/dist/core/runner/steer-admission.js +6 -8
- package/dist/core/runner/stream-steer-verb.d.ts +5 -4
- package/dist/core/runner/stream-steer-verb.js +6 -8
- package/dist/core/runner/synthetic-tools.d.ts +3 -2
- package/dist/core/runner/synthetic-tools.js +1 -1
- package/dist/core/session-policy-store.d.ts +3 -3
- package/dist/core/session-policy-store.js +18 -4
- package/dist/core/skill-grant-binding.d.ts +44 -0
- package/dist/core/skill-grant-binding.js +22 -0
- package/dist/core/skill-grant.d.ts +59 -28
- package/dist/core/skill-grant.js +96 -23
- package/dist/core/steer-replay.d.ts +46 -0
- package/dist/core/steer-replay.js +22 -0
- package/dist/core/task-event.d.ts +3 -2
- package/dist/core/task-spec.d.ts +14 -1
- package/dist/core/task-stream.d.ts +12 -6
- package/dist/core/tighten-task-spec.d.ts +2 -0
- package/dist/core/tighten-task-spec.js +3 -0
- package/dist/core/tool-errors.d.ts +2 -2
- package/dist/core/tool-name-aliases.d.ts +35 -5
- package/dist/core/tool-name-aliases.js +29 -4
- package/dist/core/tool-policy.d.ts +10 -0
- package/dist/core/tool-policy.js +9 -2
- package/dist/core/tool-spec.d.ts +7 -0
- package/dist/core/types.d.ts +2 -2
- package/dist/core/types.js +1 -1
- package/dist/core/wiring-manifest.d.ts +56 -14
- package/dist/core/wiring-manifest.js +29 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +5 -4
- package/dist/orchestration/run-workflow-tool.d.ts +4 -0
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-primitives.d.ts +3 -1
- package/dist/orchestration/workflow-primitives.js +6 -6
- package/dist/orchestration/workflow-types.d.ts +5 -0
- package/dist/orchestration/workflow.d.ts +3 -2
- package/dist/orchestration/workflow.js +14 -6
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +21 -5
- package/wire-changes/core@7.43.0.json +985 -0
- package/wire-changes/core@7.44.0.json +469 -0
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
import { type RuleToolNameVerdict } from "./tool-name-aliases.js";
|
|
1
2
|
import { type ToolCallRequest } from "./tool-policy.js";
|
|
2
3
|
import { type AllowRuleMember, type AllowRuleShadow } from "./allow-rule-shadow.js";
|
|
3
4
|
import { type PersistedRule } from "./permission-rule-model.js";
|
|
4
5
|
import { type AskClass, type AssertAllKeysHandled, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js";
|
|
5
6
|
import { type EngineNotice } from "./engine-notice.js";
|
|
6
|
-
/** Why an entry grants (or refuses) nothing — the closed set the start-of-run notice names, for every list.
|
|
7
|
-
|
|
7
|
+
/** Why an entry grants (or refuses) nothing — the closed set the start-of-run notice names, for every list. `tool_name`: the
|
|
8
|
+
* entry's tool name is one the name judgment reads as stale against the run's roster (`verdict` says which); `near_miss`: the
|
|
9
|
+
* run mounts no tool of that exact name but mounts one that differs only by case (`currentName` is its spelling). */
|
|
10
|
+
export declare const ALLOW_ENTRY_INERT_REASONS: readonly ["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar", "tool_name", "near_miss"];
|
|
8
11
|
export type AllowEntryInertReason = (typeof ALLOW_ENTRY_INERT_REASONS)[number];
|
|
12
|
+
/** The facts beside a `tool_name` / `near_miss` reason: the judgment's word, and the spelling to write instead when there is one. */
|
|
13
|
+
export interface AllowEntryInertDetail {
|
|
14
|
+
readonly verdict?: Exclude<RuleToolNameVerdict["kind"], "current">;
|
|
15
|
+
readonly currentName?: string;
|
|
16
|
+
}
|
|
17
|
+
/** One entry a list reads as nothing. */
|
|
18
|
+
export type AllowEntryInert = {
|
|
19
|
+
readonly entry: string;
|
|
20
|
+
readonly reason: AllowEntryInertReason;
|
|
21
|
+
} & AllowEntryInertDetail;
|
|
9
22
|
/** One entry the grammar reads (kind closed): a whole non-shell tool; a protocol namespace; the whole shell tool; a shell
|
|
10
23
|
* command rule; the Read directory form; a deny-side path pattern (a skill's `disallowed-tools`). */
|
|
11
24
|
export type CompiledAllowEntry = {
|
|
@@ -27,22 +40,25 @@ export type CompiledAllowEntry = {
|
|
|
27
40
|
readonly kind: "path";
|
|
28
41
|
readonly rule: PersistedRule;
|
|
29
42
|
};
|
|
30
|
-
/** Read one entry under one direction. */
|
|
31
|
-
export declare function compileAllowEntry(raw: string, behavior: "allow" | "deny"): CompiledAllowEntry | AllowEntryInertReason;
|
|
32
43
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
44
|
+
* Read one entry under one direction. `roster` = the tool names the run mounts (aliases included): given, the entry's tool name
|
|
45
|
+
* is also read against it (a stale name, or one that differs from a mounted tool's only by case, reads as `tool_name` /
|
|
46
|
+
* `near_miss`); absent, the grammar alone is read — what a resume's pre-claim screen can read before the leg has a roster.
|
|
47
|
+
*/
|
|
48
|
+
export declare function compileAllowEntry(raw: string, behavior: "allow" | "deny", roster?: ReadonlySet<string>): CompiledAllowEntry | AllowEntryInertReason;
|
|
49
|
+
/**
|
|
50
|
+
* Compile one list WHOLE: the entries it reads and the ones it reads as nothing (each with its reason and, for a name the
|
|
51
|
+
* roster refuses, the judgment's word and the spelling to write). An allow list holding `*` voids the list (every other entry
|
|
52
|
+
* on it reads as nothing too: the list grants nothing — the safe direction). A `*` on a deny list reads as nothing by itself
|
|
53
|
+
* and the rest of that list still refuses: a deny list never loses an entry to a wildcard beside it. `roster` as in
|
|
54
|
+
* {@link compileAllowEntry}.
|
|
36
55
|
*/
|
|
37
|
-
export declare function compileAllowList(entries: readonly string[], behavior: "allow" | "deny"): {
|
|
56
|
+
export declare function compileAllowList(entries: readonly string[], behavior: "allow" | "deny", roster?: ReadonlySet<string>): {
|
|
38
57
|
rows: Array<{
|
|
39
58
|
entry: string;
|
|
40
59
|
compiled: CompiledAllowEntry;
|
|
41
60
|
}>;
|
|
42
|
-
inert:
|
|
43
|
-
entry: string;
|
|
44
|
-
reason: AllowEntryInertReason;
|
|
45
|
-
}>;
|
|
61
|
+
inert: AllowEntryInert[];
|
|
46
62
|
};
|
|
47
63
|
/** The programs whose open allow rule the reference implementation drops in auto mode — its interpreter / launcher table
|
|
48
64
|
* plus the shells and the run-anything builtins (2.1.280). A program that only reaches the network is not here. */
|
|
@@ -117,11 +133,10 @@ export interface AllowEntryGateSeats {
|
|
|
117
133
|
*/
|
|
118
134
|
export declare class AllowEntryTable {
|
|
119
135
|
private held;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}>;
|
|
136
|
+
private readonly heldEntries;
|
|
137
|
+
/** Write a list's rows (a row already held is not held twice). Returns the entries that read as nothing. `roster` as in
|
|
138
|
+
* {@link compileAllowEntry}: a list the leg itself wrote is read against it, a carried one (a delegating parent's capture) is not. */
|
|
139
|
+
seed(entries: readonly string[], roster?: ReadonlySet<string>): readonly AllowEntryInert[];
|
|
125
140
|
/** The entries held, as written (a copy) — what a delegated child is handed. */
|
|
126
141
|
entries(): readonly string[];
|
|
127
142
|
/** The lane over this table, bound to the leg's call facts. */
|
|
@@ -129,8 +144,10 @@ export declare class AllowEntryTable {
|
|
|
129
144
|
}
|
|
130
145
|
/**
|
|
131
146
|
* Seed one leg's request and deployment tables — a delegated child's captured request entries first (an entry that reads as
|
|
132
|
-
* nothing was announced where it was written), then the leg's own list
|
|
133
|
-
*
|
|
147
|
+
* nothing was announced where it was written, and is read without the roster for the same reason), then the leg's own list
|
|
148
|
+
* against `roster`; the deployment's grammar reading is announced once, at construction, so only its name readings
|
|
149
|
+
* (`tool_name`, `near_miss` — facts about THIS leg's roster) are announced here — and return the dead entries of the leg's
|
|
150
|
+
* OWN lists, the skills' beside the request's, for its one start-of-run notice.
|
|
134
151
|
*/
|
|
135
152
|
export declare function seedAllowEntryTables(tables: {
|
|
136
153
|
readonly request: AllowEntryTable;
|
|
@@ -139,13 +156,11 @@ export declare function seedAllowEntryTables(tables: {
|
|
|
139
156
|
skillInert: ReadonlyArray<{
|
|
140
157
|
skill: string;
|
|
141
158
|
list: "allowed-tools" | "disallowed-tools";
|
|
142
|
-
|
|
143
|
-
reason: AllowEntryInertReason;
|
|
144
|
-
}>;
|
|
159
|
+
} & AllowEntryInert>;
|
|
145
160
|
inherited: readonly string[] | undefined;
|
|
146
161
|
request: readonly string[] | undefined;
|
|
147
162
|
deployment: readonly string[] | undefined;
|
|
148
|
-
}): AllowEntryInertNote[];
|
|
163
|
+
}, roster: ReadonlySet<string>): AllowEntryInertNote[];
|
|
149
164
|
/** The deployment's allow rules, read ONCE at construction: screened (a value that is not a list of strings refuses), frozen,
|
|
150
165
|
* and its dead entries announced once (no session). Returns the deps with the snapshot in place of the caller's list. */
|
|
151
166
|
export declare function withDeploymentAllowedTools<D extends {
|
|
@@ -181,6 +196,7 @@ declare const SHADOW_REASON: {
|
|
|
181
196
|
readonly rule_unreadable: "this call could not be read against a deny or ask rule, so whether one forbids it is unknown";
|
|
182
197
|
readonly ask_rule: string;
|
|
183
198
|
readonly denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides";
|
|
199
|
+
readonly classifier_no_ruling: "the auto-mode classifier could not judge this call, so a person decides";
|
|
184
200
|
readonly requires_user_interaction: "this tool's server declares that a call must be put to a person";
|
|
185
201
|
readonly operator_forced: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule";
|
|
186
202
|
readonly shell_floor: string;
|
|
@@ -221,21 +237,24 @@ export declare function cardDisclosureOf(decision: {
|
|
|
221
237
|
/** The notice for list entries that grant (or refuse) nothing — one code for every list. */
|
|
222
238
|
export declare const ALLOWED_TOOL_NOT_GRANTED_CODE = "permission.allowed_tool_not_granted";
|
|
223
239
|
/** One entry that reads as nothing, and the list it came from (a skill's names the skill and which of its two lists). */
|
|
224
|
-
export type AllowEntryInertNote = {
|
|
240
|
+
export type AllowEntryInertNote = ({
|
|
225
241
|
readonly source: "skill";
|
|
226
242
|
readonly skill: string;
|
|
227
243
|
readonly list: "allowed-tools" | "disallowed-tools";
|
|
228
|
-
|
|
229
|
-
readonly reason: AllowEntryInertReason;
|
|
230
|
-
} | {
|
|
244
|
+
} & AllowEntryInert) | ({
|
|
231
245
|
readonly source: AllowEntrySource;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
246
|
+
} & AllowEntryInert);
|
|
247
|
+
/**
|
|
248
|
+
* One entry that reads as nothing, as a person reads it: the entry as written (quoted, so a surrounding space shows) and why
|
|
249
|
+
* — the reason word, with the spelling to write when the reading has one. The one phrasing the start-of-run notice and a
|
|
250
|
+
* skill's refused activation share.
|
|
251
|
+
*/
|
|
252
|
+
export declare function inertEntryPhrase(e: AllowEntryInert): string;
|
|
235
253
|
/**
|
|
236
254
|
* The notice for entries this grammar reads as nothing — delivered only when one exists: once per prepared task for the
|
|
237
255
|
* skill and request lists (`sessionId` given), once per Runner for the deployment's list. The person whose list it is
|
|
238
|
-
* reads it (audience `user`); the run goes on (an entry that grants nothing makes it ask more).
|
|
256
|
+
* reads it (audience `user`); the run goes on (an entry that grants nothing makes it ask more). A skill whose
|
|
257
|
+
* `disallowed-tools` holds such an entry is named as one that does not activate.
|
|
239
258
|
*/
|
|
240
259
|
export declare function allowedToolsNotGrantedNotice(inert: readonly AllowEntryInertNote[], sessionId: string | undefined): EngineNotice;
|
|
241
260
|
export {};
|
package/dist/core/allow-entry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { heldBy } from "./tool-name-readings.js";
|
|
2
2
|
import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
|
|
3
|
-
import {
|
|
3
|
+
import { judgeRuleToolName, splitNamespacedName } from "./tool-name-aliases.js";
|
|
4
|
+
import { catalogToolNames } from "./tool-registry.js";
|
|
4
5
|
import { COARSE_SHELL_TOOLS } from "./tool-policy.js";
|
|
5
6
|
import { ALLOW_RULE_MEMBERS } from "./allow-rule-shadow.js";
|
|
6
7
|
import { COMMAND_RULE_TOOL, READ_RULE_TOOL, findAdmittingRule, laneToolNamesOf, parseRuleText, programRunReachOf, ruleToolGrammarOf } from "./permission-rule-model.js";
|
|
@@ -9,66 +10,95 @@ import { splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier
|
|
|
9
10
|
import { mandateDisclosureOf } from "./ask-origin.js";
|
|
10
11
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
11
12
|
import { deliverEngineNotice } from "./engine-notice.js";
|
|
12
|
-
export const ALLOW_ENTRY_INERT_REASONS = Object.freeze(["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar"]);
|
|
13
|
+
export const ALLOW_ENTRY_INERT_REASONS = Object.freeze(["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar", "tool_name", "near_miss"]);
|
|
13
14
|
function ruleRowOf(parsed) {
|
|
14
15
|
return { ...parsed, scope: { kind: "global" }, adds: [{ dot: { actor: "list", counter: 1 }, origin: "user", createdAt: "" }] };
|
|
15
16
|
}
|
|
16
17
|
const TOOL_NAME_SPELLING = /^[^\s()]+$/;
|
|
17
|
-
|
|
18
|
+
function mountedSpellingOf(name, roster) {
|
|
19
|
+
const folded = name.toLowerCase();
|
|
20
|
+
for (const mounted of roster)
|
|
21
|
+
if (mounted.toLowerCase() === folded)
|
|
22
|
+
return mounted;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function toolNameReading(name, roster) {
|
|
26
|
+
const verdict = judgeRuleToolName(name, roster);
|
|
27
|
+
if (verdict.kind !== "current") {
|
|
28
|
+
const currentName = verdict.kind === "unprefixed_namespace" ? undefined : verdict.currentName;
|
|
29
|
+
return { inert: "tool_name", detail: { verdict: verdict.kind, ...(currentName !== undefined ? { currentName } : {}) } };
|
|
30
|
+
}
|
|
31
|
+
if (roster.has(name) || catalogToolNames().has(name) || splitNamespacedName(name) !== undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
const mounted = mountedSpellingOf(name, roster);
|
|
34
|
+
return mounted === undefined ? undefined : { inert: "near_miss", detail: { currentName: mounted } };
|
|
35
|
+
}
|
|
36
|
+
function readAllowEntry(raw, behavior, roster) {
|
|
37
|
+
const inert = (reason) => ({ inert: reason });
|
|
38
|
+
const row = (compiled) => ({ compiled });
|
|
18
39
|
if (raw !== raw.trim())
|
|
19
|
-
return "grammar";
|
|
40
|
+
return inert("grammar");
|
|
20
41
|
const parsed = parsePermissionRule(raw);
|
|
21
42
|
const name = parsed.toolName;
|
|
22
43
|
if (!TOOL_NAME_SPELLING.test(name))
|
|
23
|
-
return "grammar";
|
|
44
|
+
return inert("grammar");
|
|
24
45
|
if (name === "*")
|
|
25
|
-
return "wildcard_all";
|
|
46
|
+
return inert("wildcard_all");
|
|
47
|
+
if (roster !== undefined) {
|
|
48
|
+
const stale = toolNameReading(name, roster);
|
|
49
|
+
if (stale !== undefined)
|
|
50
|
+
return stale;
|
|
51
|
+
}
|
|
26
52
|
if (parsed.ruleContent === undefined) {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
if (
|
|
30
|
-
return "namespace_wildcard";
|
|
53
|
+
const split = splitNamespacedName(name);
|
|
54
|
+
if (split !== undefined) {
|
|
55
|
+
if (split.peer.includes("*"))
|
|
56
|
+
return inert("namespace_wildcard");
|
|
31
57
|
if (isNamespacedCoveringRuleName(name))
|
|
32
|
-
return { kind: "namespace", pattern: name };
|
|
33
|
-
return { kind: "tool", name };
|
|
58
|
+
return row({ kind: "namespace", pattern: name });
|
|
59
|
+
return row({ kind: "tool", name });
|
|
34
60
|
}
|
|
35
|
-
return COARSE_SHELL_TOOLS.includes(name) ? { kind: "shell", name } : { kind: "tool", name };
|
|
61
|
+
return row(COARSE_SHELL_TOOLS.includes(name) ? { kind: "shell", name } : { kind: "tool", name });
|
|
36
62
|
}
|
|
37
63
|
if (name === COMMAND_RULE_TOOL) {
|
|
38
64
|
const allowRead = parseRuleText(raw, behavior);
|
|
39
65
|
if ("rule" in allowRead)
|
|
40
|
-
return { kind: "command", rule: ruleRowOf(allowRead.rule) };
|
|
66
|
+
return row({ kind: "command", rule: ruleRowOf(allowRead.rule) });
|
|
41
67
|
if (behavior === "allow" && allowRead.reject.code === "invalid.bare_interpreter_prefix") {
|
|
42
68
|
const wide = parseRuleText(raw, "deny");
|
|
43
69
|
if ("rule" in wide)
|
|
44
|
-
return { kind: "command", rule: ruleRowOf({ ...wide.rule, behavior: "allow" }) };
|
|
70
|
+
return row({ kind: "command", rule: ruleRowOf({ ...wide.rule, behavior: "allow" }) });
|
|
45
71
|
}
|
|
46
|
-
return "grammar";
|
|
72
|
+
return inert("grammar");
|
|
47
73
|
}
|
|
48
74
|
if (ruleToolGrammarOf(name) === "path") {
|
|
49
75
|
const read = parseRuleText(raw, behavior);
|
|
50
76
|
if ("rule" in read) {
|
|
51
77
|
if (read.rule.match === "subpath" && read.rule.tool === READ_RULE_TOOL && behavior === "allow")
|
|
52
|
-
return { kind: "read_dir", rule: ruleRowOf(read.rule) };
|
|
78
|
+
return row({ kind: "read_dir", rule: ruleRowOf(read.rule) });
|
|
53
79
|
if (behavior === "deny")
|
|
54
|
-
return { kind: "path", rule: ruleRowOf(read.rule) };
|
|
80
|
+
return row({ kind: "path", rule: ruleRowOf(read.rule) });
|
|
55
81
|
}
|
|
56
|
-
return "path_pattern";
|
|
82
|
+
return inert("path_pattern");
|
|
57
83
|
}
|
|
58
84
|
if (/^domain:/.test(parsed.ruleContent))
|
|
59
|
-
return "domain";
|
|
60
|
-
return "content_unsupported";
|
|
85
|
+
return inert("domain");
|
|
86
|
+
return inert("content_unsupported");
|
|
61
87
|
}
|
|
62
|
-
export function
|
|
88
|
+
export function compileAllowEntry(raw, behavior, roster) {
|
|
89
|
+
const read = readAllowEntry(raw, behavior, roster);
|
|
90
|
+
return "compiled" in read ? read.compiled : read.inert;
|
|
91
|
+
}
|
|
92
|
+
export function compileAllowList(entries, behavior, roster) {
|
|
63
93
|
const voided = behavior === "allow" && entries.some((e) => compileAllowEntry(e, behavior) === "wildcard_all");
|
|
64
94
|
const rows = [];
|
|
65
95
|
const inert = [];
|
|
66
96
|
for (const entry of entries) {
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
|
|
97
|
+
const read = voided ? { inert: "wildcard_all" } : readAllowEntry(entry, behavior, roster);
|
|
98
|
+
if ("compiled" in read)
|
|
99
|
+
rows.push({ entry, compiled: read.compiled });
|
|
70
100
|
else
|
|
71
|
-
|
|
101
|
+
inert.push({ entry, reason: read.inert, ...read.detail });
|
|
72
102
|
}
|
|
73
103
|
return { rows, inert };
|
|
74
104
|
}
|
|
@@ -179,11 +209,15 @@ export function allowingRows(rows, req, mode) {
|
|
|
179
209
|
}
|
|
180
210
|
export class AllowEntryTable {
|
|
181
211
|
held = [];
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
212
|
+
heldEntries = new Set();
|
|
213
|
+
seed(entries, roster) {
|
|
214
|
+
const { rows, inert } = compileAllowList(entries, "allow", roster);
|
|
215
|
+
for (const r of rows) {
|
|
216
|
+
if (this.heldEntries.has(r.entry))
|
|
217
|
+
continue;
|
|
218
|
+
this.heldEntries.add(r.entry);
|
|
219
|
+
this.held.push({ behavior: "allow", entry: r.entry, compiled: r.compiled });
|
|
220
|
+
}
|
|
187
221
|
return inert;
|
|
188
222
|
}
|
|
189
223
|
entries() {
|
|
@@ -198,11 +232,15 @@ export class AllowEntryTable {
|
|
|
198
232
|
};
|
|
199
233
|
}
|
|
200
234
|
}
|
|
201
|
-
export function seedAllowEntryTables(tables, lists) {
|
|
235
|
+
export function seedAllowEntryTables(tables, lists, roster) {
|
|
202
236
|
tables.request.seed(lists.inherited ?? []);
|
|
203
|
-
const requestInert = tables.request.seed(lists.request ?? []);
|
|
204
|
-
tables.deployment.seed(lists.deployment ?? []);
|
|
205
|
-
return [
|
|
237
|
+
const requestInert = tables.request.seed(lists.request ?? [], roster);
|
|
238
|
+
const deploymentInert = tables.deployment.seed(lists.deployment ?? [], roster).filter((e) => e.reason === "tool_name" || e.reason === "near_miss");
|
|
239
|
+
return [
|
|
240
|
+
...lists.skillInert.map((e) => ({ source: "skill", ...e })),
|
|
241
|
+
...requestInert.map((e) => ({ source: "request", ...e })),
|
|
242
|
+
...deploymentInert.map((e) => ({ source: "deployment", ...e })),
|
|
243
|
+
];
|
|
206
244
|
}
|
|
207
245
|
export function withDeploymentAllowedTools(deps) {
|
|
208
246
|
if (deps.allowedTools === undefined)
|
|
@@ -276,6 +314,7 @@ const SHADOW_REASON = {
|
|
|
276
314
|
rule_unreadable: "this call could not be read against a deny or ask rule, so whether one forbids it is unknown",
|
|
277
315
|
ask_rule: mandateDisclosureOf(undefined),
|
|
278
316
|
denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides",
|
|
317
|
+
classifier_no_ruling: "the auto-mode classifier could not judge this call, so a person decides",
|
|
279
318
|
requires_user_interaction: "this tool's server declares that a call must be put to a person",
|
|
280
319
|
operator_forced: "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule",
|
|
281
320
|
shell_floor: mandateDisclosureOf("operator_always"),
|
|
@@ -327,14 +366,38 @@ export function cardDisclosureOf(decision, fallbackMessage) {
|
|
|
327
366
|
}
|
|
328
367
|
export const ALLOWED_TOOL_NOT_GRANTED_CODE = "permission.allowed_tool_not_granted";
|
|
329
368
|
const NOTICE_ENTRIES_SHOWN = 5;
|
|
369
|
+
export function inertEntryPhrase(e) {
|
|
370
|
+
const spelling = e.currentName !== undefined ? inlineUntrusted(e.currentName, 120) : undefined;
|
|
371
|
+
const why = (() => {
|
|
372
|
+
if (e.reason === "near_miss")
|
|
373
|
+
return `near_miss: the mounted spelling is "${spelling}"`;
|
|
374
|
+
if (e.reason !== "tool_name" || e.verdict === undefined)
|
|
375
|
+
return e.reason;
|
|
376
|
+
switch (e.verdict) {
|
|
377
|
+
case "retired":
|
|
378
|
+
return `tool_name: a retired tool name${spelling !== undefined ? `, now "${spelling}"` : ""}`;
|
|
379
|
+
case "unprefixed_namespace":
|
|
380
|
+
return 'tool_name: a "__" name with no protocol prefix';
|
|
381
|
+
case "unmintable_segment":
|
|
382
|
+
return `tool_name: a peer segment the engine never mints${spelling !== undefined ? `, write "${spelling}"` : ""}`;
|
|
383
|
+
default: {
|
|
384
|
+
const exhaustive = e.verdict;
|
|
385
|
+
return exhaustive;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
})();
|
|
389
|
+
return `${inlineUntrusted(JSON.stringify(e.entry), 122)} (${why})`;
|
|
390
|
+
}
|
|
330
391
|
export function allowedToolsNotGrantedNotice(inert, sessionId) {
|
|
331
392
|
const where = (e) => (e.source === "skill" ? `skill ${inlineUntrusted(e.skill, 80)} ${e.list}` : e.source === "request" ? "this run's allowedTools" : "the deployment's allowedTools");
|
|
332
|
-
const shown = inert.slice(0, NOTICE_ENTRIES_SHOWN).map((e) => `${where(e)}
|
|
393
|
+
const shown = inert.slice(0, NOTICE_ENTRIES_SHOWN).map((e) => `${where(e)} ${inertEntryPhrase(e)}`);
|
|
333
394
|
const rest = inert.length - shown.length;
|
|
395
|
+
const refusing = inert.some((e) => "list" in e && e.list === "disallowed-tools");
|
|
334
396
|
return {
|
|
335
397
|
code: ALLOWED_TOOL_NOT_GRANTED_CODE,
|
|
336
398
|
message: `${inert.length} allowed-tools entr${inert.length === 1 ? "y grants" : "ies grant"} or refuse${inert.length === 1 ? "s" : ""} nothing — ${shown.join("; ")}${rest > 0 ? `; and ${rest} more` : ""}. ` +
|
|
337
|
-
"A list holding `*` voids the list, `mcp__*` names no server,
|
|
399
|
+
"A list holding `*` voids the list, `mcp__*` names no server, a write-path or domain entry has no allow reading, and a tool name must be spelled as the tool is mounted; name the tools (or `mcp__<server>`) instead." +
|
|
400
|
+
(refusing ? " Such an entry in a disallowed-tools list keeps its Skill from activating: a refusal that reads as nothing is not dropped." : ""),
|
|
338
401
|
detail: { ...(sessionId !== undefined ? { sessionId } : {}), entries: inert.map((e) => ({ ...e })) },
|
|
339
402
|
};
|
|
340
403
|
}
|
|
@@ -445,8 +445,11 @@ export type ToolMarkSource = "deployment" | "mcp_annotation" | "a2a_default";
|
|
|
445
445
|
* row cell by cell, because the reference implementation judges the declaration ahead of its allow-rule lookup and its bypass release.
|
|
446
446
|
* `unstated` is the mode's answer for a call nobody stated: a tool whose permission seat is `passthrough` (it states no
|
|
447
447
|
* permission of its own — a remote peer's tool) reached the end of the fold with an allow no layer stated.
|
|
448
|
+
* `classifier_no_ruling` is a classifier station's question to a person because the auto-mode classification did not rule
|
|
449
|
+
* (`classifierNoRuling` — it could not run, or replied with no verdict): the denial-limit fallback's row (only a person
|
|
450
|
+
* answers it; the classifier is never asked again).
|
|
448
451
|
*/
|
|
449
|
-
export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "requires_user_interaction", "operator_forced", "shell_floor", "write_protection_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
|
|
452
|
+
export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "classifier_no_ruling", "requires_user_interaction", "operator_forced", "shell_floor", "write_protection_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
|
|
450
453
|
export type AskClass = (typeof ASK_CLASSES)[number];
|
|
451
454
|
/** Membership test for {@link AskClass} — the word crosses the wire on `tool_end.gate.disposition`. */
|
|
452
455
|
export declare function isAskClass(v: unknown): v is AskClass;
|
|
@@ -549,6 +552,13 @@ export declare const MODE_ARBITRATION: {
|
|
|
549
552
|
readonly ruleMayClear: false;
|
|
550
553
|
readonly hookAskFloor: false;
|
|
551
554
|
};
|
|
555
|
+
readonly classifier_no_ruling: {
|
|
556
|
+
readonly bypassImmune: true;
|
|
557
|
+
readonly classifierEligible: false;
|
|
558
|
+
readonly sandboxAdmission: false;
|
|
559
|
+
readonly ruleMayClear: false;
|
|
560
|
+
readonly hookAskFloor: false;
|
|
561
|
+
};
|
|
552
562
|
readonly requires_user_interaction: {
|
|
553
563
|
readonly bypassImmune: true;
|
|
554
564
|
readonly classifierEligible: false;
|
|
@@ -725,9 +735,10 @@ export declare function classifyAsk(d: Extract<PermissionResult, {
|
|
|
725
735
|
* and the ask's prompt as what was requested. The gate appends the caller-side remedy. */
|
|
726
736
|
export declare function headlessDenialMessage(toolName: string, askMessage: string): string;
|
|
727
737
|
/** The state of the approver seat a remedy is written for: `absent` — no approver is wired for the leg; `unavailable` — one is
|
|
728
|
-
* wired and answered that nobody is reachable for this ask
|
|
729
|
-
*
|
|
730
|
-
|
|
738
|
+
* wired and answered that nobody is reachable for this ask; `declared_none` — the run declared that nobody on it can be
|
|
739
|
+
* asked (`approvalReach: "none"`), whatever is wired. Absent in the input reads as `absent` (the posture the function was
|
|
740
|
+
* written for). */
|
|
741
|
+
export type HeadlessApproverSeat = "absent" | "unavailable" | "declared_none";
|
|
731
742
|
/**
|
|
732
743
|
* The ONE sentence a headless auto-deny (an ask that reached its final deny because no approver is wired) adds
|
|
733
744
|
* after its refusal: the caller-side change that would have let this call run. Read off the effective mode, the
|
|
@@ -741,7 +752,7 @@ export type HeadlessApproverSeat = "absent" | "unavailable";
|
|
|
741
752
|
* · any other releasable ask — `bypassPermissions` releases it (disclosed on the call).
|
|
742
753
|
* Every arm also names the approver, in the clause of the seat's own state ({@link HeadlessApproverSeat}): none wired ⇒
|
|
743
754
|
* wire one; one wired that answered `unavailable` for this ask ⇒ make a person reachable through it (a wired approver is
|
|
744
|
-
* never told to be wired). The refusal's tool text and the `delegation.ask_unresolvable` notice both carry this sentence —
|
|
755
|
+
* never told to be wired); the run declared nobody can be asked ⇒ run it where a person can be, or park it. The refusal's tool text and the `delegation.ask_unresolvable` notice both carry this sentence —
|
|
745
756
|
* they call this function, neither spells it.
|
|
746
757
|
*/
|
|
747
758
|
export declare function headlessApprovalRemedy(input: {
|
package/dist/core/ask-origin.js
CHANGED
|
@@ -177,6 +177,7 @@ export const ASK_CLASSES = Object.freeze([
|
|
|
177
177
|
"rule_unreadable",
|
|
178
178
|
"ask_rule",
|
|
179
179
|
"denial_limit_fallback",
|
|
180
|
+
"classifier_no_ruling",
|
|
180
181
|
"requires_user_interaction",
|
|
181
182
|
"operator_forced",
|
|
182
183
|
"shell_floor",
|
|
@@ -206,6 +207,7 @@ export const MODE_ARBITRATION = {
|
|
|
206
207
|
rule_unreadable: { bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
|
|
207
208
|
ask_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
|
|
208
209
|
denial_limit_fallback: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
|
|
210
|
+
classifier_no_ruling: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
|
|
209
211
|
requires_user_interaction: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
|
|
210
212
|
operator_forced: { bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
|
|
211
213
|
shell_floor: { bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
|
|
@@ -220,7 +222,7 @@ export const MODE_ARBITRATION = {
|
|
|
220
222
|
unstated: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: true, hookAskFloor: false },
|
|
221
223
|
ordinary: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: true, hookAskFloor: false },
|
|
222
224
|
};
|
|
223
|
-
export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
|
|
225
|
+
export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "classifier_no_ruling", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
|
|
224
226
|
export const RELEASABLE_ASK_CLASSES = ASK_CLASSES.filter((c) => !MODE_ARBITRATION[c].bypassImmune);
|
|
225
227
|
export const MODE_RELEASE_REASONS = Object.freeze(["rule_unreadable"]);
|
|
226
228
|
const MODE_RELEASE_REASON_SET = new Set(MODE_RELEASE_REASONS);
|
|
@@ -253,6 +255,8 @@ export function decisionAskClassesOf(d) {
|
|
|
253
255
|
out.add("ask_rule");
|
|
254
256
|
if (d.denialLimitFallback !== undefined)
|
|
255
257
|
out.add("denial_limit_fallback");
|
|
258
|
+
if (d.classifierNoRuling !== undefined)
|
|
259
|
+
out.add("classifier_no_ruling");
|
|
256
260
|
if (d.authoredBy === "deployment")
|
|
257
261
|
out.add("operator_forced");
|
|
258
262
|
if (d.probeMandated === true)
|
|
@@ -333,10 +337,14 @@ export function classifyAsk(d, facts) {
|
|
|
333
337
|
return { classes, askClass, row: foldArbitrationRow(classes) };
|
|
334
338
|
}
|
|
335
339
|
export function headlessDenialMessage(toolName, askMessage) {
|
|
336
|
-
return `Permission for this tool use was denied: it requires interactive approval, and permission prompts are not available in this session (
|
|
340
|
+
return `Permission for this tool use was denied: it requires interactive approval, and permission prompts are not available in this session (nobody can be asked to approve "${toolName}" in this run — headless auto-deny). The action was NOT performed. Do not claim it succeeded, and do not retry it in this session — report the limitation to the user, or suggest an alternative. What was requested: ${askMessage}`;
|
|
337
341
|
}
|
|
338
342
|
export function headlessApprovalRemedy(input) {
|
|
339
|
-
const approver = input.approverSeat === "unavailable"
|
|
343
|
+
const approver = input.approverSeat === "unavailable"
|
|
344
|
+
? "make a person reachable through the approver that is wired (it answered `unavailable` for this ask)"
|
|
345
|
+
: input.approverSeat === "declared_none"
|
|
346
|
+
? 'run it where a person can be asked — this run declares `approvalReach: "none"` (nobody on it can be asked) — or arm a durable approval park so the ask waits for a person'
|
|
347
|
+
: "wire an approver (`onAsk`) to put it to a person";
|
|
340
348
|
const c = input.classification;
|
|
341
349
|
if (input.mode === "acceptEdits" && input.unplacedOptions !== undefined && (c === undefined || (c.classes.size === 1 && c.classes.has("ordinary"))))
|
|
342
350
|
return `${fileCommandOptionsSentence(input.unplacedOptions.program)} Spell the ${input.unplacedOptions.program} without option words for the mode to answer it, or ${approver}`;
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* · `approver_unavailable` — the approver consulted for the call (the run's own seat, or an ancestor's
|
|
6
6
|
* frozen seat on a delegated child) answered the ROUTING question `unavailable` and no durable park
|
|
7
7
|
* took the call afterwards (`tool_end.gate.settlement.kind:"approver_unavailable"`);
|
|
8
|
-
* · `no_approver_wired` — no approver (or, for a question, no question face) is
|
|
9
|
-
* the seat is the deny posture string
|
|
8
|
+
* · `no_approver_wired` — nobody in this run can be asked: no approver (or, for a question, no question face) is
|
|
9
|
+
* wired, the seat is the deny posture string, or the run declared that nobody on it can be asked
|
|
10
|
+
* (`approvalReach: "none"`): the headless auto-deny (`settlement.kind:"no_approver"`);
|
|
10
11
|
* · `mandate_unreconstructible` — an inherited durable-approval mandate binds the ask and THIS leg has no
|
|
11
12
|
* park facility to honor it, so the delegated child refuses in-fold rather than let a synchronous layer
|
|
12
13
|
* resolve what its ancestor bound to a park (an in-fold deny; no settlement rides the frame).
|
|
@@ -6,7 +6,7 @@ export function isAskUnresolvableCause(v) {
|
|
|
6
6
|
}
|
|
7
7
|
const CAUSE_CLAUSE = {
|
|
8
8
|
approver_unavailable: "the approver reported that nobody is reachable for this ask",
|
|
9
|
-
no_approver_wired: "
|
|
9
|
+
no_approver_wired: "nobody in this run can be asked to answer it — the headless posture",
|
|
10
10
|
mandate_unreconstructible: "an inherited durable-approval mandate binds this ask and this leg has no park facility to honor it",
|
|
11
11
|
};
|
|
12
12
|
const CAUSE_REMEDY_SEAT = {
|
package/dist/core/auto-mode.d.ts
CHANGED
|
@@ -166,11 +166,12 @@ export declare function rungDidNotRule(v: AutoModeVerdict): AutoModeFallbackCaus
|
|
|
166
166
|
*/
|
|
167
167
|
export declare function classifierUnavailableDenyMessage(toolName: string, cause: AutoModeUnavailableCause, attribution?: AutoModeClassifierRound): string;
|
|
168
168
|
/**
|
|
169
|
-
* The text an ask carries to a person when the classifier
|
|
170
|
-
* (the gate's classifier station and the inherited station): the ask's own text first — the card still
|
|
171
|
-
* is being asked — then why auto mode is asking at all
|
|
169
|
+
* The text an ask carries to a person when the classifier did not rule on it ({@link rungDidNotRule}) and the leg has an
|
|
170
|
+
* approval seat (the gate's classifier station and the inherited station): the ask's own text first — the card still
|
|
171
|
+
* says what is being asked — then why auto mode is asking at all, read off the same cause the ask's `classifierNoRuling`
|
|
172
|
+
* member carries (a client that does not read the member reads this). One writer for both stations.
|
|
172
173
|
*/
|
|
173
|
-
export declare function
|
|
174
|
+
export declare function classifierNoRulingAskMessage(askText: string, cause: AutoModeFallbackCause): string;
|
|
174
175
|
/**
|
|
175
176
|
* The model-facing deny text for a `parse_error` verdict — CC 2.1.259 `i7t` stem ("Auto mode could not evaluate
|
|
176
177
|
* this action and is blocking it for safety"; 2.1.250 `Ure`): the classifier answered, but not with a verdict,
|
package/dist/core/auto-mode.js
CHANGED
|
@@ -84,7 +84,9 @@ export function classifierUnavailableDenyMessage(toolName, cause, attribution) {
|
|
|
84
84
|
"If it keeps failing, continue with other tasks that don't require this action and come back to it later. " +
|
|
85
85
|
"Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used.");
|
|
86
86
|
}
|
|
87
|
-
export function
|
|
87
|
+
export function classifierNoRulingAskMessage(askText, cause) {
|
|
88
|
+
if (cause === "parse_error")
|
|
89
|
+
return `${askText}\n\nThe auto-mode classifier replied without a verdict, so auto mode could not judge this call; it needs an approval instead of the classifier's verdict.`;
|
|
88
90
|
const parenthetical = cause === "timeout" ? " (timed out)" : "";
|
|
89
91
|
return `${askText}\n\nThe auto-mode classifier is temporarily unavailable${parenthetical}, so auto mode could not judge this call; it needs an approval instead of the classifier's verdict.`;
|
|
90
92
|
}
|