@sema-agent/core 7.49.5 → 7.50.1
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 +157 -0
- package/dist/agents/observer.js +3 -10
- package/dist/agents/subagent-steps.d.ts +6 -14
- package/dist/agents/subagent-steps.js +3 -17
- package/dist/brain/stream-shared.d.ts +1 -1
- package/dist/core/allow-entry.d.ts +1 -1
- package/dist/core/allow-entry.js +1 -1
- package/dist/core/ask-origin.d.ts +15 -11
- package/dist/core/ask-origin.js +6 -14
- package/dist/core/auto-mode-prompt.js +6 -2
- package/dist/core/checkpoint-store.d.ts +6 -4
- package/dist/core/checkpoint-store.js +1 -1
- package/dist/core/context-edit.d.ts +2 -2
- package/dist/core/decision-receipt.d.ts +2 -2
- package/dist/core/decision-receipt.js +30 -11
- package/dist/core/delegation-frames.d.ts +66 -18
- package/dist/core/delegation-frames.js +22 -0
- package/dist/core/engine-notice.d.ts +7 -5
- package/dist/core/frame-settlement.d.ts +6 -1
- package/dist/core/gate-fold.js +34 -19
- package/dist/core/gate-lanes.js +15 -13
- package/dist/core/gate-outcome.d.ts +2 -2
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +28 -20
- package/dist/core/human-input-projection.js +2 -0
- package/dist/core/lsp-diagnostics.d.ts +3 -0
- package/dist/core/lsp-diagnostics.js +6 -0
- package/dist/core/lsp-session.d.ts +39 -17
- package/dist/core/lsp-session.js +118 -58
- package/dist/core/lsp.d.ts +83 -3
- package/dist/core/lsp.js +146 -16
- package/dist/core/mcp.js +2 -1
- package/dist/core/media-byte-cap.d.ts +14 -0
- package/dist/core/media-byte-cap.js +15 -3
- package/dist/core/memory-engine/consolidation-driver.d.ts +23 -1
- package/dist/core/memory-engine/consolidation-driver.js +54 -6
- package/dist/core/memory-engine/consolidation-write-gate.d.ts +46 -0
- package/dist/core/memory-engine/consolidation-write-gate.js +82 -0
- package/dist/core/memory-engine/consolidation.d.ts +17 -0
- package/dist/core/memory-engine/consolidation.js +1 -0
- package/dist/core/memory-engine/distiller.d.ts +8 -2
- package/dist/core/memory-engine/distiller.js +8 -6
- package/dist/core/memory-engine/engine.d.ts +37 -4
- package/dist/core/memory-engine/engine.js +127 -38
- package/dist/core/memory-engine/file-backend.d.ts +8 -3
- package/dist/core/memory-engine/file-backend.js +44 -16
- package/dist/core/memory-engine/index.d.ts +2 -2
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/memory-engine/landing-admission.d.ts +65 -0
- package/dist/core/memory-engine/landing-admission.js +68 -0
- package/dist/core/memory-engine/types.d.ts +23 -0
- package/dist/core/mode-policy.d.ts +3 -2
- package/dist/core/mode-policy.js +2 -0
- package/dist/core/permission-rule-model.d.ts +19 -13
- package/dist/core/permission-rule-model.js +7 -4
- package/dist/core/permission-rule-provider.d.ts +11 -0
- package/dist/core/permission-rules.d.ts +1 -1
- package/dist/core/permission-rules.js +5 -2
- package/dist/core/persisted-rule-arms.d.ts +15 -7
- package/dist/core/persisted-rule-arms.js +13 -6
- package/dist/core/reminder-mint.d.ts +5 -0
- package/dist/core/reminder-mint.js +1 -0
- package/dist/core/retired-keys.d.ts +8 -0
- package/dist/core/retired-keys.js +4 -0
- package/dist/core/rule-lane-reads.d.ts +4 -4
- package/dist/core/rule-lane-reads.js +10 -1
- package/dist/core/runner/construction-doors.d.ts +10 -4
- package/dist/core/runner/construction-doors.js +25 -2
- package/dist/core/runner/contracts.d.ts +20 -3
- package/dist/core/runner/env-capability-census.js +1 -1
- package/dist/core/runner/gate-exit.js +2 -1
- package/dist/core/runner/memory-consolidation-driver.d.ts +6 -4
- package/dist/core/runner/memory-consolidation-driver.js +5 -2
- package/dist/core/runner/memory-consolidation-write-judge.d.ts +17 -0
- package/dist/core/runner/memory-consolidation-write-judge.js +14 -0
- package/dist/core/runner/permission-rule-lanes.d.ts +16 -22
- package/dist/core/runner/permission-rule-lanes.js +13 -43
- package/dist/core/runner/person-path-rows.d.ts +244 -0
- package/dist/core/runner/person-path-rows.js +387 -0
- package/dist/core/runner/policy-throw.d.ts +34 -0
- package/dist/core/runner/policy-throw.js +25 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +8 -1
- package/dist/core/runner/prepare-artifact.d.ts +5 -3
- package/dist/core/runner/prepare-artifact.js +2 -8
- package/dist/core/runner/prepare-ask-lane.d.ts +2 -1
- package/dist/core/runner/prepare-ask-lane.js +14 -16
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
- package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
- package/dist/core/runner/prepare-context-lane.js +2 -2
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -3
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +19 -10
- package/dist/core/runner/prepare-hands-readface.js +57 -43
- package/dist/core/runner/prepare-lsp.d.ts +15 -13
- package/dist/core/runner/prepare-lsp.js +11 -11
- package/dist/core/runner/prepare-policy-chain.d.ts +8 -4
- package/dist/core/runner/prepare-policy-chain.js +11 -10
- package/dist/core/runner/prepare-run-refs.js +1 -1
- package/dist/core/runner/prepare-task.js +24 -16
- package/dist/core/runner/prepare-turn-wiring.d.ts +13 -6
- package/dist/core/runner/prepare-turn-wiring.js +19 -8
- package/dist/core/runner/read-root-grant.d.ts +1 -25
- package/dist/core/runner/read-root-grant.js +1 -17
- package/dist/core/runner/retired-runner-deps.d.ts +2 -2
- package/dist/core/runner/run-brain-sinks.d.ts +1 -5
- package/dist/core/runner/run-brain-sinks.js +2 -4
- package/dist/core/runner/run-harness-handlers.js +5 -4
- package/dist/core/runner/run-identity-wiring.d.ts +4 -4
- package/dist/core/runner/run-identity-wiring.js +10 -1
- package/dist/core/runner/run-leg.d.ts +1 -1
- package/dist/core/runner/run-leg.js +22 -9
- package/dist/core/runner/run-settle-and-teardown.d.ts +0 -4
- package/dist/core/runner/run-settle-and-teardown.js +7 -4
- package/dist/core/runner/run-terminal-adoption.d.ts +1 -1
- package/dist/core/runner/run-terminal-adoption.js +1 -1
- package/dist/core/runner/run-turn-boundary.js +2 -2
- package/dist/core/runner/runtask.js +8 -8
- package/dist/core/runner/session-file-state-replay.d.ts +14 -4
- package/dist/core/runner/session-file-state-replay.js +8 -0
- package/dist/core/runner/stream-steer-verb.js +3 -0
- package/dist/core/runner/tool-end-body.d.ts +5 -4
- package/dist/core/runner/tool-end-body.js +4 -4
- package/dist/core/runner/tool-output-projection.d.ts +6 -4
- package/dist/core/runner/tool-output-projection.js +66 -47
- package/dist/core/runner-deps.d.ts +6 -3
- package/dist/core/shared-memory/types.d.ts +2 -2
- package/dist/core/shell-read-silence.d.ts +23 -0
- package/dist/core/shell-read-silence.js +18 -0
- package/dist/core/synthetic-read-verdict.d.ts +80 -21
- package/dist/core/synthetic-read-verdict.js +89 -18
- package/dist/core/task-event.d.ts +113 -25
- package/dist/core/task-spec.d.ts +44 -0
- package/dist/core/task-tool-shape.d.ts +1 -1
- package/dist/core/tool-catalog-entries.js +16 -2
- package/dist/core/tool-policy.d.ts +25 -15
- package/dist/core/tool-policy.js +9 -13
- package/dist/core/trace.d.ts +3 -2
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/core/untrusted-text.d.ts +2 -2
- package/dist/core/user-attachment.d.ts +164 -0
- package/dist/core/user-attachment.js +437 -0
- package/dist/engine/compaction/compaction.js +10 -3
- package/dist/engine/compaction/utils.js +6 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +19 -1
- package/dist/engine/execution-env/node-execution-env.js +125 -0
- package/dist/engine/harness/agent-harness.d.ts +12 -2
- package/dist/engine/harness/agent-harness.js +15 -3
- package/dist/engine/harness/types.d.ts +21 -0
- package/dist/engine/llm/content-block-render.d.ts +15 -0
- package/dist/engine/llm/content-block-render.js +10 -0
- package/dist/engine/llm/index.d.ts +1 -0
- package/dist/engine/llm/index.js +1 -0
- package/dist/engine/llm/types.d.ts +80 -4
- package/dist/engine/llm/types.js +43 -1
- package/dist/engine/session/import-validate.js +2 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +2 -1
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/internal/llm.d.ts +2 -2
- package/dist/internal/llm.js +1 -1
- package/dist/orchestration/workflow.d.ts +4 -5
- package/dist/orchestration/workflow.js +2 -2
- package/dist/prompts/default.d.ts +1 -1
- package/dist/tools/artifact/artifact-tool.js +1 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
- package/dist/tools/fs/dir-entries.d.ts +4 -10
- package/dist/tools/fs/dir-entries.js +4 -11
- package/dist/tools/fs/file-read-check.js +3 -3
- package/dist/tools/fs/fs-pdf.js +2 -4
- package/dist/tools/fs/fs-read.d.ts +47 -4
- package/dist/tools/fs/fs-read.js +63 -19
- package/dist/tools/fs/fs-search-tools.d.ts +3 -3
- package/dist/tools/fs/fs-search-tools.js +7 -2
- package/dist/tools/fs/fs-write.d.ts +1 -1
- package/dist/tools/fs/fs-write.js +7 -6
- package/dist/tools/fs/index.d.ts +7 -1
- package/dist/tools/fs/index.js +56 -34
- package/dist/tools/fs/read-deny-home.d.ts +19 -1
- package/dist/tools/fs/read-deny-home.js +32 -22
- package/dist/tools/fs/read-deny.d.ts +14 -0
- package/dist/tools/fs/read-deny.js +8 -0
- package/dist/tools/fs/repo-map.d.ts +3 -2
- package/dist/tools/fs/repo-map.js +11 -3
- package/dist/tools/fs/safety.d.ts +41 -2
- package/dist/tools/fs/safety.js +12 -3
- package/dist/tools/fs/shell-read-check.d.ts +12 -3
- package/dist/tools/fs/shell-read-check.js +44 -24
- package/dist/tools/fs/shell-read-device.d.ts +1 -1
- package/dist/tools/fs/shell-read-face.d.ts +15 -0
- package/dist/tools/fs/shell-read-face.js +5 -0
- package/dist/tools/fs/shell-read-judge.d.ts +9 -4
- package/dist/tools/fs/shell-read-judge.js +1 -1
- package/dist/tools/loop-tick.d.ts +2 -2
- package/package.json +3 -1
- package/test/export-surface.snapshot.json +39 -1
- package/wire-changes/core@7.50.0.json +1699 -0
- package/wire-changes/core@7.50.1.json +65 -0
- package/dist/core/runner/persisted-path-rows.d.ts +0 -88
- package/dist/core/runner/persisted-path-rows.js +0 -108
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `tool_end` output/structured projection, verbatim from
|
|
3
3
|
* `runtask.ts`'s runLocked R4 phase. Pure functions over the harness `AgentToolResult`: zero closure
|
|
4
|
-
* state, zero runtask coupling — the
|
|
5
|
-
*
|
|
4
|
+
* state, zero runtask coupling — the module-level inputs are the text cap below and the shared media budget default
|
|
5
|
+
* (`mediaBudgetBytes`); the leg hands in its own `mediaByteCapBytes`.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
type ToolOutput = {
|
|
8
8
|
output: unknown;
|
|
9
9
|
truncated: boolean;
|
|
10
10
|
totalChars?: number;
|
|
11
|
-
}
|
|
11
|
+
};
|
|
12
|
+
export declare const toolOutputFrom: (result: unknown, mediaByteCapBytes: number | undefined) => ToolOutput | undefined;
|
|
12
13
|
/** The `tool_end.structured` projection over a harness tool result: the CC card when `details.type` names a card
|
|
13
14
|
* the leg admits, else undefined. Minted per leg by {@link createStructuredProjector}; every body projection takes it
|
|
14
15
|
* as a parameter — there is no module-level card set to fall back to. */
|
|
@@ -23,3 +24,4 @@ export declare function createStructuredProjector(opts?: {
|
|
|
23
24
|
declaredCards?: Iterable<string>;
|
|
24
25
|
onUndeclared?: (type: string) => void;
|
|
25
26
|
}): StructuredProjector;
|
|
27
|
+
export {};
|
|
@@ -1,62 +1,81 @@
|
|
|
1
|
+
import { isTextBlock, renderContentBlock } from "../../internal/llm.js";
|
|
2
|
+
import { imageBytes, mediaBudgetBytes } from "../media-byte-cap.js";
|
|
1
3
|
import { engineCardTypes } from "../tool-registry.js";
|
|
2
4
|
import { readMember } from "../thrown-value.js";
|
|
3
5
|
const MAX_TOOL_END_OUTPUT_CHARS = 16384;
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
const marker = (n) => `…[+${n} chars truncated]`;
|
|
7
|
+
const isMediaBlock = (b) => {
|
|
8
|
+
const block = b;
|
|
9
|
+
return (block?.type === "image" || block?.type === "document") && typeof block.data === "string";
|
|
10
|
+
};
|
|
11
|
+
const ownChars = (b) => isTextBlock(b) ? b.text.length + (JSON.stringify({ ...b, type: undefined, text: undefined })?.length ?? 2) - 2 : isMediaBlock(b) ? b.data.length : (JSON.stringify(b)?.length ?? 0);
|
|
12
|
+
const boundTextLane = (blocks, lane) => {
|
|
13
|
+
const sized = lane.map((i) => ({ i, size: ownChars(blocks[i]) }));
|
|
14
|
+
const stream = sized.reduce((n, { size }, k) => n + size + (k > 0 ? 1 : 0), 0);
|
|
15
|
+
const forms = new Map();
|
|
16
|
+
let used = 0;
|
|
17
|
+
for (const [k, { i, size }] of sized.entries()) {
|
|
18
|
+
const sep = k > 0 ? 1 : 0;
|
|
19
|
+
if (used + sep + size <= MAX_TOOL_END_OUTPUT_CHARS) {
|
|
20
|
+
used += sep + size;
|
|
21
|
+
forms.set(i, blocks[i]);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const shown = renderContentBlock(blocks[i]).slice(0, Math.max(0, MAX_TOOL_END_OUTPUT_CHARS - used - sep));
|
|
25
|
+
const notShown = stream - Math.min(MAX_TOOL_END_OUTPUT_CHARS, used + sep) - (isTextBlock(blocks[i]) ? shown.length : 0);
|
|
26
|
+
forms.set(i, { type: "text", text: shown + marker(notShown) });
|
|
27
|
+
return { forms, cut: true };
|
|
7
28
|
}
|
|
8
|
-
|
|
9
|
-
|
|
29
|
+
return { forms, cut: false };
|
|
30
|
+
};
|
|
31
|
+
const wireBytes = (b) => Buffer.byteLength(JSON.stringify(b)) + 1;
|
|
32
|
+
const SUMMARY_RESERVE = wireBytes({ type: "omitted", blocks: Number.MAX_SAFE_INTEGER, bytes: Number.MAX_SAFE_INTEGER, truncated: true }) + 2;
|
|
33
|
+
const fitMedia = (media, budget) => {
|
|
34
|
+
let used = 0;
|
|
35
|
+
let degraded = false;
|
|
36
|
+
const blocks = [];
|
|
37
|
+
for (const [i, m] of media.entries()) {
|
|
38
|
+
const cost = used + m.data.length >= budget ? Infinity : wireBytes(m);
|
|
39
|
+
const kept = used + cost <= budget;
|
|
40
|
+
const out = kept ? m : { type: m.type, ...(typeof m.mimeType === "string" && m.mimeType.length <= 255 ? { mimeType: m.mimeType } : {}), bytes: imageBytes(m), truncated: true };
|
|
41
|
+
const outCost = kept ? cost : wireBytes(out);
|
|
42
|
+
degraded ||= !kept;
|
|
43
|
+
if (used + outCost > budget) {
|
|
44
|
+
const rest = media.slice(i);
|
|
45
|
+
blocks.push({ type: "omitted", blocks: rest.length, bytes: rest.reduce((n, r) => n + imageBytes(r), 0), truncated: true });
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
used += outCost;
|
|
49
|
+
blocks.push(out);
|
|
10
50
|
}
|
|
51
|
+
return { blocks, degraded };
|
|
11
52
|
};
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export const toolOutputFrom = (result) => {
|
|
17
|
-
const content = result !== null && typeof result === "object" ? result.content : result;
|
|
18
|
-
if (content === undefined)
|
|
19
|
-
return undefined;
|
|
20
|
-
if (typeof content === "string") {
|
|
21
|
-
if (content.length <= MAX_TOOL_END_OUTPUT_CHARS)
|
|
53
|
+
const project = (content, mediaByteCapBytes) => {
|
|
54
|
+
if (!Array.isArray(content)) {
|
|
55
|
+
const s = typeof content === "string" ? content : (JSON.stringify(content) ?? "");
|
|
56
|
+
if (s.length <= MAX_TOOL_END_OUTPUT_CHARS)
|
|
22
57
|
return { output: content, truncated: false };
|
|
23
|
-
|
|
24
|
-
return { output: t.text, truncated: true, totalChars: t.totalChars };
|
|
58
|
+
return { output: s.slice(0, MAX_TOOL_END_OUTPUT_CHARS) + marker(s.length - MAX_TOOL_END_OUTPUT_CHARS), truncated: true, totalChars: s.length };
|
|
25
59
|
}
|
|
26
|
-
|
|
60
|
+
const media = content.map(isMediaBlock);
|
|
61
|
+
const text = boundTextLane(content, content.flatMap((_, i) => (media[i] ? [] : [i])));
|
|
62
|
+
const limit = mediaBudgetBytes(mediaByteCapBytes);
|
|
63
|
+
const textBytes = [...text.forms.values()].reduce((n, f) => n + wireBytes(f), 0);
|
|
64
|
+
const fit = fitMedia(content.filter((_, i) => media[i]), limit === Infinity ? Infinity : Math.ceil((limit * 4) / 3) - SUMMARY_RESERVE - textBytes);
|
|
65
|
+
if (!text.cut && !fit.degraded)
|
|
66
|
+
return { output: content, truncated: false };
|
|
67
|
+
let next = 0;
|
|
68
|
+
const output = content.flatMap((_, i) => (media[i] ? (fit.blocks[next++] ?? []) : text.forms.has(i) ? [text.forms.get(i)] : []));
|
|
69
|
+
return { output, truncated: true, totalChars: content.reduce((n, b) => n + ownChars(b), 0) };
|
|
70
|
+
};
|
|
71
|
+
export const toolOutputFrom = (result, mediaByteCapBytes) => {
|
|
27
72
|
try {
|
|
28
|
-
|
|
73
|
+
const content = result !== null && typeof result === "object" ? result.content : result;
|
|
74
|
+
return content === undefined ? undefined : project(content, mediaByteCapBytes);
|
|
29
75
|
}
|
|
30
76
|
catch {
|
|
31
|
-
return { output: "[unserializable tool output]", truncated:
|
|
32
|
-
}
|
|
33
|
-
if (serialized.length <= MAX_TOOL_END_OUTPUT_CHARS)
|
|
34
|
-
return { output: content, truncated: false };
|
|
35
|
-
let anyNonText = false;
|
|
36
|
-
let trueChars = 0;
|
|
37
|
-
const raw = Array.isArray(content)
|
|
38
|
-
? content
|
|
39
|
-
.map((b) => {
|
|
40
|
-
const block = b;
|
|
41
|
-
if (block?.type === "text" && typeof block.text === "string") {
|
|
42
|
-
trueChars += block.text.length;
|
|
43
|
-
return block.text;
|
|
44
|
-
}
|
|
45
|
-
anyNonText = true;
|
|
46
|
-
trueChars += typeof block?.data === "string" ? block.data.length : (safeJsonLength(block) ?? 0);
|
|
47
|
-
return `[${typeof block?.type === "string" ? block.type : "block"}${typeof block?.mimeType === "string" ? `: ${block.mimeType}` : ""}]`;
|
|
48
|
-
})
|
|
49
|
-
.join("\n")
|
|
50
|
-
: serialized;
|
|
51
|
-
const totalChars = Array.isArray(content) ? trueChars : serialized.length;
|
|
52
|
-
if (raw.length > MAX_TOOL_END_OUTPUT_CHARS) {
|
|
53
|
-
const t = trunc(raw);
|
|
54
|
-
return { output: t.text, truncated: true, totalChars };
|
|
55
|
-
}
|
|
56
|
-
if (!anyNonText) {
|
|
57
|
-
return { output: raw, truncated: false };
|
|
77
|
+
return { output: "[unserializable tool output]", truncated: true };
|
|
58
78
|
}
|
|
59
|
-
return { output: raw, truncated: true, totalChars };
|
|
60
79
|
};
|
|
61
80
|
const CC_DETAIL_TYPES = engineCardTypes();
|
|
62
81
|
export function createStructuredProjector(opts = {}) {
|
|
@@ -852,8 +852,9 @@ export interface RunnerDeps {
|
|
|
852
852
|
* family this seat complements, not replaces — BCE stays the fleet-row lane with registry `a*`
|
|
853
853
|
* handles and ticks; this seat is the leg-identity lane), and workflow-spawned agents. Frames are
|
|
854
854
|
* minted at the child leg's own runner chokepoint and carry the #281 件A identity envelope plus a
|
|
855
|
-
* terminal status summary
|
|
856
|
-
* per
|
|
855
|
+
* terminal status summary; the `spawn` phase is the projection of the child's `task_started` frame
|
|
856
|
+
* (once per cycle), the `terminal` phase is per leg — see {@link DelegationLifecycleEvent} for the
|
|
857
|
+
* emission points and the recorded honest absences. Wire it once at Runner construction; a
|
|
857
858
|
* deployment needs NO RunInternals access to observe delegation any more. Observation only — a
|
|
858
859
|
* throwing or rejecting observer is contained ({@link deliverDelegationLifecycle}) and never
|
|
859
860
|
* faults the child run; a PRESENT non-function value here is announced once per process and the
|
|
@@ -1446,7 +1447,9 @@ export interface RunnerDeps {
|
|
|
1446
1447
|
* CC 2.1.196 `tengu_media_byte_cap` parity). Image reads (`Read` → ImageContent) are per-image-capped but
|
|
1447
1448
|
* accumulate across turns; this bounds the SUM in the request by stripping the OLDEST images (replaced with a
|
|
1448
1449
|
* marker) — request-only, non-destructive (the durable session keeps the image; the model can re-Read it).
|
|
1449
|
-
*
|
|
1450
|
+
* The same budget bounds the media of one `tool_end.output` (spent there as base64 on the wire, x 4/3); both readers
|
|
1451
|
+
* read it through `mediaBudgetBytes`. Default {@link AGGREGATE_MEDIA_BUDGET_BYTES}; set `Infinity`/`0` to disable. A
|
|
1452
|
+
* non-number, `NaN` or a negative is refused at `new Runner` (`config.invalid_argument`), never read as "no budget".
|
|
1450
1453
|
*/
|
|
1451
1454
|
mediaByteCapBytes?: number;
|
|
1452
1455
|
/** Optional telemetry fired when {@link mediaByteCapBytes} strips media in a request (CC's
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* design/177 — the SHARED memory-store seam (connected team/deployment memory libraries the model may
|
|
3
3
|
* BROWSE and READ through two tools) and its model-facing constants.
|
|
4
4
|
*
|
|
5
|
-
* Upstream anchor: CC 2.1.223's `memory_list` / `memory_read` pair
|
|
6
|
-
*
|
|
5
|
+
* Upstream anchor: CC 2.1.223's `memory_list` / `memory_read` pair, whose defining property was READ/WRITE
|
|
6
|
+
* ASYMMETRY (no model-side write channel); CC no longer has that shape — 2.1.296 also mounts `yd` (cli296.js:242348) "memory_write" — and sema keeps the 223 one (KL-878): write governance is an
|
|
7
7
|
* environment concern, and the tool face's entire participation in it is truthfully reporting each
|
|
8
8
|
* store's `writable` bit. sema keeps that structure and pushes the whole credential/consent half onto
|
|
9
9
|
* the HOST (BYOM: core holds no identity face), so this module's seam is a single pull-shaped snapshot
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ShellReadVerdict } from "../tools/fs/shell-read-check.js";
|
|
2
|
+
import type { GatePass } from "./runner/gate-exit.js";
|
|
3
|
+
import type { AskGuard } from "./tool-policy.js";
|
|
4
|
+
/** Whose silence a station's answer carries, each its own class on the question it keeps or mints: unread rows are a rule that could
|
|
5
|
+
* not be read (the rule lane's class); the finding `unanswered` is, unless rows are unread, the environment's silence — shell text the
|
|
6
|
+
* engine cannot read (a guard) — and the marks only pick whose. Either is a question only a person answers; `probe` follows the finding. */
|
|
7
|
+
export declare function silencedBy(verdict: ShellReadVerdict, d: {
|
|
8
|
+
readonly guards?: readonly AskGuard[];
|
|
9
|
+
readonly ruleUnreadable?: unknown;
|
|
10
|
+
}): {
|
|
11
|
+
ruleUnreadable?: "persisted_rows";
|
|
12
|
+
guards?: readonly AskGuard[];
|
|
13
|
+
requiresRealApproval?: true;
|
|
14
|
+
};
|
|
15
|
+
/** The finding `unanswered` IS the silence, applied once, before any disposition arm: a station — a host-mounted one included — may
|
|
16
|
+
* spell `vouch` or `mandated` beside it, and neither word vouches for what nobody judged. A deny stands; an ask keeps its words and
|
|
17
|
+
* gains the silence's class; an allow becomes the silence's question (`ask.message`), one only a person answers. Nothing is admitted,
|
|
18
|
+
* no `probe` is stamped and no grant is offered — the station made no finding. */
|
|
19
|
+
export declare function applyUnansweredSilence(pass: GatePass, verdict: ShellReadVerdict, ask: {
|
|
20
|
+
readonly message: string;
|
|
21
|
+
readonly account: string | undefined;
|
|
22
|
+
readonly tightenedBy: "shell_gate" | "safety";
|
|
23
|
+
}): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function silencedBy(verdict, d) {
|
|
2
|
+
const environment = verdict.environmentSilent === true || (verdict.finding === "unanswered" && verdict.rowsUnread !== true);
|
|
3
|
+
const rows = verdict.rowsUnread === true && d.ruleUnreadable === undefined ? { ruleUnreadable: "persisted_rows" } : {};
|
|
4
|
+
const guards = environment && d.guards?.includes("unverifiable_shell_text") !== true ? { guards: [...(d.guards ?? []), "unverifiable_shell_text"] } : {};
|
|
5
|
+
return environment || verdict.rowsUnread === true ? { ...rows, ...guards, requiresRealApproval: true } : {};
|
|
6
|
+
}
|
|
7
|
+
export function applyUnansweredSilence(pass, verdict, ask) {
|
|
8
|
+
if (verdict.details !== undefined)
|
|
9
|
+
pass.shellReadDetails = verdict.details;
|
|
10
|
+
if (pass.decision.action === "ask") {
|
|
11
|
+
pass.decision = { ...pass.decision, ...silencedBy(verdict, pass.decision) };
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (pass.decision.action !== "allow")
|
|
15
|
+
return;
|
|
16
|
+
pass.decision = { action: "ask", message: ask.message, decisionReason: "safety", ...silencedBy(verdict, {}), ...(ask.account !== undefined ? { probeReason: ask.account } : {}) };
|
|
17
|
+
pass.tightenedBy = ask.tightenedBy;
|
|
18
|
+
}
|
|
@@ -1,24 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The synthetic-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The synthetic-call judge — "would this call be refused outright?", asked without a model call, without an
|
|
3
|
+
* approver and without a hook. One judge, one set of layers, for every consumer that needs the answer for a file the
|
|
4
|
+
* model did not itself ask to touch. It has TWO FACES over one body (the layer walk below is written once):
|
|
5
|
+
*
|
|
6
|
+
* READ — a `Read` of a file the model did not ask for (the changed-file notice; a user's attachment). An ordinary
|
|
7
|
+
* `ask` — a person's own ask rule — is not a refusal: the person who would answer it is the one asking.
|
|
8
|
+
* WRITE — a `Write` of a path the engine itself writes in the background (the memory consolidation run). Every `ask`
|
|
9
|
+
* is an objection here: nobody is present to answer it, so a layer that would ask a person refuses the write.
|
|
10
|
+
*
|
|
11
|
+
* It never picks a direction. The read face answers in FOUR states:
|
|
6
12
|
*
|
|
7
13
|
* deny — some layer refuses it;
|
|
8
|
-
*
|
|
9
|
-
*
|
|
14
|
+
* mandated_ask — no layer refuses it, and some layer would put the read to a person no rule may answer for
|
|
15
|
+
* (the ask's own mandate reading, {@link ruleClearanceOf}: a real-approval demand, the deployment's
|
|
16
|
+
* own question, an ancestor's, a hook's; or an organization rule's ask) — the person who must
|
|
17
|
+
* answer is not necessarily the one asking;
|
|
18
|
+
* no_objection — every layer it consulted answered and none objected;
|
|
19
|
+
* unanswerable — no layer objected, and at least one could not answer (see the table below).
|
|
20
|
+
*
|
|
21
|
+
* The write face answers the same four, with `ask` in place of `mandated_ask`: any layer's ask, mandated or not.
|
|
10
22
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
23
|
+
* ONE PRECEDENCE on both faces: a deny anywhere wins over a held ask, and a held ask over an unanswerable layer; the walk
|
|
24
|
+
* goes on past an ask (a later layer's deny is the stronger answer). The faces differ only in which asks are held. The
|
|
25
|
+
* CONSUMER decides where `mandated_ask` and `unanswerable` go: a notice that merely tells the model a file changed reads
|
|
26
|
+
* both as no objection (the worst case is a notice about a file it then cannot re-read); a face that would put a file's
|
|
27
|
+
* contents in front of the model reads both as a refusal; a background write reads everything but `no_objection` as a
|
|
28
|
+
* refusal. Every answer but `unanswerable` names the location the layers judged (`judged`, the canonical view of the
|
|
29
|
+
* stamped target, when a stamp was taken), so a consumer that resolved the file itself can tell the judged file from the
|
|
30
|
+
* one it opens.
|
|
14
31
|
*
|
|
15
|
-
* THE LAYERS (every one that can refuse a model's own
|
|
32
|
+
* THE LAYERS (every one that can refuse a model's own call): the run's skill deny rows; the
|
|
16
33
|
* deny-narrowing fold (session rules, inherited ancestor rules, frozen constraint projections); the
|
|
17
34
|
* deployment's caller policy; each live inherited parent policy; the person's persisted-rule lane; the org
|
|
18
|
-
* lane. Not consulted, by design: PreToolUse hooks (the deployment's screening of a model's CALL), the
|
|
19
|
-
*
|
|
20
|
-
* and containment roots (the path's own resolution judges those
|
|
21
|
-
* anywhere wins over an unanswerable layer elsewhere.
|
|
35
|
+
* lane. Not consulted, by design: PreToolUse hooks (the deployment's screening of a model's CALL), the permission mode's write
|
|
36
|
+
* gate, the integrity guards and the write-protection table (a live `Write` meets them; the read face never needs them and the
|
|
37
|
+
* write face's background run is not asked), and the read-deny table and containment roots (the path's own resolution judges those first).
|
|
22
38
|
*
|
|
23
39
|
* FAULT → STATE (the live gate's own readings, through the same functions — never a second copy):
|
|
24
40
|
* | layer | fault | state |
|
|
@@ -28,11 +44,13 @@
|
|
|
28
44
|
* | caller / inherited policy | its deny carries an engine settlement (an approval | unanswerable `approval_seat` |
|
|
29
45
|
* | | seat that ended unanswered; in a combinePolicies | |
|
|
30
46
|
* | | composition a rule deny anywhere is returned instead)| |
|
|
31
|
-
* | persisted lane | store unreadable, throws, times out (readPersistedRuleLane ⇒ `{unreadable}`) |
|
|
47
|
+
* | persisted lane | a store partition (or all of it) is unreadable, the lane throws, times out (readPersistedRuleLane ⇒ `{unreadable}`) | unanswerable `store_unreadable` (both faces: a row nobody could read may forbid the call) |
|
|
32
48
|
* | persisted lane | the call cannot be read against the rows (`reason`) — applyPersistedTightening ⇒ `unreadable` | unanswerable `call_unreadable` |
|
|
33
49
|
* | org lane | unavailable: throws, garbage (readOrgRuleLane) | unanswerable `org_unavailable` |
|
|
50
|
+
* | any policy layer | answers with a rewrite of the arguments (`updatedInput`) | unanswerable `rewritten` |
|
|
51
|
+
* | inherited parent policy | answers `ask` (the ancestor's approver's question) | mandated_ask (write: ask) |
|
|
34
52
|
* | any layer | the judgment's budget (one dependency timeout, or a | unanswerable `timed_out` |
|
|
35
|
-
* | | batch's shared deadline) is spent |
|
|
53
|
+
* | | batch's shared deadline) is spent | |
|
|
36
54
|
*
|
|
37
55
|
* NEVER AN APPROVER. A deployment policy (the caller's, an inherited parent's) is consulted with an
|
|
38
56
|
* ALREADY-ABORTED signal and no approver seat: the `ToolPolicy.check` contract is that an aborted signal
|
|
@@ -45,32 +63,50 @@
|
|
|
45
63
|
* the notice face stays silent. The engine's own layers (narrowing, skill rows, the two lanes) get the
|
|
46
64
|
* run's real signal: they are pure matchers and store reads.
|
|
47
65
|
*
|
|
48
|
-
* THE PROBE'S IDENTITY. Every layer sees `
|
|
49
|
-
* call id `<face>-visibility-probe`
|
|
50
|
-
*
|
|
66
|
+
* THE PROBE'S IDENTITY. Every layer sees the probed tool's name (`Read` / `Write`), the probed arguments, and a stable
|
|
67
|
+
* synthetic call id (`<face>-visibility-probe` for a read, `<face>-write-probe` for a write; {@link syntheticReadProbeId},
|
|
68
|
+
* {@link syntheticWriteProbeId}), so a deployment's audit can tell a probe from a model's call. The write face stamps the
|
|
69
|
+
* facts the live gate stamps on a write (the roster's `Write` face, the tracked cwd and root, the advertised writable
|
|
70
|
+
* directories, the data root's exemption): a containment fence and a data-root exemption judge the probe as they judge the call.
|
|
51
71
|
*/
|
|
52
72
|
import type { ToolCallRequest, ToolCallFace, ToolPolicy } from "./tool-policy.js";
|
|
53
|
-
import type
|
|
73
|
+
import { type CallTargetStamp } from "./effective-path-target.js";
|
|
54
74
|
import { type SkillGrantLane } from "./skill-grant.js";
|
|
55
75
|
import type { OrgGateVerdict, PersistedRuleAnswer } from "./hooks.js";
|
|
56
76
|
/** The layers the judge consults, as named in its verdicts. */
|
|
57
77
|
export declare const SYNTHETIC_READ_LAYERS: readonly ["skill_rows", "narrowing", "caller_policy", "inherited_constraint", "persisted_rules", "org_rules"];
|
|
58
78
|
export type SyntheticReadLayer = (typeof SYNTHETIC_READ_LAYERS)[number];
|
|
59
79
|
/** Why a layer could not answer — the consumer's disclosure word. */
|
|
60
|
-
export declare const SYNTHETIC_READ_UNANSWERABLE_CAUSES: readonly ["threw", "timed_out", "approval_seat", "call_unreadable", "org_unavailable"];
|
|
80
|
+
export declare const SYNTHETIC_READ_UNANSWERABLE_CAUSES: readonly ["threw", "timed_out", "approval_seat", "call_unreadable", "org_unavailable", "rewritten", "store_unreadable"];
|
|
61
81
|
export type SyntheticReadUnanswerableCause = (typeof SYNTHETIC_READ_UNANSWERABLE_CAUSES)[number];
|
|
62
82
|
export type SyntheticReadVerdict = {
|
|
63
83
|
verdict: "deny";
|
|
64
84
|
layer: SyntheticReadLayer;
|
|
65
85
|
message?: string;
|
|
86
|
+
judged?: string;
|
|
87
|
+
} | {
|
|
88
|
+
verdict: "mandated_ask";
|
|
89
|
+
layer: SyntheticReadLayer;
|
|
90
|
+
message?: string;
|
|
91
|
+
judged?: string;
|
|
66
92
|
} | {
|
|
67
93
|
verdict: "no_objection";
|
|
94
|
+
judged?: string;
|
|
68
95
|
} | {
|
|
69
96
|
verdict: "unanswerable";
|
|
70
97
|
layer: SyntheticReadLayer;
|
|
71
98
|
cause: SyntheticReadUnanswerableCause;
|
|
72
99
|
reason: string;
|
|
73
100
|
};
|
|
101
|
+
/** The write face's answer: the read face's states with `ask` — any layer's ask, mandated or not — in place of `mandated_ask`. */
|
|
102
|
+
export type SyntheticWriteVerdict = Exclude<SyntheticReadVerdict, {
|
|
103
|
+
verdict: "mandated_ask";
|
|
104
|
+
}> | {
|
|
105
|
+
verdict: "ask";
|
|
106
|
+
layer: SyntheticReadLayer;
|
|
107
|
+
message?: string;
|
|
108
|
+
judged?: string;
|
|
109
|
+
};
|
|
74
110
|
export interface SyntheticReadLayers {
|
|
75
111
|
skillGrants?: Pick<SkillGrantLane, "denying">;
|
|
76
112
|
denyNarrowingPolicy?: ToolPolicy;
|
|
@@ -97,9 +133,27 @@ export interface SyntheticReadCall {
|
|
|
97
133
|
/** The live roster's face for `Read`, when the consumer has one. */
|
|
98
134
|
face?: ToolCallFace;
|
|
99
135
|
}
|
|
136
|
+
/** The path a synthetic `Write` probes. The request's other facts come from the judge's {@link SyntheticWriteFacts}. */
|
|
137
|
+
export interface SyntheticWriteCall {
|
|
138
|
+
file_path: string;
|
|
139
|
+
}
|
|
140
|
+
/** The facts the live gate stamps on a write, read at the moment of each judgment (the leg's cells move: the tracked cwd,
|
|
141
|
+
* the advertised-directory ledger). Every member is optional — a leg without hands has no roster face, cwd or root. */
|
|
142
|
+
export interface SyntheticWriteFacts {
|
|
143
|
+
/** The live roster's face for `Write` (a containment fence admits a write only by the tool's own declared path target). */
|
|
144
|
+
face?: ToolCallFace | undefined;
|
|
145
|
+
cwd?: string | undefined;
|
|
146
|
+
root?: string | undefined;
|
|
147
|
+
advertisedWritableDirs?: {
|
|
148
|
+
readonly roots: readonly string[];
|
|
149
|
+
} | undefined;
|
|
150
|
+
exemptRoots?: readonly string[] | undefined;
|
|
151
|
+
}
|
|
100
152
|
/** The stable synthetic call id a probe carries: `<face>-visibility-probe`, `face` a lowercase word
|
|
101
153
|
* (letters and hyphens) naming the consumer. */
|
|
102
154
|
export declare function syntheticReadProbeId(face: string): string;
|
|
155
|
+
/** The write face's stable synthetic call id: `<face>-write-probe`, `face` the same lowercase word. */
|
|
156
|
+
export declare function syntheticWriteProbeId(face: string): string;
|
|
103
157
|
/** One judgment's wait budget. `deadlineAt` (epoch ms) bounds the WHOLE judgment — every layer waits at most
|
|
104
158
|
* what is left of it, and a layer reached after it has passed is not consulted (`timed_out`). Absent ⇒ the
|
|
105
159
|
* dependency timeout from the start of this judgment. A consumer judging a batch passes one shared deadline,
|
|
@@ -108,4 +162,9 @@ export interface SyntheticReadBudget {
|
|
|
108
162
|
deadlineAt?: number;
|
|
109
163
|
}
|
|
110
164
|
export type SyntheticReadJudge = (call: SyntheticReadCall, face: string, budget?: SyntheticReadBudget) => Promise<SyntheticReadVerdict>;
|
|
165
|
+
export type SyntheticWriteJudge = (call: SyntheticWriteCall, face: string, budget?: SyntheticReadBudget) => Promise<SyntheticWriteVerdict>;
|
|
166
|
+
/** The READ face: only a mandated ask is held (a person's own ask is that person's question). */
|
|
111
167
|
export declare function createSyntheticReadJudge(layers: SyntheticReadLayers): SyntheticReadJudge;
|
|
168
|
+
/** The WRITE face: every ask is held as an objection (no approver is present), and a persisted store that cannot be read is
|
|
169
|
+
* unanswerable rather than silent. */
|
|
170
|
+
export declare function createSyntheticWriteJudge(layers: SyntheticReadLayers, facts: () => SyntheticWriteFacts): SyntheticWriteJudge;
|
|
@@ -1,20 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { callTargetReadingOf } from "./effective-path-target.js";
|
|
2
|
+
import { advertisedWritableRootsSeat, attestationOf, describeThrown, exemptRootsSeat, refuseOutOfContractDecision } from "./tool-policy.js";
|
|
3
|
+
import { ownedClearanceFacts, ruleClearanceOf } from "./ask-origin.js";
|
|
2
4
|
import { skillRowRefusal } from "./skill-grant.js";
|
|
3
5
|
import { applyPersistedTightening } from "./persisted-rule-arms.js";
|
|
4
6
|
import { readOrgRuleLane, readPersistedRuleLane } from "./rule-lane-reads.js";
|
|
5
7
|
import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "./permission-rule-org.js";
|
|
8
|
+
import { deepJsonEqual } from "./runner/decide-continuation.js";
|
|
6
9
|
export const SYNTHETIC_READ_LAYERS = Object.freeze(["skill_rows", "narrowing", "caller_policy", "inherited_constraint", "persisted_rules", "org_rules"]);
|
|
7
|
-
export const SYNTHETIC_READ_UNANSWERABLE_CAUSES = Object.freeze(["threw", "timed_out", "approval_seat", "call_unreadable", "org_unavailable"]);
|
|
10
|
+
export const SYNTHETIC_READ_UNANSWERABLE_CAUSES = Object.freeze(["threw", "timed_out", "approval_seat", "call_unreadable", "org_unavailable", "rewritten", "store_unreadable"]);
|
|
8
11
|
export function syntheticReadProbeId(face) {
|
|
12
|
+
return `${probeFaceWord(face)}-visibility-probe`;
|
|
13
|
+
}
|
|
14
|
+
export function syntheticWriteProbeId(face) {
|
|
15
|
+
return `${probeFaceWord(face)}-write-probe`;
|
|
16
|
+
}
|
|
17
|
+
function probeFaceWord(face) {
|
|
9
18
|
if (!/^[a-z][a-z-]*$/.test(face))
|
|
10
|
-
throw new Error(`
|
|
11
|
-
return
|
|
19
|
+
throw new Error(`synthetic probe id: the probe face must be a lowercase word (letters and hyphens), got ${JSON.stringify(face)}`);
|
|
20
|
+
return face;
|
|
12
21
|
}
|
|
13
22
|
const ABORTED = AbortSignal.abort();
|
|
23
|
+
function sameArguments(value, probed) {
|
|
24
|
+
try {
|
|
25
|
+
return deepJsonEqual(value, probed);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
14
31
|
export function createSyntheticReadJudge(layers) {
|
|
32
|
+
const judge = createProbeJudge(layers, "read");
|
|
15
33
|
return async (call, face, budget) => {
|
|
16
|
-
const deadlineAt = budget?.deadlineAt ?? Date.now() + ORG_ADJUDICATION_TIMEOUT_MS;
|
|
17
|
-
const left = () => Math.max(0, Math.min(ORG_ADJUDICATION_TIMEOUT_MS, deadlineAt - Date.now()));
|
|
18
34
|
const probe = {
|
|
19
35
|
toolName: "Read",
|
|
20
36
|
args: { file_path: call.file_path, ...(call.offset !== undefined ? { offset: call.offset } : {}), ...(call.limit !== undefined ? { limit: call.limit } : {}) },
|
|
@@ -22,32 +38,76 @@ export function createSyntheticReadJudge(layers) {
|
|
|
22
38
|
...(call.cwd !== undefined ? { cwd: call.cwd } : {}),
|
|
23
39
|
...(call.face !== undefined ? { face: call.face } : {}),
|
|
24
40
|
};
|
|
41
|
+
const v = await judge(probe, budget);
|
|
42
|
+
return v.verdict === "ask" ? { ...v, verdict: "mandated_ask" } : v;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function createSyntheticWriteJudge(layers, facts) {
|
|
46
|
+
const judge = createProbeJudge(layers, "write");
|
|
47
|
+
return async (call, face, budget) => {
|
|
48
|
+
const f = facts();
|
|
49
|
+
const probe = {
|
|
50
|
+
toolName: "Write",
|
|
51
|
+
args: { file_path: call.file_path, content: "" },
|
|
52
|
+
toolCallId: syntheticWriteProbeId(face),
|
|
53
|
+
...(f.cwd !== undefined ? { cwd: f.cwd } : {}),
|
|
54
|
+
...(f.root !== undefined ? { root: f.root } : {}),
|
|
55
|
+
...(f.face !== undefined ? { face: f.face } : {}),
|
|
56
|
+
...advertisedWritableRootsSeat(f.advertisedWritableDirs),
|
|
57
|
+
...(f.exemptRoots !== undefined ? exemptRootsSeat(f.exemptRoots) : {}),
|
|
58
|
+
};
|
|
59
|
+
return judge(probe, budget);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function createProbeJudge(layers, kind) {
|
|
63
|
+
const everyAskHeld = kind === "write";
|
|
64
|
+
return async (probe, budget) => {
|
|
65
|
+
const deadlineAt = budget?.deadlineAt ?? Date.now() + ORG_ADJUDICATION_TIMEOUT_MS;
|
|
66
|
+
const left = () => Math.max(0, Math.min(ORG_ADJUDICATION_TIMEOUT_MS, deadlineAt - Date.now()));
|
|
67
|
+
const probedArgs = structuredClone(probe.args);
|
|
25
68
|
const stamped = layers.stampCallTarget?.(probe, layers.signal);
|
|
26
69
|
const req = stamped !== undefined ? await stamped : probe;
|
|
70
|
+
const view = callTargetReadingOf(req)?.protective;
|
|
71
|
+
const judged = view?.kind === "path" && view.canonical !== undefined ? { judged: view.canonical } : {};
|
|
27
72
|
let unanswered;
|
|
73
|
+
let held;
|
|
28
74
|
const cannot = (layer, cause, reason) => {
|
|
29
75
|
unanswered ??= { verdict: "unanswerable", layer, cause, reason };
|
|
30
76
|
};
|
|
77
|
+
const ask = (layer, message, mandated) => {
|
|
78
|
+
if (everyAskHeld || mandated)
|
|
79
|
+
held ??= { verdict: "ask", layer, ...(message !== undefined ? { message } : {}), ...judged };
|
|
80
|
+
};
|
|
31
81
|
const spent = (layer) => {
|
|
32
82
|
if (left() > 0)
|
|
33
83
|
return false;
|
|
34
84
|
cannot(layer, "timed_out", "the judgment's wait budget was spent before this layer was asked");
|
|
35
85
|
return true;
|
|
36
86
|
};
|
|
37
|
-
const policyLayer = async (layer, policy, signal) => {
|
|
87
|
+
const policyLayer = async (layer, policy, signal, ancestorOwned = false) => {
|
|
38
88
|
if (spent(layer))
|
|
39
89
|
return undefined;
|
|
40
90
|
const waitMs = left();
|
|
41
91
|
let timedOut = false;
|
|
92
|
+
let thrown = undefined;
|
|
42
93
|
const TIMEOUT = Symbol("timeout");
|
|
43
94
|
let d;
|
|
44
95
|
try {
|
|
45
|
-
d = await settleOrgVerdictWithin(Promise.resolve()
|
|
96
|
+
d = await settleOrgVerdictWithin(Promise.resolve()
|
|
97
|
+
.then(() => policy.check(req, signal))
|
|
98
|
+
.catch((err) => {
|
|
99
|
+
thrown = { err };
|
|
100
|
+
return TIMEOUT;
|
|
101
|
+
}), TIMEOUT, { ...(layers.signal !== undefined ? { signal: layers.signal } : {}), timeoutMs: waitMs, onFallback: () => void (timedOut = true) });
|
|
46
102
|
}
|
|
47
103
|
catch (err) {
|
|
48
104
|
cannot(layer, "threw", describeThrown(err));
|
|
49
105
|
return undefined;
|
|
50
106
|
}
|
|
107
|
+
if (thrown !== undefined) {
|
|
108
|
+
cannot(layer, "threw", describeThrown(thrown.err));
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
51
111
|
if (d === TIMEOUT || timedOut) {
|
|
52
112
|
cannot(layer, "timed_out", `no answer within ${waitMs}ms (or the run ended first)`);
|
|
53
113
|
return undefined;
|
|
@@ -60,18 +120,25 @@ export function createSyntheticReadJudge(layers) {
|
|
|
60
120
|
cannot(layer, "threw", describeThrown(err));
|
|
61
121
|
return undefined;
|
|
62
122
|
}
|
|
123
|
+
if (read.action !== "deny" && ((read.updatedInput !== undefined && !sameArguments(read.updatedInput, probedArgs)) || !sameArguments(req.args, probedArgs))) {
|
|
124
|
+
cannot(layer, "rewritten", "the layer would rewrite the call's arguments, so its answer is not about this call");
|
|
125
|
+
}
|
|
126
|
+
if (read.action === "ask") {
|
|
127
|
+
ask(layer, read.message, ancestorOwned || ruleClearanceOf(ownedClearanceFacts(read), {}).mandated);
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
63
130
|
if (read.action !== "deny")
|
|
64
131
|
return undefined;
|
|
65
132
|
if (attestationOf(read, req)?.settlement !== undefined) {
|
|
66
|
-
cannot(layer, "approval_seat", "the policy would ask an approver, who is never consulted for a
|
|
133
|
+
cannot(layer, "approval_seat", "the policy would ask an approver, who is never consulted for a synthetic probe");
|
|
67
134
|
return undefined;
|
|
68
135
|
}
|
|
69
|
-
return { verdict: "deny", layer, ...(read.message !== undefined ? { message: read.message } : {}) };
|
|
136
|
+
return { verdict: "deny", layer, ...(read.message !== undefined ? { message: read.message } : {}), ...judged };
|
|
70
137
|
};
|
|
71
138
|
try {
|
|
72
139
|
const refusal = skillRowRefusal(layers.skillGrants, req);
|
|
73
140
|
if (refusal !== undefined)
|
|
74
|
-
return { verdict: "deny", layer: "skill_rows", ...(refusal.message !== undefined ? { message: refusal.message } : {}) };
|
|
141
|
+
return { verdict: "deny", layer: "skill_rows", ...(refusal.message !== undefined ? { message: refusal.message } : {}), ...judged };
|
|
75
142
|
}
|
|
76
143
|
catch (err) {
|
|
77
144
|
cannot("skill_rows", "threw", describeThrown(err));
|
|
@@ -82,14 +149,14 @@ export function createSyntheticReadJudge(layers) {
|
|
|
82
149
|
return v;
|
|
83
150
|
}
|
|
84
151
|
if (layers.callerPolicy !== undefined) {
|
|
85
|
-
const v = await policyLayer("caller_policy", layers.callerPolicy, ABORTED);
|
|
152
|
+
const v = await policyLayer("caller_policy", layers.callerPolicy, ABORTED, (layers.inheritedPolicies ?? []).includes(layers.callerPolicy));
|
|
86
153
|
if (v !== undefined)
|
|
87
154
|
return v;
|
|
88
155
|
}
|
|
89
156
|
for (const parent of layers.inheritedPolicies ?? []) {
|
|
90
157
|
if (parent === layers.callerPolicy)
|
|
91
158
|
continue;
|
|
92
|
-
const v = await policyLayer("inherited_constraint", parent, ABORTED);
|
|
159
|
+
const v = await policyLayer("inherited_constraint", parent, ABORTED, true);
|
|
93
160
|
if (v !== undefined)
|
|
94
161
|
return v;
|
|
95
162
|
}
|
|
@@ -97,17 +164,21 @@ export function createSyntheticReadJudge(layers) {
|
|
|
97
164
|
const read = await readPersistedRuleLane(layers.persistedRuleLane, req, layers.signal, left());
|
|
98
165
|
const tightened = applyPersistedTightening({ action: "allow" }, read).tightened;
|
|
99
166
|
if (tightened === "deny")
|
|
100
|
-
return { verdict: "deny", layer: "persisted_rules" };
|
|
101
|
-
if (tightened === "
|
|
102
|
-
|
|
167
|
+
return { verdict: "deny", layer: "persisted_rules", ...judged };
|
|
168
|
+
if (tightened === "ask")
|
|
169
|
+
ask("persisted_rules", undefined, false);
|
|
170
|
+
if (read.unreadable === true)
|
|
171
|
+
cannot("persisted_rules", read.reason !== undefined ? "call_unreadable" : "store_unreadable", read.reason ?? "a partition of the person's rule store could not be read, so whether a standing rule forbids this call is unknown");
|
|
103
172
|
}
|
|
104
173
|
if (layers.orgRuleLane !== undefined && !spent("org_rules")) {
|
|
105
174
|
const org = await readOrgRuleLane(layers.orgRuleLane, req, layers.signal, left());
|
|
106
175
|
if (org.status === "unavailable")
|
|
107
176
|
cannot("org_rules", left() > 0 ? "org_unavailable" : "timed_out", org.disclosures.join("; ") || "organization governance could not be read");
|
|
108
177
|
else if (org.verdict?.behavior === "deny")
|
|
109
|
-
return { verdict: "deny", layer: "org_rules", message: `an organization policy rule (${org.verdict.rule}) denies it
|
|
178
|
+
return { verdict: "deny", layer: "org_rules", message: `an organization policy rule (${org.verdict.rule}) denies it`, ...judged };
|
|
179
|
+
else if (org.verdict?.behavior === "ask")
|
|
180
|
+
ask("org_rules", `an organization policy rule (${org.verdict.rule}) requires approval for it`, true);
|
|
110
181
|
}
|
|
111
|
-
return unanswered ?? { verdict: "no_objection" };
|
|
182
|
+
return held ?? unanswered ?? { verdict: "no_objection", ...judged };
|
|
112
183
|
};
|
|
113
184
|
}
|