@sema-agent/core 5.21.1 → 5.23.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 +103 -0
- package/dist/agents/send-message-tool.js +6 -3
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +48 -6
- package/dist/brain/errors.d.ts +20 -0
- package/dist/brain/errors.js +40 -0
- package/dist/brain/retry.d.ts +16 -2
- package/dist/brain/retry.js +3 -2
- package/dist/brain/status-sink.d.ts +9 -2
- package/dist/brain/stream-engine.d.ts +22 -0
- package/dist/brain/stream-engine.js +41 -10
- package/dist/core/ask-class.d.ts +48 -0
- package/dist/core/ask-class.js +33 -0
- package/dist/core/checkpoint-store.d.ts +103 -10
- package/dist/core/checkpoint-store.js +3 -1
- package/dist/core/governance-codes.d.ts +38 -0
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.d.ts +101 -1
- package/dist/core/hooks.js +116 -14
- package/dist/core/locked-config.d.ts +7 -1
- package/dist/core/locked-config.js +2 -1
- package/dist/core/memory-engine/delegation-provenance.d.ts +62 -0
- package/dist/core/memory-engine/delegation-provenance.js +26 -0
- package/dist/core/memory-engine/engine.d.ts +94 -1
- package/dist/core/memory-engine/engine.js +329 -12
- package/dist/core/memory-engine/header-hints.d.ts +30 -0
- package/dist/core/memory-engine/header-hints.js +41 -0
- package/dist/core/memory-engine/index.d.ts +3 -2
- package/dist/core/memory-engine/index.js +3 -2
- package/dist/core/memory-engine/layout.d.ts +232 -0
- package/dist/core/memory-engine/layout.js +469 -1
- package/dist/core/memory-engine/tools.d.ts +30 -0
- package/dist/core/memory-engine/tools.js +108 -17
- package/dist/core/permission-rule-consent.d.ts +25 -9
- package/dist/core/permission-rule-consent.js +93 -21
- package/dist/core/permission-rule-model.d.ts +9 -1
- package/dist/core/permission-rule-model.js +2 -2
- package/dist/core/permission-rule-org.d.ts +195 -0
- package/dist/core/permission-rule-org.js +234 -0
- package/dist/core/permission-rule-store.d.ts +249 -6
- package/dist/core/permission-rule-store.js +314 -3
- package/dist/core/permission-rule-sync.d.ts +139 -0
- package/dist/core/permission-rule-sync.js +343 -0
- package/dist/core/runner/prepare-memory.js +35 -8
- package/dist/core/runner/prepare-task.d.ts +64 -3
- package/dist/core/runner/prepare-task.js +357 -32
- package/dist/core/runner/runtask.js +166 -6
- package/dist/core/shared-memory/contract.js +19 -4
- package/dist/core/shared-memory/normalize.d.ts +3 -1
- package/dist/core/shared-memory/tools.js +73 -17
- package/dist/core/shared-memory/types.d.ts +27 -1
- package/dist/core/store-contracts/permission-rule-sync-contract.d.ts +33 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +186 -0
- package/dist/core/task-notification.d.ts +5 -2
- package/dist/core/task-registry-agent.d.ts +1 -1
- package/dist/core/task-registry-agent.js +6 -2
- package/dist/core/task-registry-shared.d.ts +9 -2
- package/dist/core/task-registry.d.ts +9 -3
- package/dist/core/task-registry.js +2 -0
- package/dist/core/tool-policy.d.ts +155 -4
- package/dist/core/tool-policy.js +148 -10
- package/dist/core/tool-result-store.d.ts +9 -1
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/trace.d.ts +79 -1
- package/dist/core/types.d.ts +94 -3
- package/dist/core/wiring-manifest.d.ts +16 -1
- package/dist/core/wiring-manifest.js +7 -1
- package/dist/index.d.ts +15 -8
- package/dist/index.js +12 -5
- package/dist/orchestration/goal.d.ts +10 -0
- package/dist/orchestration/goal.js +6 -5
- package/dist/stores/file/adoption/adopt.d.ts +146 -0
- package/dist/stores/file/adoption/adopt.js +616 -0
- package/dist/stores/file/adoption/marker.d.ts +194 -0
- package/dist/stores/file/adoption/marker.js +198 -0
- package/dist/stores/file/background-agent-store.js +2 -0
- package/dist/stores/file/checkpoint-store.d.ts +4 -0
- package/dist/stores/file/checkpoint-store.js +3 -0
- package/dist/stores/file/file-snapshot-store.js +2 -0
- package/dist/stores/file/index.d.ts +2 -0
- package/dist/stores/file/index.js +4 -0
- package/dist/stores/file/mailbox-store.js +2 -0
- package/dist/stores/file/memory-store.js +2 -0
- package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
- package/dist/stores/file/permission-rule-adopt.js +95 -0
- package/dist/stores/file/permission-rule-store.d.ts +80 -2
- package/dist/stores/file/permission-rule-store.js +189 -46
- package/dist/stores/file/session-policy-store.js +2 -0
- package/dist/stores/file/session-store.js +2 -0
- package/dist/stores/file/task-list-store.js +2 -0
- package/dist/stores/file/tool-result-store.js +2 -0
- package/dist/stores/file/usage-window-store.js +2 -0
- package/dist/stores/file/workflow-journal-store.js +2 -0
- package/dist/stores/file/workflow-run-store.js +2 -0
- package/dist/tools/fs/bash-readonly-classifier.js +59 -10
- package/package.json +3 -2
package/dist/core/tool-policy.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { homedir } from "node:os";
|
|
3
|
+
import { brandPolicyAskClass } from "./ask-class.js";
|
|
2
4
|
import { join, normalize as normalizePath, posix as posixPath, sep, win32 as winPath } from "node:path";
|
|
3
5
|
import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
|
|
4
6
|
import { boundInputHashOf } from "./canonical-json.js";
|
|
@@ -12,6 +14,67 @@ export function isApprovalSettledBy(v) {
|
|
|
12
14
|
export function decisionText(d) {
|
|
13
15
|
return d.message;
|
|
14
16
|
}
|
|
17
|
+
export function checkToolPolicyProjection(projection, req) {
|
|
18
|
+
for (const c of projection.components) {
|
|
19
|
+
if (c.kind === "tool_deny") {
|
|
20
|
+
if (c.names.includes(req.toolName)) {
|
|
21
|
+
return { action: "deny", message: `tool "${req.toolName}" is denied by a frozen inherited policy projection` };
|
|
22
|
+
}
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (c.kind === "tool_allowlist") {
|
|
26
|
+
if (!c.names.includes(req.toolName)) {
|
|
27
|
+
return { action: "deny", message: `tool "${req.toolName}" is not in a frozen inherited policy projection's allowlist` };
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!c.tools.includes(req.toolName))
|
|
32
|
+
continue;
|
|
33
|
+
const command = req.args?.command;
|
|
34
|
+
if (typeof command !== "string") {
|
|
35
|
+
if (c.unparseableAction === "deny") {
|
|
36
|
+
return { action: "deny", message: `tool "${req.toolName}" call has no parseable command string (frozen inherited policy projection)` };
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const parsed = parseLeadingCommandName(command);
|
|
41
|
+
if ("reject" in parsed) {
|
|
42
|
+
if (c.unparseableAction === "deny") {
|
|
43
|
+
return { action: "deny", message: `command is not a single simple command (${parsed.reject}) (frozen inherited policy projection)` };
|
|
44
|
+
}
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (c.deny.includes(parsed.name)) {
|
|
48
|
+
return { action: "deny", message: `command "${parsed.name}" is denied by a frozen inherited policy projection` };
|
|
49
|
+
}
|
|
50
|
+
if (c.allow !== undefined && !c.allow.includes(parsed.name) && c.unmatchedAction === "deny") {
|
|
51
|
+
return { action: "deny", message: `command "${parsed.name}" is not in a frozen inherited policy projection's allowlist` };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
export function constraintChainEntryOf(policy, meta) {
|
|
57
|
+
const m = {
|
|
58
|
+
...(meta?.autoModeArmed ? { autoModeArmed: true } : {}),
|
|
59
|
+
...(meta?.durableMandate ? { durableMandate: true } : {}),
|
|
60
|
+
...(meta?.contentMandate ? { contentMandate: true } : {}),
|
|
61
|
+
};
|
|
62
|
+
const p = policy.projection;
|
|
63
|
+
if (p === undefined)
|
|
64
|
+
return { opaque: true, ...m };
|
|
65
|
+
return { components: p.components, requiresLiveRemainder: p.requiresLiveRemainder, ...m };
|
|
66
|
+
}
|
|
67
|
+
function stableJson(v) {
|
|
68
|
+
if (v === null || typeof v !== "object")
|
|
69
|
+
return JSON.stringify(v) ?? "null";
|
|
70
|
+
if (Array.isArray(v))
|
|
71
|
+
return `[${v.map(stableJson).join(",")}]`;
|
|
72
|
+
const keys = Object.keys(v).sort();
|
|
73
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableJson(v[k])}`).join(",")}}`;
|
|
74
|
+
}
|
|
75
|
+
export function constraintChainDigest(chain) {
|
|
76
|
+
return `cpv1:${createHash("sha256").update(stableJson(chain)).digest("hex")}`;
|
|
77
|
+
}
|
|
15
78
|
const ALLOW = { action: "allow" };
|
|
16
79
|
const RETIRED_TEXT_FIELD = "reason";
|
|
17
80
|
const RETIRED_TEXT_FIELD_DENY_MESSAGE = `a permission decision carries the retired "${RETIRED_TEXT_FIELD}" field — rename it to "message" (the one text field ` +
|
|
@@ -100,6 +163,13 @@ export function createAllowDenyPolicy(opts) {
|
|
|
100
163
|
const allow = opts.allow ? new Set(opts.allow) : undefined;
|
|
101
164
|
const deny = new Set(opts.deny ?? []);
|
|
102
165
|
return {
|
|
166
|
+
projection: {
|
|
167
|
+
components: [
|
|
168
|
+
...(opts.deny && opts.deny.length > 0 ? [{ kind: "tool_deny", names: [...opts.deny] }] : []),
|
|
169
|
+
...(opts.allow ? [{ kind: "tool_allowlist", names: [...opts.allow] }] : []),
|
|
170
|
+
],
|
|
171
|
+
requiresLiveRemainder: false,
|
|
172
|
+
},
|
|
103
173
|
nameSets: [{ ...(opts.allow ? { allow: [...opts.allow] } : {}), ...(opts.deny ? { deny: [...opts.deny] } : {}) }],
|
|
104
174
|
check(req) {
|
|
105
175
|
const toolName = req.toolName;
|
|
@@ -124,6 +194,13 @@ export function createApprovalPolicy(opts) {
|
|
|
124
194
|
const deny = new Set(opts.deny ?? []);
|
|
125
195
|
const auto = new Set(opts.autoAllow ?? []);
|
|
126
196
|
return {
|
|
197
|
+
projection: {
|
|
198
|
+
components: [
|
|
199
|
+
...(opts.deny && opts.deny.length > 0 ? [{ kind: "tool_deny", names: [...opts.deny] }] : []),
|
|
200
|
+
...(opts.denyByDefault === true ? [{ kind: "tool_allowlist", names: [...opts.requireApproval, ...(opts.autoAllow ?? [])] }] : []),
|
|
201
|
+
],
|
|
202
|
+
requiresLiveRemainder: opts.requireApproval.length > 0,
|
|
203
|
+
},
|
|
127
204
|
nameSets: [
|
|
128
205
|
{
|
|
129
206
|
ask: [...opts.requireApproval],
|
|
@@ -179,7 +256,25 @@ export function createApprovalPolicy(opts) {
|
|
|
179
256
|
}
|
|
180
257
|
export function combinePolicies(...policies) {
|
|
181
258
|
const nameSets = policies.flatMap(toolPolicyNameSets);
|
|
259
|
+
const childProjections = policies.map((p) => p.projection);
|
|
260
|
+
const projectedComponents = [];
|
|
261
|
+
let combinedRemainder = false;
|
|
262
|
+
for (const p of childProjections) {
|
|
263
|
+
if (p === undefined) {
|
|
264
|
+
combinedRemainder = true;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
projectedComponents.push(...p.components);
|
|
268
|
+
if (p.requiresLiveRemainder) {
|
|
269
|
+
combinedRemainder = true;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const combinedProjection = childProjections.some((p) => p !== undefined)
|
|
274
|
+
? { components: projectedComponents, requiresLiveRemainder: combinedRemainder }
|
|
275
|
+
: undefined;
|
|
182
276
|
return {
|
|
277
|
+
...(combinedProjection !== undefined ? { projection: combinedProjection } : {}),
|
|
183
278
|
...(nameSets.length > 0 ? { nameSets } : {}),
|
|
184
279
|
async check(req, signal) {
|
|
185
280
|
let asked;
|
|
@@ -225,7 +320,22 @@ export function createCoarseCommandNamePolicy(opts) {
|
|
|
225
320
|
const fallback = (reason) => defaultAction === "deny"
|
|
226
321
|
? { action: "deny", message: reason, decisionReason: "rule" }
|
|
227
322
|
: { action: "ask", message: reason, decisionReason: "rule" };
|
|
228
|
-
|
|
323
|
+
const fallbackIsDeny = defaultAction === "deny";
|
|
324
|
+
const projection = {
|
|
325
|
+
components: [
|
|
326
|
+
{
|
|
327
|
+
kind: "shell_command",
|
|
328
|
+
tools: [...shellTools],
|
|
329
|
+
deny: [...deny],
|
|
330
|
+
...(allow !== undefined ? { allow: [...allow] } : {}),
|
|
331
|
+
unmatchedAction: fallbackIsDeny ? "deny" : "none",
|
|
332
|
+
unparseableAction: fallbackIsDeny ? "deny" : "none",
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
requiresLiveRemainder: !fallbackIsDeny,
|
|
336
|
+
};
|
|
337
|
+
return brandPolicyAskClass({
|
|
338
|
+
projection,
|
|
229
339
|
check(req) {
|
|
230
340
|
if (!shellTools.has(req.toolName))
|
|
231
341
|
return ALLOW;
|
|
@@ -245,7 +355,7 @@ export function createCoarseCommandNamePolicy(opts) {
|
|
|
245
355
|
}
|
|
246
356
|
return ALLOW;
|
|
247
357
|
},
|
|
248
|
-
};
|
|
358
|
+
}, "sandbox_local");
|
|
249
359
|
}
|
|
250
360
|
const UNVERIFIABLE_DELETE_SAFE_VARS = ["TMPDIR", "HOME", "PWD"];
|
|
251
361
|
const DELETE_HEADS = new Set(["rm", "xargs", "Remove-Item"]);
|
|
@@ -473,7 +583,7 @@ function hasRecursiveForce(argvTail) {
|
|
|
473
583
|
export function createUnverifiableDeletePolicy(opts) {
|
|
474
584
|
const safeVars = new Set([...UNVERIFIABLE_DELETE_SAFE_VARS, ...(opts?.safeVars ?? [])]);
|
|
475
585
|
const shellTools = canonicalToolNameSet(opts?.tools);
|
|
476
|
-
return {
|
|
586
|
+
return brandPolicyAskClass({
|
|
477
587
|
check(req) {
|
|
478
588
|
if (!shellTools.has(req.toolName))
|
|
479
589
|
return ALLOW;
|
|
@@ -492,7 +602,7 @@ export function createUnverifiableDeletePolicy(opts) {
|
|
|
492
602
|
`(or assign the variable in the same command, e.g. \`DIR=/exact/path; rm -rf "$DIR"\`) so the target can be verified.`,
|
|
493
603
|
};
|
|
494
604
|
},
|
|
495
|
-
};
|
|
605
|
+
}, "external_authority");
|
|
496
606
|
}
|
|
497
607
|
function pathSegments(p) {
|
|
498
608
|
return p.split(/[\\/]/).filter(Boolean);
|
|
@@ -569,7 +679,7 @@ export function createTranscriptIntegrityPolicy(opts) {
|
|
|
569
679
|
`files — modifying or deleting them tampers with the run's own audit trail. Reading them (ls/cat/grep ` +
|
|
570
680
|
`as a single simple command) is fine.`,
|
|
571
681
|
});
|
|
572
|
-
return {
|
|
682
|
+
return brandPolicyAskClass({
|
|
573
683
|
check(req) {
|
|
574
684
|
const toolName = req.toolName;
|
|
575
685
|
if (shellTools.has(toolName)) {
|
|
@@ -597,7 +707,7 @@ export function createTranscriptIntegrityPolicy(opts) {
|
|
|
597
707
|
}
|
|
598
708
|
return ALLOW;
|
|
599
709
|
},
|
|
600
|
-
};
|
|
710
|
+
}, "external_authority");
|
|
601
711
|
}
|
|
602
712
|
const delegatedApproverRoot = new WeakMap();
|
|
603
713
|
export function withDelegationProvenance(onAsk, delegation) {
|
|
@@ -734,12 +844,40 @@ export async function resolveAsk(req, onAsk, signal) {
|
|
|
734
844
|
};
|
|
735
845
|
}
|
|
736
846
|
if (typeof ok === "object" && ok !== null) {
|
|
737
|
-
|
|
738
|
-
|
|
847
|
+
const supplied = ok.settledBy;
|
|
848
|
+
const allowed = ok.allow;
|
|
849
|
+
const suppliedEdit = ok.updatedInput;
|
|
850
|
+
if (supplied !== undefined && supplied !== "human" && supplied !== "timeout") {
|
|
851
|
+
return {
|
|
852
|
+
action: "deny",
|
|
853
|
+
message: `the approver for "${req.toolName}" reported settledBy "${typeof supplied === "string" ? containThrownText(supplied) : supplied === null ? "null" : typeof supplied}", which is outside what a synchronous ` +
|
|
854
|
+
`approver may self-report — it is exactly "human" or "timeout" (or omitted); denied fail-closed`,
|
|
855
|
+
decisionReason: "mode",
|
|
856
|
+
settledBy: "aborted",
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
if (supplied === "timeout" && allowed === true) {
|
|
860
|
+
return {
|
|
861
|
+
action: "deny",
|
|
862
|
+
message: `the approver for "${req.toolName}" returned an allow settled by "timeout" — an elapsed approval window cannot be ` +
|
|
863
|
+
`what approved a call; denied fail-closed (report timeout with allow:false, or allow with settledBy "human"/omitted)`,
|
|
864
|
+
decisionReason: "mode",
|
|
865
|
+
settledBy: "timeout",
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
if (allowed !== true) {
|
|
869
|
+
return {
|
|
870
|
+
action: "deny",
|
|
871
|
+
message: supplied === "timeout"
|
|
872
|
+
? `approval for "${req.toolName}" was not answered before the approver's own window elapsed: ${req.message}`
|
|
873
|
+
: `approval denied for "${req.toolName}": ${req.message}`,
|
|
874
|
+
decisionReason: "mode",
|
|
875
|
+
settledBy: supplied === "timeout" ? "timeout" : "human",
|
|
876
|
+
};
|
|
739
877
|
}
|
|
740
|
-
if (
|
|
878
|
+
if (suppliedEdit === undefined)
|
|
741
879
|
return { action: "allow", decisionReason: "mode", presentedInput: presented.value, settledBy: "human" };
|
|
742
|
-
const edit = tryCloneArgs(
|
|
880
|
+
const edit = tryCloneArgs(suppliedEdit);
|
|
743
881
|
if (!edit.ok) {
|
|
744
882
|
return {
|
|
745
883
|
action: "deny",
|
|
@@ -213,7 +213,15 @@ export declare function firstPartyOffloadPolicy(toolName: string): {
|
|
|
213
213
|
};
|
|
214
214
|
/** Head+tail preview of an offloaded result, pointing at {@link OFFLOAD_TOOL_NAME} via `ref`. Exported so
|
|
215
215
|
* the per-message aggregate budget (design/64 §17.2) produces the SAME preview format (deterministic →
|
|
216
|
-
* byte-identical across queries → prompt-cache safe).
|
|
216
|
+
* byte-identical across queries → prompt-cache safe).
|
|
217
|
+
*
|
|
218
|
+
* #115 (2026-08-09) — the truncation line states WHERE the rest is, not what anyone can do with it.
|
|
219
|
+
* "read more" alone reads as a promise that the full output is retrievable, and the model relays that
|
|
220
|
+
* promise to the person it is talking to; what is actually guaranteed is narrower and exactly two
|
|
221
|
+
* things: the text is retained in this run's tool-result store, and THIS chain can page it back. A
|
|
222
|
+
* host-side read face (an HTTP route that hands the user the whole output) is a separate deployment
|
|
223
|
+
* feature that may or may not exist — so the line names it as a deployment fact instead of implying
|
|
224
|
+
* it, and the model can answer "can I get the full log?" without guessing. */
|
|
217
225
|
export declare function buildPreview(full: string, ref: string, sizes?: {
|
|
218
226
|
head: number;
|
|
219
227
|
tail: number;
|
|
@@ -154,7 +154,8 @@ export function buildPreview(full, ref, sizes, reachableTools) {
|
|
|
154
154
|
const tail = full.slice(tailStart);
|
|
155
155
|
return (`${PERSISTED_OUTPUT_PREFIX}"${ref}" chars="${total}">\n` +
|
|
156
156
|
`${head}\n` +
|
|
157
|
-
`…[truncated — ${total} chars total
|
|
157
|
+
`…[truncated — ${total} chars total, retained in this run's tool-result store; a user-facing read face is a deployment fact. ` +
|
|
158
|
+
`${offloadPagebackHint(ref, "preview", reachableTools)}]\n` +
|
|
158
159
|
`${tail}\n` +
|
|
159
160
|
`</persisted-output>`);
|
|
160
161
|
}
|
package/dist/core/trace.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* core's own contract is only the field names here; it deliberately stops at supplying the FACTS a bridge
|
|
17
17
|
* needs (ids, turn indices, token counts) rather than adopting the convention's vocabulary internally.
|
|
18
18
|
*/
|
|
19
|
-
import type { TaskStatus, ToolEffect } from "./types.js";
|
|
19
|
+
import type { BrainRetryErrClass, TaskStatus, ToolEffect } from "./types.js";
|
|
20
20
|
import type { ThinkingLevel } from "../internal/harness-types.js";
|
|
21
21
|
import type { ToolManifestRow } from "../prompt-assembly/tool-catalog.js";
|
|
22
22
|
/**
|
|
@@ -382,6 +382,76 @@ export type TraceEvent = {
|
|
|
382
382
|
taskId: string;
|
|
383
383
|
message: string;
|
|
384
384
|
ts: number;
|
|
385
|
+
} | {
|
|
386
|
+
/**
|
|
387
|
+
* design/182 §4.6 — a cloud-sync round moved a (rule, scope) from REMOVED back to LIVE: another
|
|
388
|
+
* replica's add outlived the local tombstone (add-wins is the locked merge semantic, so the
|
|
389
|
+
* transition itself is not news — its VISIBILITY is). The person who deleted the rule has to be
|
|
390
|
+
* able to learn that it came back, and to delete it again. Metadata only: the canonical rule
|
|
391
|
+
* text and the scope KIND — a project scope's root path stays out of the trace.
|
|
392
|
+
*/
|
|
393
|
+
kind: "permission.rule_sync_resurrected";
|
|
394
|
+
version: 1;
|
|
395
|
+
/** The bucket this round synced. A local-owner bucket cannot sync, so this is always a principal. */
|
|
396
|
+
principal: string;
|
|
397
|
+
rule: string;
|
|
398
|
+
scopeKind: "global" | "project";
|
|
399
|
+
ts: number;
|
|
400
|
+
} | {
|
|
401
|
+
/**
|
|
402
|
+
* design/182 §4.3/§5.2 — a cloud-sync round REFUSED an inbound row, or quarantined a local one.
|
|
403
|
+
* The reason is a closed code from the governance-codes family (never free text): the machine
|
|
404
|
+
* reads the table. One event per dropped row — a silently refused row is indistinguishable from
|
|
405
|
+
* a row that never existed, which is the failure this event exists to prevent.
|
|
406
|
+
*/
|
|
407
|
+
kind: "permission.rule_sync_dropped";
|
|
408
|
+
version: 1;
|
|
409
|
+
principal: string;
|
|
410
|
+
rule: string;
|
|
411
|
+
scopeKind: "global" | "project";
|
|
412
|
+
/** Closed reason code ({@link import("./governance-codes.js").RuleSyncDropReason}). */
|
|
413
|
+
reason: string;
|
|
414
|
+
ts: number;
|
|
415
|
+
} | {
|
|
416
|
+
/**
|
|
417
|
+
* design/182 §7.4 — an org-governed deployment could NOT adjudicate against an org snapshot
|
|
418
|
+
* (never installed, provider failed past the staleness bound, or the persisted state was
|
|
419
|
+
* refused). The gate answered by failing the WHOLE decision boundary closed: every terminal
|
|
420
|
+
* allow became a real-approval ask. This is the operator-facing half of that tighten — the
|
|
421
|
+
* deployment is paying the "ask about everything" price and has to be able to see why.
|
|
422
|
+
*/
|
|
423
|
+
kind: "permission.org_snapshot_unavailable";
|
|
424
|
+
version: 1;
|
|
425
|
+
taskId: string;
|
|
426
|
+
toolName: string;
|
|
427
|
+
toolCallId: string;
|
|
428
|
+
/** The overlay's own disclosure lines (deployment-authored/engine-authored, never model text),
|
|
429
|
+
* joined — provider failure, refused snapshot, rollback refusal, missing durable persistence. */
|
|
430
|
+
message: string;
|
|
431
|
+
ts: number;
|
|
432
|
+
} | {
|
|
433
|
+
/**
|
|
434
|
+
* F-012 L2 — a pending ask was AUTO-ADMITTED by the sandbox-admission leg: the deployment's
|
|
435
|
+
* execution env declares an isolated sandbox, every surviving ask on the call was
|
|
436
|
+
* engine-classified `sandbox_local`, and the call crosses no declared boundary. This record is
|
|
437
|
+
* the durable admission ledger's operator half (the transcript half rides the tool result's
|
|
438
|
+
* system-reminder context); a consumer anchors on THIS kind, never on wording.
|
|
439
|
+
*/
|
|
440
|
+
kind: "permission.sandbox_admitted";
|
|
441
|
+
version: 1;
|
|
442
|
+
taskId: string;
|
|
443
|
+
toolName: string;
|
|
444
|
+
toolCallId: string;
|
|
445
|
+
/** The engine ask classes of the admitted call's surviving asks (all `sandbox_local` by the
|
|
446
|
+
* admission predicate; carried explicitly so the ledger states what was judged, not just that
|
|
447
|
+
* judgment passed). */
|
|
448
|
+
askClasses: readonly string[];
|
|
449
|
+
/** Which layers raised the admitted asks (`caller_policy` / `ancestor_constraint` /
|
|
450
|
+
* `gate_safety_tighten`). */
|
|
451
|
+
sourceLayers: readonly string[];
|
|
452
|
+
/** The boundary classification the admission asserted. */
|
|
453
|
+
boundary: "sandbox_internal";
|
|
454
|
+
ts: number;
|
|
385
455
|
} | {
|
|
386
456
|
/** C1 — the failover brain served this call from a FALLBACK entry (`createFailoverBrain`): the
|
|
387
457
|
* primary (and possibly earlier hops) failed cleanly upfront. Without this, same-model gateway
|
|
@@ -418,6 +488,14 @@ export type TraceEvent = {
|
|
|
418
488
|
/** 1-based attempt number that was ABANDONED (the retry that follows is attempt+1). */
|
|
419
489
|
attempt: number;
|
|
420
490
|
phase: "connect" | "midstream";
|
|
491
|
+
/** WHY the attempt is being retried, as the closed neutral bucket the user-facing status frame
|
|
492
|
+
* also carries ({@link BrainRetryErrClass}) — an operator reading this and a user reading a
|
|
493
|
+
* progress line then agree on the reason. Optional: absent on frames minted before it existed. */
|
|
494
|
+
errClass?: BrainRetryErrClass;
|
|
495
|
+
/** The backoff about to be slept, ms — 0 for an immediate re-send. Together with `attempt` this
|
|
496
|
+
* makes a whole retry chain reconstructible from the trace alone, which is what a caller that saw
|
|
497
|
+
* only silence needs to explain where the time went. */
|
|
498
|
+
nextDelayMs?: number;
|
|
421
499
|
ts: number;
|
|
422
500
|
} | {
|
|
423
501
|
/** C2 — the agent loop drove one of its self-heal recoveries (malformed-tool retry / thinking-only
|
package/dist/core/types.d.ts
CHANGED
|
@@ -737,6 +737,17 @@ export interface ToolExecuteContext {
|
|
|
737
737
|
* tool runs outside a Runner task.
|
|
738
738
|
*/
|
|
739
739
|
inheritedGateForChildren?: () => import("./runner/prepare-task.js").InheritedGate;
|
|
740
|
+
/**
|
|
741
|
+
* design/180 half A — the delegation runtime-provenance ARMING face. Runner-filled; a delegation
|
|
742
|
+
* tool calls it at spawn time: a non-undefined return means this (parent) run is armed (it mounts
|
|
743
|
+
* a memory session, or is itself recording for its own parent) and carries the chain's FROZEN
|
|
744
|
+
* content-safety snapshot — the tool then mints the child's recorder ref and threads both into
|
|
745
|
+
* the child's trusted `RunInternals.delegationProvenance`. Undefined return / absent field ⇒ the
|
|
746
|
+
* child spawns without a recorder (its deliveries read `unknown` and every judgment stays on the
|
|
747
|
+
* static floor — v1 behavior byte-identical). Same trust posture as
|
|
748
|
+
* {@link inheritedGateForChildren}: never a model/tool argument, never a TaskSpec field.
|
|
749
|
+
*/
|
|
750
|
+
delegationProvenanceForChildren?: () => import("./memory-engine/delegation-provenance.js").DelegationContentSafety | undefined;
|
|
740
751
|
/**
|
|
741
752
|
* RB-201 FO-3 (form-one audit, CC 220 `Ipd`/`ein` parity) — the auto-mode classifier decider ARMED
|
|
742
753
|
* for THIS task (`RuntimeCaps.autoMode === true` AND `RunnerDeps.autoMode` both present; the same
|
|
@@ -1802,6 +1813,17 @@ export interface TaskSpec {
|
|
|
1802
1813
|
agents?: AgentDefinition[];
|
|
1803
1814
|
/** Gate tool calls before they run (allow/deny/approval). Overrides `RunnerDeps.toolPolicy`. */
|
|
1804
1815
|
toolPolicy?: import("./tool-policy.js").ToolPolicy;
|
|
1816
|
+
/**
|
|
1817
|
+
* #93 (F-012 L3) — the OVERRIDE seat for the durable resume-edit re-adjudication policy: the policy
|
|
1818
|
+
* an approver's `updatedInput` EDIT is re-checked against before a resumed pending call executes
|
|
1819
|
+
* (and, at park-mint time, the policy a store-codec-moved projection is re-adjudicated by). ABSENT
|
|
1820
|
+
* falls back to the caller policy (`toolPolicy ?? RunnerDeps.toolPolicy`) — never to a silent skip:
|
|
1821
|
+
* the pre-#93 shape skipped the whole recheck for deployments with no caller policy, which let a
|
|
1822
|
+
* fidelity-projection-moved value land unadjudicated. Supply this only to make the resume-edit
|
|
1823
|
+
* boundary STRICTER/different from the live caller policy; it never widens (the frozen ancestor
|
|
1824
|
+
* projections and the deny-narrowing layers still apply regardless).
|
|
1825
|
+
*/
|
|
1826
|
+
basePolicyForResumeEdit?: import("./tool-policy.js").ToolPolicy;
|
|
1805
1827
|
/** How `ask` decisions resolve for this task (headless auto-deny by default). Overrides `RunnerDeps.onAsk`. */
|
|
1806
1828
|
onAsk?: import("./tool-policy.js").OnAsk;
|
|
1807
1829
|
/** Content-ask seam (design/64 §5): routes an AskUserQuestion tool call to a real human/UI. When set, the
|
|
@@ -2999,6 +3021,24 @@ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "c
|
|
|
2999
3021
|
* Both mean the same thing to a renderer — stop showing the retry state.
|
|
3000
3022
|
*/
|
|
3001
3023
|
| "recovered" | "gave_up";
|
|
3024
|
+
/**
|
|
3025
|
+
* WHY a retry wait is happening, as a closed, provider-NEUTRAL bucket — the companion to
|
|
3026
|
+
* {@link BrainStatusPhase}, which says what the brain is doing about it. A consumer rendering an
|
|
3027
|
+
* unattended progress line ("no answer for three minutes") needs the reason, and until this existed the
|
|
3028
|
+
* only carriers of it were the HTTP status and the syscall code, neither of which may cross this
|
|
3029
|
+
* channel. Values are about the SHAPE of the failure, never its provider taxonomy:
|
|
3030
|
+
* - `connect_refused` — the attempt got a definite negative about the target itself (nothing accepts
|
|
3031
|
+
* at that address, or the name has no address). This is the class the SHORT retry lane serves.
|
|
3032
|
+
* - `transport` — any other transport-level failure: a connect timeout, a reset, a mid-stream
|
|
3033
|
+
* tear, a stalled stream. No verdict about the target; the full ladder applies.
|
|
3034
|
+
* - `rate_limit` — the provider asked the caller to slow down.
|
|
3035
|
+
* - `server` — the provider reported a failure on its own side.
|
|
3036
|
+
* - `http` — a response the status predicate calls terminal, retried anyway because the
|
|
3037
|
+
* provider's own explicit retry verdict said to.
|
|
3038
|
+
* - `output_cap` — not a failure of the connection at all: the request is being re-sent with a
|
|
3039
|
+
* lowered output cap after the provider reported the context limit exceeded (no backoff).
|
|
3040
|
+
*/
|
|
3041
|
+
export type BrainRetryErrClass = "connect_refused" | "transport" | "rate_limit" | "server" | "http" | "output_cap";
|
|
3002
3042
|
/** design/99 §E3/§E10 — the payload of a {@link TaskEvent} `status` event (and the brain→runner signal). */
|
|
3003
3043
|
export interface BrainStatus {
|
|
3004
3044
|
phase: BrainStatusPhase;
|
|
@@ -3013,8 +3053,14 @@ export interface BrainStatus {
|
|
|
3013
3053
|
/** RB-420-c — 1-based index of the attempt that just failed (the wait precedes attempt `attempt + 1`);
|
|
3014
3054
|
* same numbering as the `brain.retry` telemetry frame. Absent on frames that are not a retry wait. */
|
|
3015
3055
|
attempt?: number;
|
|
3016
|
-
/** RB-420-c — the retry budget of THIS lane, so a consumer can render "attempt 3 of 10".
|
|
3056
|
+
/** RB-420-c — the retry budget of THIS lane, so a consumer can render "attempt 3 of 10". Lane, not
|
|
3057
|
+
* engine: a failure class served by a shorter ladder reports that ladder's budget, so the fraction a
|
|
3058
|
+
* consumer renders is the one actually in force rather than the engine-wide ceiling. */
|
|
3017
3059
|
maxRetries?: number;
|
|
3060
|
+
/** Why this wait is happening ({@link BrainRetryErrClass}). Present on retry-wait frames whose cause
|
|
3061
|
+
* the engine classified; absent on frames that are not a retry wait (`recovered`/`gave_up`) and on a
|
|
3062
|
+
* `circuit_open` fast-fail, which is a local verdict rather than an observed failure. */
|
|
3063
|
+
errClass?: BrainRetryErrClass;
|
|
3018
3064
|
}
|
|
3019
3065
|
/**
|
|
3020
3066
|
* design/97 CORE-8 (③): one lightweight TOOL-ACTIVITY beat surfaced from a running task, for a per-agent live
|
|
@@ -3955,8 +4001,11 @@ export interface BackgroundChildEvent {
|
|
|
3955
4001
|
*/
|
|
3956
4002
|
editedFiles?: import("../agents/subagent-steps.js").SubagentEditedFile[];
|
|
3957
4003
|
/**
|
|
3958
|
-
* terminal (residual observability, lane E): `true` when the child can be revived with `SendMessage`
|
|
3959
|
-
* retained and
|
|
4004
|
+
* terminal (residual observability, lane E): `true` when the child can be revived with `SendMessage` —
|
|
4005
|
+
* either its session is retained live and the run was not killed, or it has a NAMED durable row that no
|
|
4006
|
+
* USER stop closed (a parent teardown, a reap or a host death is precisely what the durable revival
|
|
4007
|
+
* lane recovers from). `false` for a user-stopped child, an anonymous or store-less one, and for the
|
|
4008
|
+
* session-teardown reap frame, whose lane cannot know what survives the teardown.
|
|
3960
4009
|
* Lets a parent's orchestration logic decide "continue it" vs "start fresh" without trial-and-error.
|
|
3961
4010
|
*/
|
|
3962
4011
|
resumable?: boolean;
|
|
@@ -4151,6 +4200,44 @@ export interface RunnerDeps {
|
|
|
4151
4200
|
* disclosure — a loosening face fails toward asking.
|
|
4152
4201
|
*/
|
|
4153
4202
|
permissionRuleStore?: import("./permission-rule-store.js").PermissionRuleStoreProvider;
|
|
4203
|
+
/**
|
|
4204
|
+
* design/182 §4.5 (F-011) — declare that this deployment keeps its permission rules in the
|
|
4205
|
+
* IDENTITY-LESS local bucket: a task with no `principal` resolves rules through the provider's
|
|
4206
|
+
* `forLocalOwner()` face instead of resolving to zero rules.
|
|
4207
|
+
*
|
|
4208
|
+
* A DECLARATION, never an inference (same rule as {@link permissionRuleOrg}): the local bucket is
|
|
4209
|
+
* "this machine's owner", a fact only the deployment knows. Omitted/`false` ⇒ v1 exactly — an
|
|
4210
|
+
* unauthenticated task reads no rules. Declared `true` while the wired provider has no local-owner
|
|
4211
|
+
* face (or while no provider is wired at all) is a configuration contradiction and is refused loudly
|
|
4212
|
+
* at prepare, rather than silently degrading to "the rules this person approved stopped applying".
|
|
4213
|
+
*
|
|
4214
|
+
* A local-owner bucket cannot cloud-sync (syncing is an authenticated act); it is adopted into a
|
|
4215
|
+
* principal bucket by `adoptFilePermissionRuleStore`, after which the local-owner face resolves the
|
|
4216
|
+
* adopted principal's bucket forever.
|
|
4217
|
+
*/
|
|
4218
|
+
localOwnerRules?: boolean;
|
|
4219
|
+
/**
|
|
4220
|
+
* design/182 §7 — the ORG rule overlay for an org-GOVERNED deployment. Constructed with
|
|
4221
|
+
* `createOrgRuleOverlay` (that constructor is the boot gate: a governed declaration with no snapshot
|
|
4222
|
+
* provider refuses to boot), so a value here IS the declaration — nothing is inferred from wiring.
|
|
4223
|
+
*
|
|
4224
|
+
* Present ⇒ every tool call is adjudicated against the org's published deny/ask snapshot BEFORE the
|
|
4225
|
+
* ask-resolution chain (org deny > org ask > personal allow rule > bare ask), and while the overlay
|
|
4226
|
+
* cannot adjudicate (never installed / past the staleness bound / a refused rollback) the WHOLE
|
|
4227
|
+
* decision boundary fails closed: every terminal allow becomes a real-approval ask and both
|
|
4228
|
+
* ask→allow seams are disarmed. Omitted ⇒ the org layer does not exist and the decision path is
|
|
4229
|
+
* byte-identical to a build without it.
|
|
4230
|
+
*/
|
|
4231
|
+
permissionRuleOrg?: import("./permission-rule-org.js").OrgRuleOverlay;
|
|
4232
|
+
/**
|
|
4233
|
+
* design/182 §9 — DECLARE that this deployment drives cloud sync (`syncPermissionRules`) for the
|
|
4234
|
+
* wired rule store. Purely a disclosure input: it is reported as `permissionRules.syncWired` on the
|
|
4235
|
+
* wiring manifest and changes no decision. It exists because the sync loop is HOST-driven (core
|
|
4236
|
+
* bundles no fetch and owns no timer), so there is nothing for the engine to infer — and "are this
|
|
4237
|
+
* machine's standing approvals shared with other devices and a server?" is exactly the trust-domain
|
|
4238
|
+
* fact an operator must be able to read off the manifest instead of guessing.
|
|
4239
|
+
*/
|
|
4240
|
+
permissionRuleSyncWired?: boolean;
|
|
4154
4241
|
/**
|
|
4155
4242
|
* design/99 §K — resolve the **per-principal runtime ENTITLEMENTS** the engine
|
|
4156
4243
|
* ENFORCES server-side, keyed by `spec.principal`. A deployment (the SERVICE) implements it over center's
|
|
@@ -4543,6 +4630,10 @@ export interface RunnerDeps {
|
|
|
4543
4630
|
mcpImageResizer?: import("./mcp.js").McpImageResizer;
|
|
4544
4631
|
/** Default tool-call gate for all tasks (a task's own `toolPolicy` overrides this). */
|
|
4545
4632
|
toolPolicy?: import("./tool-policy.js").ToolPolicy;
|
|
4633
|
+
/** Deployment default for {@link TaskSpec.basePolicyForResumeEdit} (#93 / F-012 L3): the resume-edit
|
|
4634
|
+
* re-adjudication override. Resolution: `spec.basePolicyForResumeEdit ?? THIS ?? (spec.toolPolicy ??
|
|
4635
|
+
* deps.toolPolicy)` — absence falls back to the caller policy, never to a silent skip. */
|
|
4636
|
+
basePolicyForResumeEdit?: import("./tool-policy.js").ToolPolicy;
|
|
4546
4637
|
/**
|
|
4547
4638
|
* How `ask` decisions resolve when a policy/hook requests human confirmation (design/37). Default
|
|
4548
4639
|
* (omitted) = `"deny"`: **headless auto-deny** — no approver, so `ask` resolves deterministically to
|
|
@@ -136,6 +136,17 @@ export interface WiringManifest {
|
|
|
136
136
|
* "this build has no such feature", and a present-and-false one says "it exists and is off here". */
|
|
137
137
|
permissionRules: {
|
|
138
138
|
storeWired: boolean;
|
|
139
|
+
/** design/182 §9 — this deployment DECLARED that it drives cloud sync for the wired rule store
|
|
140
|
+
* (`RunnerDeps.permissionRuleSyncWired`). Connecting a bucket to a sync endpoint merges every
|
|
141
|
+
* device, the transport and the server into ONE consent trust domain, which is a fact an operator
|
|
142
|
+
* reads off the manifest rather than infers. `false` on a deployment that does not sync — same
|
|
143
|
+
* present-and-false shape as `storeWired`, for the same reason. */
|
|
144
|
+
syncWired: boolean;
|
|
145
|
+
/** design/182 §7 — this deployment DECLARED org governance (`RunnerDeps.permissionRuleOrg`, whose
|
|
146
|
+
* constructor refuses to boot a governed declaration with no snapshot provider). `true` also means
|
|
147
|
+
* the fail-closed availability contract is armed: while no org snapshot can be adjudicated
|
|
148
|
+
* against, every terminal allow tightens to a real-approval ask. */
|
|
149
|
+
orgGoverned: boolean;
|
|
139
150
|
};
|
|
140
151
|
/**
|
|
141
152
|
* Governance surfaces (presence facts only). `audience: "operator"` is the MACHINE-READABLE
|
|
@@ -192,6 +203,10 @@ export interface WiringFacts {
|
|
|
192
203
|
backgroundAgentStoreWired: boolean;
|
|
193
204
|
/** design/179 — a persisted allow-rule store provider is wired. */
|
|
194
205
|
permissionRuleStoreWired: boolean;
|
|
206
|
+
/** design/182 §9 — the deployment declared that it drives cloud sync for that store. */
|
|
207
|
+
permissionRuleSyncWired: boolean;
|
|
208
|
+
/** design/182 §7 — the deployment declared org governance (an org rule overlay is wired). */
|
|
209
|
+
permissionRuleOrgGoverned: boolean;
|
|
195
210
|
hostChildEventSinkWired: boolean;
|
|
196
211
|
lockedConfigWired: boolean;
|
|
197
212
|
complianceWired: boolean;
|
|
@@ -200,7 +215,7 @@ export interface WiringFacts {
|
|
|
200
215
|
}
|
|
201
216
|
/** Named view of the deps seats the static half reads (a `Pick` of the real {@link RunnerDeps} —
|
|
202
217
|
* single-source shapes, no parallel hand-copied interface). */
|
|
203
|
-
export type StaticWiringDeps = Pick<RunnerDeps, "onAsk" | "onQuestion" | "interactionPosture" | "onElicit" | "checkpointStore" | "sessionStore" | "backgroundAgentStore" | "onBackgroundChildEvent" | "lockedConfig" | "compliancePostureResolver" | "memoryScopeAdmission" | "retentionPolicy" | "permissionRuleStore">;
|
|
218
|
+
export type StaticWiringDeps = Pick<RunnerDeps, "onAsk" | "onQuestion" | "interactionPosture" | "onElicit" | "checkpointStore" | "sessionStore" | "backgroundAgentStore" | "onBackgroundChildEvent" | "lockedConfig" | "compliancePostureResolver" | "memoryScopeAdmission" | "retentionPolicy" | "permissionRuleStore" | "permissionRuleSyncWired" | "permissionRuleOrg">;
|
|
204
219
|
/** Named view of the spec seats the static half reads (a `Pick` of the real {@link TaskSpec}). */
|
|
205
220
|
export type StaticWiringSpec = Pick<TaskSpec, "onAsk" | "onQuestion" | "checkpointStore" | "durableApproval" | "mcp" | "interactiveTools" | "interactionPosture">;
|
|
206
221
|
/**
|
|
@@ -86,7 +86,11 @@ export function deriveWiringManifest(facts) {
|
|
|
86
86
|
parkLane,
|
|
87
87
|
session: { store: manifestDurabilityOf(facts.sessionDurability) },
|
|
88
88
|
fleet: { backgroundAgentStore: facts.backgroundAgentStoreWired, hostChildEventSink: facts.hostChildEventSinkWired },
|
|
89
|
-
permissionRules: {
|
|
89
|
+
permissionRules: {
|
|
90
|
+
storeWired: facts.permissionRuleStoreWired,
|
|
91
|
+
syncWired: facts.permissionRuleSyncWired,
|
|
92
|
+
orgGoverned: facts.permissionRuleOrgGoverned,
|
|
93
|
+
},
|
|
90
94
|
governance: {
|
|
91
95
|
audience: "operator",
|
|
92
96
|
lockedConfig: facts.lockedConfigWired,
|
|
@@ -179,6 +183,8 @@ export function describeStaticWiring(deps, spec = {}) {
|
|
|
179
183
|
sessionDurability: resolveDeclaredDurability(deps.sessionStore, "sessionStore"),
|
|
180
184
|
backgroundAgentStoreWired: deps.backgroundAgentStore !== undefined,
|
|
181
185
|
permissionRuleStoreWired: deps.permissionRuleStore !== undefined,
|
|
186
|
+
permissionRuleSyncWired: deps.permissionRuleSyncWired === true,
|
|
187
|
+
permissionRuleOrgGoverned: deps.permissionRuleOrg !== undefined,
|
|
182
188
|
hostChildEventSinkWired: deps.onBackgroundChildEvent !== undefined,
|
|
183
189
|
lockedConfigWired: deps.lockedConfig !== undefined,
|
|
184
190
|
complianceWired: deps.compliancePostureResolver !== undefined,
|