@sema-agent/core 7.44.0 → 7.45.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 +172 -0
- package/dist/agents/subagent.js +33 -14
- package/dist/agents/tool-filter.d.ts +65 -4
- package/dist/agents/tool-filter.js +127 -4
- package/dist/core/agent-definition.d.ts +2 -2
- package/dist/core/allow-entry.d.ts +25 -1
- package/dist/core/allow-entry.js +29 -4
- package/dist/core/ask-origin.d.ts +23 -4
- package/dist/core/ask-origin.js +10 -4
- package/dist/core/checkpoint-store.d.ts +2 -2
- package/dist/core/decision-receipt.js +1 -1
- package/dist/core/effective-path-target.d.ts +7 -6
- package/dist/core/effective-path-target.js +3 -7
- package/dist/core/engine-notice.d.ts +20 -3
- package/dist/core/engine-notice.js +12 -6
- package/dist/core/gate-lanes.js +2 -0
- package/dist/core/gate-outcome.d.ts +5 -0
- package/dist/core/gate-outcome.js +2 -0
- package/dist/core/hooks.d.ts +2 -0
- package/dist/core/memory-engine/content-origin.d.ts +0 -34
- package/dist/core/memory-engine/content-origin.js +9 -4
- package/dist/core/memory-engine/layout.d.ts +5 -1
- package/dist/core/memory-engine/layout.js +1 -0
- package/dist/core/permission-rules.d.ts +4 -0
- package/dist/core/permission-rules.js +5 -2
- package/dist/core/real-approval.d.ts +9 -6
- package/dist/core/real-approval.js +5 -5
- package/dist/core/runner/child-tree.d.ts +13 -0
- package/dist/core/runner/child-tree.js +3 -0
- package/dist/core/runner/contracts.d.ts +8 -2
- package/dist/core/runner/prepare-ask-lane.d.ts +2 -0
- package/dist/core/runner/prepare-ask-lane.js +2 -2
- package/dist/core/runner/prepare-config-doors.js +3 -1
- package/dist/core/runner/prepare-execution-env.js +2 -1
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
- package/dist/core/runner/prepare-gate-stations.js +2 -1
- package/dist/core/runner/prepare-listings.d.ts +4 -4
- package/dist/core/runner/prepare-listings.js +2 -22
- package/dist/core/runner/prepare-park-ask.d.ts +2 -0
- package/dist/core/runner/prepare-park-ask.js +2 -2
- package/dist/core/runner/prepare-policy-chain.d.ts +2 -0
- package/dist/core/runner/prepare-policy-chain.js +4 -2
- package/dist/core/runner/prepare-task.js +10 -6
- package/dist/core/runner/resume-policy-outcome.js +2 -1
- package/dist/core/runner/run-leg.js +1 -1
- package/dist/core/runner/run-notification-lane.js +2 -2
- package/dist/core/runner/runtask.d.ts +2 -0
- package/dist/core/runner/runtask.js +2 -0
- package/dist/core/runner/stream-steer-verb.js +2 -2
- package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
- package/dist/core/runner/sub-agent-deny-lists.js +121 -0
- package/dist/core/runner/synthetic-tools.d.ts +1 -1
- package/dist/core/runner/synthetic-tools.js +3 -2
- package/dist/core/session-policy-store.d.ts +6 -2
- package/dist/core/session-policy-store.js +2 -1
- package/dist/core/skill-grant.d.ts +10 -2
- package/dist/core/skill-grant.js +20 -4
- package/dist/core/task-notification.d.ts +12 -1
- package/dist/core/task-notification.js +13 -1
- package/dist/core/task-spec.d.ts +5 -3
- package/dist/core/tool-policy.d.ts +15 -4
- package/dist/core/tool-policy.js +9 -13
- package/dist/core/tool-spec.d.ts +20 -2
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.js +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/orchestration/workflow.js +2 -1
- package/dist/tools/fs/removal-targets.d.ts +82 -0
- package/dist/tools/fs/removal-targets.js +236 -0
- package/dist/tools/fs/shell-read-write-targets.d.ts +27 -0
- package/dist/tools/fs/shell-read-write-targets.js +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +33 -1
- package/wire-changes/core@7.45.0.json +1084 -0
|
@@ -1,12 +1,135 @@
|
|
|
1
|
+
import { parsePermissionRule } from "../core/permission-rule-syntax.js";
|
|
2
|
+
export function readDeniedEntry(entry) {
|
|
3
|
+
if (typeof entry !== "string")
|
|
4
|
+
return { kind: "not_a_string", type: valueTypeLabel(entry) };
|
|
5
|
+
if (entry !== entry.trim())
|
|
6
|
+
return { kind: "name", name: entry, content: false };
|
|
7
|
+
const parsed = parsePermissionRule(entry);
|
|
8
|
+
return { kind: "name", name: parsed.toolName, content: parsed.ruleContent !== undefined };
|
|
9
|
+
}
|
|
10
|
+
export function valueTypeLabel(value) {
|
|
11
|
+
if (value === null)
|
|
12
|
+
return "null";
|
|
13
|
+
switch (typeof value) {
|
|
14
|
+
case "string":
|
|
15
|
+
return "a string";
|
|
16
|
+
case "undefined":
|
|
17
|
+
return "undefined";
|
|
18
|
+
case "number":
|
|
19
|
+
return "a number";
|
|
20
|
+
case "boolean":
|
|
21
|
+
return "a boolean";
|
|
22
|
+
case "bigint":
|
|
23
|
+
return "a bigint";
|
|
24
|
+
case "symbol":
|
|
25
|
+
return "a symbol";
|
|
26
|
+
case "function":
|
|
27
|
+
return "a function";
|
|
28
|
+
default:
|
|
29
|
+
try {
|
|
30
|
+
return Array.isArray(value) ? "an array" : "an object";
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return "an object";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const isList = (value) => Array.isArray(value);
|
|
38
|
+
const UNREADABLE_LIST = Object.freeze({});
|
|
39
|
+
export function readToolNameList(value) {
|
|
40
|
+
if (value === UNREADABLE_LIST)
|
|
41
|
+
return { kind: "unreadable" };
|
|
42
|
+
let list;
|
|
43
|
+
try {
|
|
44
|
+
list = isList(value) ? value : undefined;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { kind: "not_a_list", type: "an object" };
|
|
48
|
+
}
|
|
49
|
+
if (list === undefined)
|
|
50
|
+
return { kind: "not_a_list", type: valueTypeLabel(value) };
|
|
51
|
+
try {
|
|
52
|
+
const entries = [];
|
|
53
|
+
for (let i = 0; i < list.length; i++)
|
|
54
|
+
entries.push(list[i]);
|
|
55
|
+
return { kind: "entries", entries };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { kind: "unreadable" };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function snapshotToolNameList(value) {
|
|
62
|
+
const read = readToolNameList(value);
|
|
63
|
+
return read.kind === "entries" ? read.entries : read.kind === "unreadable" ? UNREADABLE_LIST : value;
|
|
64
|
+
}
|
|
65
|
+
const LIST_NOUN = { allowed: "allowed tools", denied: "denied tools" };
|
|
66
|
+
const LIST_NAME = { allowed: "an allowed-tools list", denied: "a denied-tools list" };
|
|
67
|
+
export function notAStringSentence(where, side, index, type) {
|
|
68
|
+
return `${where} declares in its ${LIST_NOUN[side]}, as entry ${index + 1}, a value that is ${type}, not a tool name — ${LIST_NAME[side]} holds tool names, each a string. Fix the entry.`;
|
|
69
|
+
}
|
|
70
|
+
export function notAListSentence(where, side, list) {
|
|
71
|
+
return list.kind === "not_a_list"
|
|
72
|
+
? `${where} declares in its ${LIST_NOUN[side]} a value that is ${list.type}, not a list of tool names. Fix the list.`
|
|
73
|
+
: `${where} declares in its ${LIST_NOUN[side]} a list whose entries cannot be read (an accessor or a proxy trap threw). Fix the list.`;
|
|
74
|
+
}
|
|
75
|
+
export function toolNameListFlaw(where, side, value) {
|
|
76
|
+
if (value === undefined || value === null)
|
|
77
|
+
return undefined;
|
|
78
|
+
const list = readToolNameList(value);
|
|
79
|
+
if (list.kind !== "entries")
|
|
80
|
+
return notAListSentence(where, side, list);
|
|
81
|
+
for (let i = 0; i < list.entries.length; i++) {
|
|
82
|
+
const entry = list.entries[i];
|
|
83
|
+
if (typeof entry !== "string")
|
|
84
|
+
return notAStringSentence(where, side, i, valueTypeLabel(entry));
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
export function snapshotDefinitionLists(def) {
|
|
89
|
+
const copy = { ...def };
|
|
90
|
+
if (copy.allowTools !== undefined)
|
|
91
|
+
copy.allowTools = snapshotToolNameList(copy.allowTools);
|
|
92
|
+
if (copy.denyTools !== undefined)
|
|
93
|
+
copy.denyTools = snapshotToolNameList(copy.denyTools);
|
|
94
|
+
return copy;
|
|
95
|
+
}
|
|
96
|
+
export function allowedToolNames(allowTools) {
|
|
97
|
+
if (allowTools === undefined || allowTools === null)
|
|
98
|
+
return undefined;
|
|
99
|
+
const list = readToolNameList(allowTools);
|
|
100
|
+
if (list.kind !== "entries")
|
|
101
|
+
return new Set();
|
|
102
|
+
if (list.entries.includes("*"))
|
|
103
|
+
return undefined;
|
|
104
|
+
const names = new Set();
|
|
105
|
+
for (const entry of list.entries)
|
|
106
|
+
if (typeof entry === "string")
|
|
107
|
+
names.add(entry);
|
|
108
|
+
return names;
|
|
109
|
+
}
|
|
110
|
+
function deniedNames(denyTools) {
|
|
111
|
+
const names = new Set();
|
|
112
|
+
const list = readToolNameList(denyTools);
|
|
113
|
+
if (list.kind !== "entries")
|
|
114
|
+
return names;
|
|
115
|
+
for (const entry of list.entries) {
|
|
116
|
+
const read = readDeniedEntry(entry);
|
|
117
|
+
if (read.kind === "name")
|
|
118
|
+
names.add(read.name);
|
|
119
|
+
}
|
|
120
|
+
return names;
|
|
121
|
+
}
|
|
1
122
|
export function toolNameAllowed(name, allowTools, denyTools) {
|
|
2
|
-
const allow =
|
|
3
|
-
const
|
|
123
|
+
const allow = allowedToolNames(allowTools);
|
|
124
|
+
const denied = deniedNames(denyTools);
|
|
125
|
+
const deny = denied.size > 0 ? denied : undefined;
|
|
4
126
|
const n = name;
|
|
5
127
|
return (!allow || allow.has(n)) && (!deny || !deny.has(n));
|
|
6
128
|
}
|
|
7
129
|
export function resolveToolSubset(pool, allowTools, denyTools) {
|
|
8
|
-
const allow =
|
|
9
|
-
const
|
|
130
|
+
const allow = allowedToolNames(allowTools);
|
|
131
|
+
const denied = deniedNames(denyTools);
|
|
132
|
+
const deny = denied.size > 0 ? denied : undefined;
|
|
10
133
|
if (!allow && !deny)
|
|
11
134
|
return [...pool];
|
|
12
135
|
return pool.filter((t) => {
|
|
@@ -33,7 +33,7 @@ export interface AgentDefinition {
|
|
|
33
33
|
* the delegation tool's name cannot re-delegate (CC Explore/Plan `disallowedTools` include Agent,
|
|
34
34
|
* pretty.js:409189/486948). */
|
|
35
35
|
allowTools?: string[];
|
|
36
|
-
/** Tool denylist (a deny always wins)
|
|
36
|
+
/** Tool denylist (a deny always wins), tool NAMES: `Tool(content)` hides the whole `Tool`. See {@link allowTools} for the per-agent delegation-tool note. */
|
|
37
37
|
denyTools?: string[];
|
|
38
38
|
/**
|
|
39
39
|
* F7 (CC 2.1.198 parity, per-agent `skills` — 锚 pretty.js:422126 `e.skills ?? []`, GA/ungated in 198:
|
|
@@ -116,7 +116,7 @@ export interface SkillSpec {
|
|
|
116
116
|
* either list asks once (`Execute skill: <name>`, the card listing what it releases); a skill the person invoked by
|
|
117
117
|
* name ({@link import("./task-spec.js").TaskSpec.userInvokedSkills}) grants without asking. The grant holds for that
|
|
118
118
|
* run (a steer it takes in does not end it) and the next runTask starts without it. An entry this grammar reads as nothing — `*` (it voids an allow list; on a deny list it is inert and the rest of the
|
|
119
|
-
* list still refuses), `mcp__*`, a write path, a domain — grants nothing and is announced at the start of the run (`permission.allowed_tool_not_granted`).
|
|
119
|
+
* list still refuses), `mcp__*`, a write path, a domain — grants nothing and is announced at the start of the run (`permission.allowed_tool_not_granted`). A table whose value is not a list (a string, an object…) reads as one such entry; `null` is no table.
|
|
120
120
|
*/
|
|
121
121
|
allowedTools?: string[];
|
|
122
122
|
/** The tools refused for the rest of the run this skill is activated in — the frontmatter `disallowed-tools` list,
|
|
@@ -3,17 +3,33 @@ import { type ToolCallRequest } from "./tool-policy.js";
|
|
|
3
3
|
import { type AllowRuleMember, type AllowRuleShadow } from "./allow-rule-shadow.js";
|
|
4
4
|
import { type PersistedRule } from "./permission-rule-model.js";
|
|
5
5
|
import { type AskClass, type AssertAllKeysHandled, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js";
|
|
6
|
+
import { type ToolNameListReading } from "../agents/tool-filter.js";
|
|
6
7
|
import { type EngineNotice } from "./engine-notice.js";
|
|
7
8
|
/** Why an entry grants (or refuses) nothing — the closed set the start-of-run notice names, for every list. `tool_name`: the
|
|
8
9
|
* entry's tool name is one the name judgment reads as stale against the run's roster (`verdict` says which); `near_miss`: the
|
|
9
10
|
* run mounts no tool of that exact name but mounts one that differs only by case (`currentName` is its spelling). */
|
|
10
11
|
export declare const ALLOW_ENTRY_INERT_REASONS: readonly ["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar", "tool_name", "near_miss"];
|
|
11
12
|
export type AllowEntryInertReason = (typeof ALLOW_ENTRY_INERT_REASONS)[number];
|
|
12
|
-
/** The facts beside a `tool_name` / `near_miss`
|
|
13
|
+
/** The facts beside a reason: for `tool_name` / `near_miss` the judgment's word and the spelling to write instead when there is
|
|
14
|
+
* one; for a list member that is not a string (`grammar`) the TYPE of the value (`a number`, `null`, `an object`…) — `entry` then
|
|
15
|
+
* holds {@link nonStringMemberLabel}, never the value. */
|
|
13
16
|
export interface AllowEntryInertDetail {
|
|
14
17
|
readonly verdict?: Exclude<RuleToolNameVerdict["kind"], "current">;
|
|
15
18
|
readonly currentName?: string;
|
|
19
|
+
readonly valueType?: string;
|
|
16
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* How a list member that is not a string is named wherever a list's entries are named (the inert record's `entry`, the Skill
|
|
23
|
+
* card's binding): its TYPE, in a spelling no readable entry can have — a readable entry's name holds no whitespace and a
|
|
24
|
+
* name-only entry holds no parenthesis, and this label holds a space and none. It can equal only another entry that reads as
|
|
25
|
+
* nothing, so a binding that holds it never stands for an entry that grants or refuses.
|
|
26
|
+
*/
|
|
27
|
+
export declare function nonStringMemberLabel(value: unknown): string;
|
|
28
|
+
/** How a tool-name list's VALUE that is not a list stands as one entry (the same kind of spelling as {@link nonStringMemberLabel}: it
|
|
29
|
+
* reads as `grammar`): its TYPE, or that its entries could not be read — never the value. */
|
|
30
|
+
export declare function nonListValueLabel(read: Exclude<ToolNameListReading, {
|
|
31
|
+
kind: "entries";
|
|
32
|
+
}>): string;
|
|
17
33
|
/** One entry a list reads as nothing. */
|
|
18
34
|
export type AllowEntryInert = {
|
|
19
35
|
readonly entry: string;
|
|
@@ -189,6 +205,7 @@ declare const SHADOW_REASON: {
|
|
|
189
205
|
readonly content_question: "the call is the engine's own question to a person";
|
|
190
206
|
readonly hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person";
|
|
191
207
|
readonly ancestor: "the task that delegated this one owns this question";
|
|
208
|
+
readonly dangerous_delete: "this removal targets a protected location (the filesystem root, a top-level directory, a home directory, or a workspace directory or one of its ancestors) — it is cleared by confirming this call, never by an allow rule";
|
|
192
209
|
readonly unverifiable_delete: "this removal could not be verified safe — it is cleared by confirming this call, never by an allow rule";
|
|
193
210
|
readonly transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule";
|
|
194
211
|
readonly org_rule: "an organization policy rule requires approval for this call";
|
|
@@ -244,6 +261,13 @@ export type AllowEntryInertNote = ({
|
|
|
244
261
|
} & AllowEntryInert) | ({
|
|
245
262
|
readonly source: AllowEntrySource;
|
|
246
263
|
} & AllowEntryInert);
|
|
264
|
+
/**
|
|
265
|
+
* THE ONE WRITER of a list entry into a sentence (the start-of-run notice, a skill's refusal and its decisions, a sub-agent
|
|
266
|
+
* list's refusals and advisories): the entry as a JSON string — quotes, backslashes and the usual escapes — with every
|
|
267
|
+
* character that prints as nothing, or as a space, written as `\uXXXX` (`\u{…}` beyond the BMP) and a run of spaces kept
|
|
268
|
+
* space by space, so what a person reads is what the engine read. Capped on what is printed.
|
|
269
|
+
*/
|
|
270
|
+
export declare function renderListEntry(entry: string): string;
|
|
247
271
|
/**
|
|
248
272
|
* One entry that reads as nothing, as a person reads it: the entry as written (quoted, so a surrounding space shows) and why
|
|
249
273
|
* — the reason word, with the spelling to write when the reading has one. The one phrasing the start-of-run notice and a
|
package/dist/core/allow-entry.js
CHANGED
|
@@ -9,12 +9,19 @@ import { pathRuleLaneAnswer } from "./runner/permission-rule-lanes.js";
|
|
|
9
9
|
import { splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
10
10
|
import { mandateDisclosureOf } from "./ask-origin.js";
|
|
11
11
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
12
|
+
import { valueTypeLabel } from "../agents/tool-filter.js";
|
|
12
13
|
import { deliverEngineNotice } from "./engine-notice.js";
|
|
13
14
|
export const ALLOW_ENTRY_INERT_REASONS = Object.freeze(["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar", "tool_name", "near_miss"]);
|
|
15
|
+
export function nonStringMemberLabel(value) {
|
|
16
|
+
return `<not a string: ${valueTypeLabel(value)}>`;
|
|
17
|
+
}
|
|
18
|
+
export function nonListValueLabel(read) {
|
|
19
|
+
return read.kind === "not_a_list" ? `<not a list: ${read.type}>` : "<a list whose entries cannot be read>";
|
|
20
|
+
}
|
|
14
21
|
function ruleRowOf(parsed) {
|
|
15
22
|
return { ...parsed, scope: { kind: "global" }, adds: [{ dot: { actor: "list", counter: 1 }, origin: "user", createdAt: "" }] };
|
|
16
23
|
}
|
|
17
|
-
const TOOL_NAME_SPELLING = /^[^\s()]
|
|
24
|
+
const TOOL_NAME_SPELLING = /^[^\s()\p{Cc}\p{Cf}]+$/u;
|
|
18
25
|
function mountedSpellingOf(name, roster) {
|
|
19
26
|
const folded = name.toLowerCase();
|
|
20
27
|
for (const mounted of roster)
|
|
@@ -36,6 +43,8 @@ function toolNameReading(name, roster) {
|
|
|
36
43
|
function readAllowEntry(raw, behavior, roster) {
|
|
37
44
|
const inert = (reason) => ({ inert: reason });
|
|
38
45
|
const row = (compiled) => ({ compiled });
|
|
46
|
+
if (typeof raw !== "string")
|
|
47
|
+
return { inert: "grammar", detail: { valueType: valueTypeLabel(raw) } };
|
|
39
48
|
if (raw !== raw.trim())
|
|
40
49
|
return inert("grammar");
|
|
41
50
|
const parsed = parsePermissionRule(raw);
|
|
@@ -94,11 +103,11 @@ export function compileAllowList(entries, behavior, roster) {
|
|
|
94
103
|
const rows = [];
|
|
95
104
|
const inert = [];
|
|
96
105
|
for (const entry of entries) {
|
|
97
|
-
const read =
|
|
106
|
+
const read = typeof entry !== "string" || !voided ? readAllowEntry(entry, behavior, roster) : { inert: "wildcard_all" };
|
|
98
107
|
if ("compiled" in read)
|
|
99
108
|
rows.push({ entry, compiled: read.compiled });
|
|
100
109
|
else
|
|
101
|
-
inert.push({ entry, reason: read.inert, ...read.detail });
|
|
110
|
+
inert.push({ entry: typeof entry === "string" ? entry : nonStringMemberLabel(entry), reason: read.inert, ...read.detail });
|
|
102
111
|
}
|
|
103
112
|
return { rows, inert };
|
|
104
113
|
}
|
|
@@ -307,6 +316,7 @@ const SHADOW_REASON = {
|
|
|
307
316
|
content_question: "the call is the engine's own question to a person",
|
|
308
317
|
hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person",
|
|
309
318
|
ancestor: "the task that delegated this one owns this question",
|
|
319
|
+
dangerous_delete: "this removal targets a protected location (the filesystem root, a top-level directory, a home directory, or a workspace directory or one of its ancestors) — it is cleared by confirming this call, never by an allow rule",
|
|
310
320
|
unverifiable_delete: "this removal could not be verified safe — it is cleared by confirming this call, never by an allow rule",
|
|
311
321
|
transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule",
|
|
312
322
|
org_rule: "an organization policy rule requires approval for this call",
|
|
@@ -366,7 +376,22 @@ export function cardDisclosureOf(decision, fallbackMessage) {
|
|
|
366
376
|
}
|
|
367
377
|
export const ALLOWED_TOOL_NOT_GRANTED_CODE = "permission.allowed_tool_not_granted";
|
|
368
378
|
const NOTICE_ENTRIES_SHOWN = 5;
|
|
379
|
+
const UNSEEN_IN_ENTRY = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Zs}]/gu;
|
|
380
|
+
const ENTRY_RENDER_MAX = 122;
|
|
381
|
+
function escapedCodePoint(ch) {
|
|
382
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
383
|
+
const hex = cp.toString(16).padStart(4, "0");
|
|
384
|
+
return cp > 0xffff ? `\\u{${hex}}` : `\\u${hex}`;
|
|
385
|
+
}
|
|
386
|
+
export function renderListEntry(entry) {
|
|
387
|
+
const quoted = JSON.stringify(entry)
|
|
388
|
+
.replace(/ {2,}/g, (run) => "\\u0020".repeat(run.length))
|
|
389
|
+
.replace(UNSEEN_IN_ENTRY, (ch) => (ch === " " ? ch : escapedCodePoint(ch)));
|
|
390
|
+
return inlineUntrusted(quoted, ENTRY_RENDER_MAX);
|
|
391
|
+
}
|
|
369
392
|
export function inertEntryPhrase(e) {
|
|
393
|
+
if (e.valueType !== undefined)
|
|
394
|
+
return `a value that is ${e.valueType} (grammar: not a string)`;
|
|
370
395
|
const spelling = e.currentName !== undefined ? inlineUntrusted(e.currentName, 120) : undefined;
|
|
371
396
|
const why = (() => {
|
|
372
397
|
if (e.reason === "near_miss")
|
|
@@ -386,7 +411,7 @@ export function inertEntryPhrase(e) {
|
|
|
386
411
|
}
|
|
387
412
|
}
|
|
388
413
|
})();
|
|
389
|
-
return `${
|
|
414
|
+
return `${renderListEntry(e.entry)} (${why})`;
|
|
390
415
|
}
|
|
391
416
|
export function allowedToolsNotGrantedNotice(inert, sessionId) {
|
|
392
417
|
const where = (e) => (e.source === "skill" ? `skill ${inlineUntrusted(e.skill, 80)} ${e.list}` : e.source === "request" ? "this run's allowedTools" : "the deployment's allowedTools");
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* the ask). Both stay where they are and are consumed beside the origin, not folded into it.
|
|
26
26
|
*/
|
|
27
27
|
import type { PermissionResult } from "./tool-policy.js";
|
|
28
|
+
import type { ContainmentFace } from "../tools/fs/containment-face.js";
|
|
28
29
|
/**
|
|
29
30
|
* The closed set, in AUTHORITY order — the order {@link askOriginOf} resolves a fold in when several
|
|
30
31
|
* facts hold on one call (a hook ask folded over a policy ask, an org stamp over either). The order
|
|
@@ -448,8 +449,12 @@ export type ToolMarkSource = "deployment" | "mcp_annotation" | "a2a_default";
|
|
|
448
449
|
* `classifier_no_ruling` is a classifier station's question to a person because the auto-mode classification did not rule
|
|
449
450
|
* (`classifierNoRuling` — it could not run, or replied with no verdict): the denial-limit fallback's row (only a person
|
|
450
451
|
* answers it; the classifier is never asked again).
|
|
452
|
+
* `dangerous_delete` is a removal whose target is a protected location (`dangerousRemovalOf`: the filesystem root, a
|
|
453
|
+
* top-level directory, a home directory, or a workspace directory — the task root, the working directory, an additional
|
|
454
|
+
* working directory — or an ancestor of one): the reference implementation's dangerous-removal breaker, which no mode,
|
|
455
|
+
* allow rule or classifier answers. `unverifiable_delete` is its sibling for a target the command text cannot resolve.
|
|
451
456
|
*/
|
|
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"];
|
|
457
|
+
export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "dangerous_delete", "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"];
|
|
453
458
|
export type AskClass = (typeof ASK_CLASSES)[number];
|
|
454
459
|
/** Membership test for {@link AskClass} — the word crosses the wire on `tool_end.gate.disposition`. */
|
|
455
460
|
export declare function isAskClass(v: unknown): v is AskClass;
|
|
@@ -457,7 +462,8 @@ export declare function isAskClass(v: unknown): v is AskClass;
|
|
|
457
462
|
export interface ModeArbitrationRow {
|
|
458
463
|
/** The ask still reaches a person under `bypassPermissions` (the reference implementation's `bypassImmune`). */
|
|
459
464
|
readonly bypassImmune: boolean;
|
|
460
|
-
/** The auto-mode classifier may answer it
|
|
465
|
+
/** The auto-mode classifier may answer it — as the class stands on its own: the folded row of an ask
|
|
466
|
+
* ({@link foldArbitrationRow}) also takes the read face the ask was raised under. */
|
|
461
467
|
readonly classifierEligible: boolean;
|
|
462
468
|
/** The sandbox-admission leg may admit it (containment holds the effect). */
|
|
463
469
|
readonly sandboxAdmission: boolean;
|
|
@@ -473,6 +479,8 @@ export interface ModeArbitrationRow {
|
|
|
473
479
|
* sandbox columns are the fold of the two former per-word lane tables (only `probe` moved: the classifier may
|
|
474
480
|
* now answer it — the read boundary's unestablishable spelling is fed to it as a deny-table row instead), and
|
|
475
481
|
* `ruleMayClear` restates {@link ruleClearanceOf}'s conclusion per class (a real-approval bit is never rule-clearable).
|
|
482
|
+
* `probe`'s `classifierEligible` is the OPEN read face's reading: a narrowed (`roots`) face withholds the classifier from the class,
|
|
483
|
+
* folded where the ask's row is ({@link foldArbitrationRow}).
|
|
476
484
|
* `mode` is the permission mode's OWN question (`plan`'s two sentences, `decisionReason: "mode"`): the reference
|
|
477
485
|
* implementation puts it to a person past every allow rule and never to an automatic lane, and its bypass conversion
|
|
478
486
|
* releases it like any other non-immune ask. `unstated` is the reference implementation's passthrough conversion: an allow
|
|
@@ -503,6 +511,13 @@ export declare const MODE_ARBITRATION: {
|
|
|
503
511
|
readonly ruleMayClear: false;
|
|
504
512
|
readonly hookAskFloor: false;
|
|
505
513
|
};
|
|
514
|
+
readonly dangerous_delete: {
|
|
515
|
+
readonly bypassImmune: true;
|
|
516
|
+
readonly classifierEligible: false;
|
|
517
|
+
readonly sandboxAdmission: false;
|
|
518
|
+
readonly ruleMayClear: false;
|
|
519
|
+
readonly hookAskFloor: true;
|
|
520
|
+
};
|
|
506
521
|
readonly unverifiable_delete: {
|
|
507
522
|
readonly bypassImmune: true;
|
|
508
523
|
readonly classifierEligible: true;
|
|
@@ -683,6 +698,8 @@ export interface AskClassFacts extends AskOriginFacts {
|
|
|
683
698
|
readonly toolMarkSources?: ReadonlySet<ToolMarkSource>;
|
|
684
699
|
readonly a2aIsolatePeers?: boolean;
|
|
685
700
|
readonly ancestorClasses?: ReadonlySet<AskClass>;
|
|
701
|
+
/** The read face the leg this ask was raised on resolved ({@link ContainmentFace}); absent ⇒ none resolved (a leg with no hands). */
|
|
702
|
+
readonly readFace?: ContainmentFace;
|
|
686
703
|
}
|
|
687
704
|
/** The classification the arbitration reads: the whole set, the disclosed word (the set's first member in
|
|
688
705
|
* {@link ASK_CLASSES} order) and the row folded across the set. */
|
|
@@ -718,8 +735,10 @@ export declare function decisionAskClassesOf(d: Extract<PermissionResult, {
|
|
|
718
735
|
}>): Set<AskClass>;
|
|
719
736
|
/** Fold a class set onto one row: `bypassImmune` and `hookAskFloor` by OR (any immune member keeps the ask; any floored
|
|
720
737
|
* member floors a hook's ask), the three permissive switches by AND (one refusing member withholds the lane). An empty
|
|
721
|
-
* set is `ordinary`.
|
|
722
|
-
|
|
738
|
+
* set is `ordinary`. `readFace` is the face the ask was raised under: `roots` takes the classifier's lane from a set that
|
|
739
|
+
* holds a class the narrowed face withholds from it — the one place the face enters, so every reader of `classifierEligible`
|
|
740
|
+
* reads it already folded. */
|
|
741
|
+
export declare function foldArbitrationRow(classes: ReadonlySet<AskClass>, readFace?: ContainmentFace): ModeArbitrationRow;
|
|
723
742
|
/**
|
|
724
743
|
* The ONE class derivation: the decision half ({@link decisionAskClassesOf}) joined with the gate's own facts —
|
|
725
744
|
* the question tool, an ancestor's mark, the org layer's two arms, the persisted lane's unreadable call, the tool
|
package/dist/core/ask-origin.js
CHANGED
|
@@ -170,6 +170,7 @@ export const ASK_CLASSES = Object.freeze([
|
|
|
170
170
|
"content_question",
|
|
171
171
|
"hook",
|
|
172
172
|
"ancestor",
|
|
173
|
+
"dangerous_delete",
|
|
173
174
|
"unverifiable_delete",
|
|
174
175
|
"transcript_write",
|
|
175
176
|
"org_rule",
|
|
@@ -200,6 +201,7 @@ export const MODE_ARBITRATION = {
|
|
|
200
201
|
content_question: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
|
|
201
202
|
hook: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
|
|
202
203
|
ancestor: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
|
|
204
|
+
dangerous_delete: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
|
|
203
205
|
unverifiable_delete: { bypassImmune: true, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
|
|
204
206
|
transcript_write: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
|
|
205
207
|
org_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
|
|
@@ -222,7 +224,8 @@ export const MODE_ARBITRATION = {
|
|
|
222
224
|
unstated: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: true, hookAskFloor: false },
|
|
223
225
|
ordinary: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: true, hookAskFloor: false },
|
|
224
226
|
};
|
|
225
|
-
|
|
227
|
+
const CLASSIFIER_WITHHELD_ON_ROOTS = new Set(["probe"]);
|
|
228
|
+
export const RRA_EXPLAINERS = new Set(["dangerous_delete", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "classifier_no_ruling", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
|
|
226
229
|
export const RELEASABLE_ASK_CLASSES = ASK_CLASSES.filter((c) => !MODE_ARBITRATION[c].bypassImmune);
|
|
227
230
|
export const MODE_RELEASE_REASONS = Object.freeze(["rule_unreadable"]);
|
|
228
231
|
const MODE_RELEASE_REASON_SET = new Set(MODE_RELEASE_REASONS);
|
|
@@ -243,6 +246,8 @@ export function screenAskClassMembers(askClass, classes) {
|
|
|
243
246
|
}
|
|
244
247
|
export function decisionAskClassesOf(d) {
|
|
245
248
|
const out = new Set();
|
|
249
|
+
if (d.guard === "dangerous_delete")
|
|
250
|
+
out.add("dangerous_delete");
|
|
246
251
|
if (d.guard === "unverifiable_delete")
|
|
247
252
|
out.add("unverifiable_delete");
|
|
248
253
|
if (d.guard === "transcript_write")
|
|
@@ -273,10 +278,11 @@ export function decisionAskClassesOf(d) {
|
|
|
273
278
|
out.add(c);
|
|
274
279
|
return out;
|
|
275
280
|
}
|
|
276
|
-
export function foldArbitrationRow(classes) {
|
|
281
|
+
export function foldArbitrationRow(classes, readFace) {
|
|
277
282
|
if (classes.size === 0)
|
|
278
283
|
return MODE_ARBITRATION.ordinary;
|
|
279
|
-
|
|
284
|
+
const withheld = readFace === "roots" && [...classes].some((c) => CLASSIFIER_WITHHELD_ON_ROOTS.has(c));
|
|
285
|
+
let bypassImmune = false, classifierEligible = !withheld, sandboxAdmission = true, ruleMayClear = true, hookAskFloor = false;
|
|
280
286
|
for (const c of classes) {
|
|
281
287
|
const row = MODE_ARBITRATION[c];
|
|
282
288
|
bypassImmune ||= row.bypassImmune;
|
|
@@ -334,7 +340,7 @@ export function classifyAsk(d, facts) {
|
|
|
334
340
|
if (classes.size === 0)
|
|
335
341
|
classes.add("ordinary");
|
|
336
342
|
const askClass = ASK_CLASSES.find((c) => classes.has(c));
|
|
337
|
-
return { classes, askClass, row: foldArbitrationRow(classes) };
|
|
343
|
+
return { classes, askClass, row: foldArbitrationRow(classes, facts.readFace) };
|
|
338
344
|
}
|
|
339
345
|
export function headlessDenialMessage(toolName, askMessage) {
|
|
340
346
|
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}`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GateOutcome } from "./gate-outcome.js";
|
|
1
|
+
import type { GateOutcome, HostDecider } from "./gate-outcome.js";
|
|
2
2
|
import { type ExecutionOutcomeRecordWord } from "./checkpoint-execution-record.js";
|
|
3
3
|
import type { CheckpointPause } from "./pause-registry.js";
|
|
4
4
|
import type { SystemInjectionPriority } from "./task-notification.js";
|
|
@@ -625,7 +625,7 @@ export type ResumeOutcome = {
|
|
|
625
625
|
* authorization; a replay states its own facts.
|
|
626
626
|
*/
|
|
627
627
|
hostDecision: {
|
|
628
|
-
decidedBy:
|
|
628
|
+
decidedBy: HostDecider;
|
|
629
629
|
approver?: string;
|
|
630
630
|
};
|
|
631
631
|
/**
|
|
@@ -11,7 +11,7 @@ export const refusedOutOfContract = (message) => {
|
|
|
11
11
|
export const isEngineRefusal = (d) => typeof d === "object" && d !== null && ENGINE_REFUSALS.has(d);
|
|
12
12
|
const DECISION_REASONS = Object.freeze(["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only", "skill", "deployment_rule", "request_rule"]);
|
|
13
13
|
const DECISION_ACTIONS = Object.keys({ allow: 0, deny: 0, ask: 0 });
|
|
14
|
-
const GUARDS = Object.keys({ unverifiable_delete: 0, transcript_write: 0, artifact_shared_read: 0 });
|
|
14
|
+
const GUARDS = Object.keys({ dangerous_delete: 0, unverifiable_delete: 0, transcript_write: 0, artifact_shared_read: 0 });
|
|
15
15
|
const RULE_UNREADABLE_WORDS = Object.keys({ content_deny: 0, content_ask: 0, persisted_call: 0 });
|
|
16
16
|
const AUTHORED_BY_WORDS = Object.keys({ deployment: 0 });
|
|
17
17
|
const memberOf = (words) => {
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
*
|
|
29
29
|
* Nothing here judges anything: it answers "which absolute path is this call about". Containment, the
|
|
30
30
|
* sensitive-path deny face and the rule precedence walk are unchanged and still run where they ran.
|
|
31
|
-
* `undefined` means "this call
|
|
32
|
-
*
|
|
31
|
+
* `undefined` means "this call names no path" (no declaration, a non-string slot, an absent slot that targets
|
|
32
|
+
* nothing); a path the call does name whose base this reading lacks is `unreadable` — never a guess, never silence.
|
|
33
33
|
*
|
|
34
34
|
* RepoMap shares the search tools' root-relative `path` semantics but declares NO path target at all (no
|
|
35
35
|
* rule family reaches it, and the rule grammar is derived from the catalog's path targets), so it has no
|
|
@@ -71,8 +71,8 @@ export declare function splitAbsoluteGlobPattern(pattern: string, family: PathFa
|
|
|
71
71
|
} | undefined;
|
|
72
72
|
/**
|
|
73
73
|
* The reading of a call's path slot under ONE declaration: `path` = the absolute target's identity; `unreadable`
|
|
74
|
-
* = the slot spells a target but the base it needs is not a usable base of
|
|
75
|
-
* only, as the ask a person clears — never to the allow arm); `refused` = a namespace refusal of the
|
|
74
|
+
* = the slot spells a target (or targets its base) but the base it needs is not supplied or not a usable base of
|
|
75
|
+
* this tree (fed to TIGHTENING rows only, as the ask a person clears — never to the allow arm); `refused` = a namespace refusal of the
|
|
76
76
|
* spelling itself (a rule speaks about no such spelling: no verdict, no ask — the fs throat delivers the
|
|
77
77
|
* refusal with the same word); `undefined` = no target in this reading.
|
|
78
78
|
*/
|
|
@@ -97,8 +97,9 @@ export type PathTargetReading = {
|
|
|
97
97
|
* ③ otherwise a `patternParam`'s ABSOLUTE pattern supplies its own base directory;
|
|
98
98
|
* ④ otherwise the base itself (the tool searches all of it).
|
|
99
99
|
*
|
|
100
|
-
* A base the
|
|
101
|
-
*
|
|
100
|
+
* A base the caller did not supply yields `unreadable`: a relative spelling with nothing to resolve against
|
|
101
|
+
* names a file this reading cannot locate — claiming it names `/<spelling>` would be a fabrication, and
|
|
102
|
+
* claiming it names nothing would let every rule about that file stay silent.
|
|
102
103
|
*/
|
|
103
104
|
export declare function effectivePathTargetOf(call: ToolCallLike, pt: ToolCallPathTarget | undefined, bases: ToolPathBases): PathTargetReading;
|
|
104
105
|
/**
|
|
@@ -31,10 +31,8 @@ export function effectivePathTargetOf(call, pt, bases) {
|
|
|
31
31
|
if (spelling.spelled !== undefined)
|
|
32
32
|
return resolveSpelling(spelling.spelled, spelling.base, bases);
|
|
33
33
|
const declaredBase = spelling.base;
|
|
34
|
-
if (declaredBase === undefined)
|
|
35
|
-
return undefined;
|
|
36
34
|
const family = pathFamilyOf(bases);
|
|
37
|
-
return isAbsoluteForFamily(family, declaredBase) ? readingOf(pathIdentityOf(declaredBase, family, bases.root ?? bases.cwd)) : unreadableBase(declaredBase);
|
|
35
|
+
return declaredBase !== undefined && isAbsoluteForFamily(family, declaredBase) ? readingOf(pathIdentityOf(declaredBase, family, bases.root ?? bases.cwd)) : unreadableBase(declaredBase);
|
|
38
36
|
}
|
|
39
37
|
export function effectivePathSpellingOf(call, pt, bases) {
|
|
40
38
|
if (pt === undefined)
|
|
@@ -63,9 +61,7 @@ function resolveSpelling(spelled, declaredBase, bases) {
|
|
|
63
61
|
const tree = bases.root ?? bases.cwd;
|
|
64
62
|
if (isAbsoluteForFamily(family, screened.spelled))
|
|
65
63
|
return readingOf(pathIdentityOf(screened.spelled, family, tree));
|
|
66
|
-
if (declaredBase === undefined || family === undefined)
|
|
67
|
-
return undefined;
|
|
68
|
-
if (!isAbsoluteForFamily(family, declaredBase))
|
|
64
|
+
if (declaredBase === undefined || family === undefined || !isAbsoluteForFamily(family, declaredBase))
|
|
69
65
|
return unreadableBase(declaredBase);
|
|
70
66
|
return readingOf(pathIdentityOf(joinForFamily(family, declaredBase, screened.spelled), family, tree));
|
|
71
67
|
}
|
|
@@ -75,5 +71,5 @@ function readingOf(r) {
|
|
|
75
71
|
return r.reason === "relative" || r.reason === "unanchored" ? undefined : { kind: "refused", reason: r.reason };
|
|
76
72
|
}
|
|
77
73
|
function unreadableBase(base) {
|
|
78
|
-
return { kind: "unreadable", reason: `the base this call resolves against (${JSON.stringify(base)}) is not a fully-qualified path of this tree's family` };
|
|
74
|
+
return { kind: "unreadable", reason: base === undefined ? "the call's path is relative to a base that was not supplied" : `the base this call resolves against (${JSON.stringify(base)}) is not a fully-qualified path of this tree's family` };
|
|
79
75
|
}
|
|
@@ -394,8 +394,9 @@ export interface EngineNotice {
|
|
|
394
394
|
* frame's two count keys). They are NOT redelivered (a steer aimed at a finished run must not
|
|
395
395
|
* fire at the next one — unlike ENGINE notes, which pend per session); the loud half of the
|
|
396
396
|
* #257 contract's "accepted = enqueued, not consumed" sentence;
|
|
397
|
-
* `detail: {
|
|
398
|
-
*
|
|
397
|
+
* `detail: { <countKey>, cause, taskId?, sessionId?, runId? }` (the `code` and the `countKey` of each family are the
|
|
398
|
+
* table {@link USER_INPUT_UNDRAINED_FAMILIES}, keyed by the lane the input was queued on — `injectionLaneOf` — and the
|
|
399
|
+
* minter writes exactly those; `cause` on both rows, closed `USER_INPUT_UNDRAINED_CAUSES`: `"leg_end"` — this engine's arm, the
|
|
399
400
|
* leg ended with it still owed; `"crashed"` — minted by the SERVING LAYER, not this engine, before the
|
|
400
401
|
* session's next run, for inputs it had accepted whose consumption was not confirmed before the process died: their queue was the
|
|
401
402
|
* serving layer's, so only it can count them, and it picks the family by the LANE the input was queued on (a `later`
|
|
@@ -404,7 +405,7 @@ export interface EngineNotice {
|
|
|
404
405
|
* per-run identity, the key this account is actually about — see
|
|
405
406
|
* {@link undrainedUserInputNotices}; all omitted when the caller has none, never fabricated). A delegated child
|
|
406
407
|
* leg is announced on its PARENT's session as well — the direct parent only — as a second row per family,
|
|
407
|
-
* `detail: {
|
|
408
|
+
* `detail: { <countKey>, cause, taskId?, sessionId: <the parent's>, childSessionId }` (no `runId`: nothing
|
|
408
409
|
* carries the parent's), message tail `(a child leg, <childSessionId>)`; `childSessionId` present = a child reported it.
|
|
409
410
|
* Per-run, at most once per family (the terminal sweep is a single site) — which is why
|
|
410
411
|
* `taskId`/`sessionId` cannot key it: both answer the same value for two runs of one session.
|
|
@@ -971,6 +972,22 @@ export declare function settleOwedUserInput(account: OwedUserInputAccount, rowRe
|
|
|
971
972
|
*/
|
|
972
973
|
export declare const USER_INPUT_UNDRAINED_CAUSES: readonly ["leg_end", "crashed"];
|
|
973
974
|
export type UserInputUndrainedCause = (typeof USER_INPUT_UNDRAINED_CAUSES)[number];
|
|
975
|
+
/**
|
|
976
|
+
* The two families of the stranded-input account, keyed by the lane the input was queued on (`injectionLaneOf` maps a steer's
|
|
977
|
+
* tier to it): the notice `code` the family is announced under, and the `detail` key that carries its count
|
|
978
|
+
* (`detail[countKey]`, beside `cause`). {@link undrainedUserInputNotices} writes its rows with exactly these two spellings, so a
|
|
979
|
+
* serving layer that mints the `crashed` arm picks its family by lane and spells code and count key the way the engine does.
|
|
980
|
+
*/
|
|
981
|
+
export declare const USER_INPUT_UNDRAINED_FAMILIES: Readonly<{
|
|
982
|
+
readonly steer: Readonly<{
|
|
983
|
+
readonly code: "task.user_steer_undrained";
|
|
984
|
+
readonly countKey: "steer";
|
|
985
|
+
}>;
|
|
986
|
+
readonly followUp: Readonly<{
|
|
987
|
+
readonly code: "task.user_followup_undrained";
|
|
988
|
+
readonly countKey: "followUp";
|
|
989
|
+
}>;
|
|
990
|
+
}>;
|
|
974
991
|
/** Where a child leg's account is ALSO announced: its spawner's session, and that run's task id when the spawner carried one
|
|
975
992
|
* (it never carries the run id, and the row never invents one). */
|
|
976
993
|
interface ParentLegIds {
|
|
@@ -52,6 +52,12 @@ export function settleOwedUserInput(account, rowReturned, ids, onNotice) {
|
|
|
52
52
|
}
|
|
53
53
|
const SETTLED_OWED_ACCOUNTS = new WeakSet();
|
|
54
54
|
export const USER_INPUT_UNDRAINED_CAUSES = Object.freeze(["leg_end", "crashed"]);
|
|
55
|
+
const STEER_UNDRAINED = "task.user_steer_undrained";
|
|
56
|
+
const FOLLOWUP_UNDRAINED = "task.user_followup_undrained";
|
|
57
|
+
export const USER_INPUT_UNDRAINED_FAMILIES = Object.freeze({
|
|
58
|
+
steer: Object.freeze({ code: STEER_UNDRAINED, countKey: "steer" }),
|
|
59
|
+
followUp: Object.freeze({ code: FOLLOWUP_UNDRAINED, countKey: "followUp" }),
|
|
60
|
+
});
|
|
55
61
|
export function parentLegIds(internals) {
|
|
56
62
|
const parentSessionId = internals?.parentSessionId;
|
|
57
63
|
if (typeof parentSessionId !== "string" || parentSessionId === "")
|
|
@@ -73,17 +79,17 @@ export function undrainedUserInputNotices(counts, taskId, sessionId, runId, pare
|
|
|
73
79
|
const cause = "leg_end";
|
|
74
80
|
if (counts.steer > 0) {
|
|
75
81
|
const message = `${counts.steer} user steer(s) ${tail}`;
|
|
76
|
-
const count = { steer: counts.steer, cause };
|
|
77
|
-
own.push({ code:
|
|
82
|
+
const count = { [USER_INPUT_UNDRAINED_FAMILIES.steer.countKey]: counts.steer, cause };
|
|
83
|
+
own.push({ code: STEER_UNDRAINED, message, detail: { ...count, ...tid, ...sid, ...rid } });
|
|
78
84
|
if (toParent)
|
|
79
|
-
copies.push({ code:
|
|
85
|
+
copies.push({ code: STEER_UNDRAINED, message: `${message}${copyTail}`, detail: { ...count, ...ptid, sessionId: parentSession, childSessionId: sessionId } });
|
|
80
86
|
}
|
|
81
87
|
if (counts.followUp > 0) {
|
|
82
88
|
const message = `${counts.followUp} user follow-up(s) ${tail}`;
|
|
83
|
-
const count = { followUp: counts.followUp, cause };
|
|
84
|
-
own.push({ code:
|
|
89
|
+
const count = { [USER_INPUT_UNDRAINED_FAMILIES.followUp.countKey]: counts.followUp, cause };
|
|
90
|
+
own.push({ code: FOLLOWUP_UNDRAINED, message, detail: { ...count, ...tid, ...sid, ...rid } });
|
|
85
91
|
if (toParent)
|
|
86
|
-
copies.push({ code:
|
|
92
|
+
copies.push({ code: FOLLOWUP_UNDRAINED, message: `${message}${copyTail}`, detail: { ...count, ...ptid, sessionId: parentSession, childSessionId: sessionId } });
|
|
87
93
|
}
|
|
88
94
|
return [...own, ...copies];
|
|
89
95
|
}
|
package/dist/core/gate-lanes.js
CHANGED
|
@@ -40,6 +40,7 @@ export async function runGateLanes(pass) {
|
|
|
40
40
|
writeProtected: pass.writeProtected === true,
|
|
41
41
|
writeProtectionUnresolved: pass.writeProtectionUnresolved === true,
|
|
42
42
|
});
|
|
43
|
+
const readFaceNow = input.readFace?.();
|
|
43
44
|
const askOriginFacts = (org, ruleCallUnreadable) => ({
|
|
44
45
|
contentQuestion: heldBy("lanes.contentQuestionClass", QUESTION_TOOL_NAMES, pass.req),
|
|
45
46
|
markedUnresolvable: markedUnresolvable(),
|
|
@@ -49,6 +50,7 @@ export async function runGateLanes(pass) {
|
|
|
49
50
|
mandateMarks: mandateMarksNow(),
|
|
50
51
|
...(input.toolMarkSources !== undefined ? { toolMarkSources: input.toolMarkSources } : {}),
|
|
51
52
|
...(input.a2aIsolatePeers === true ? { a2aIsolatePeers: true } : {}),
|
|
53
|
+
...(readFaceNow !== undefined ? { readFace: readFaceNow } : {}),
|
|
52
54
|
});
|
|
53
55
|
const classifyStanding = (unstamped, facts) => (pass.askClassification = classifyAsk(unstamped, facts));
|
|
54
56
|
let orgTightenCount = 0;
|