@sema-agent/core 7.50.1 → 7.51.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 +275 -0
- package/dist/agents/child-failure.d.ts +5 -16
- package/dist/agents/child-failure.js +4 -24
- package/dist/agents/subagent-steps.js +2 -2
- package/dist/agents/subagent.d.ts +4 -13
- package/dist/agents/subagent.js +94 -204
- package/dist/agents/tool-filter.d.ts +2 -2
- package/dist/brain/anthropic.d.ts +1 -1
- package/dist/brain/anthropic.js +7 -5
- package/dist/brain/open-responses.d.ts +1 -1
- package/dist/brain/open-responses.js +3 -2
- package/dist/brain/openai.d.ts +1 -1
- package/dist/brain/openai.js +3 -2
- package/dist/brain/request-route.d.ts +2 -2
- package/dist/brain/route-adjudicator.d.ts +16 -7
- package/dist/brain/route-adjudicator.js +23 -7
- package/dist/brain/route-conformance.js +23 -5
- package/dist/brain/stream-engine.js +9 -3
- package/dist/core/a2a.js +6 -8
- package/dist/core/allow-entry.d.ts +45 -26
- package/dist/core/allow-entry.js +48 -24
- package/dist/core/arg-summary.d.ts +8 -85
- package/dist/core/arg-summary.js +4 -386
- package/dist/core/ask-origin.d.ts +35 -33
- package/dist/core/ask-origin.js +9 -11
- package/dist/core/checkpoint-requirements.d.ts +6 -0
- package/dist/core/checkpoint-requirements.js +1 -0
- package/dist/core/checkpoint-store.d.ts +15 -34
- package/dist/core/checkpoint-store.js +5 -4
- package/dist/core/classifier-unavailable-arm.d.ts +12 -8
- package/dist/core/classifier-unavailable-arm.js +7 -2
- package/dist/core/credential-vocabulary.d.ts +3 -2
- package/dist/core/credential-vocabulary.js +27 -18
- package/dist/core/delegation-frames.d.ts +40 -15
- package/dist/core/delegation-frames.js +34 -0
- package/dist/core/engine-notice.d.ts +35 -11
- package/dist/core/gate-fold.js +9 -7
- package/dist/core/gate-lanes.js +6 -4
- package/dist/core/gate-outcome.d.ts +87 -11
- package/dist/core/gate-outcome.js +171 -61
- package/dist/core/governance-codes.d.ts +7 -1
- package/dist/core/governance-codes.js +9 -0
- package/dist/core/hooks.d.ts +4 -4
- package/dist/core/hooks.js +4 -3
- package/dist/core/mcp-connection-pool.d.ts +2 -0
- package/dist/core/mcp-connection-pool.js +2 -1
- package/dist/core/mcp-failure.js +2 -1
- package/dist/core/mcp-server-spec.d.ts +27 -5
- package/dist/core/mcp.d.ts +13 -38
- package/dist/core/mcp.js +28 -23
- package/dist/core/media-byte-cap.d.ts +25 -0
- package/dist/core/media-byte-cap.js +18 -2
- package/dist/core/memory-engine/consolidation-driver.js +1 -1
- package/dist/core/mode-policy.d.ts +25 -1
- package/dist/core/mode-policy.js +28 -13
- package/dist/core/own-property-receipt.d.ts +18 -0
- package/dist/core/own-property-receipt.js +39 -0
- package/dist/core/permission-rule-model.d.ts +32 -11
- package/dist/core/permission-rule-model.js +12 -7
- package/dist/core/persisted-rule-arms.d.ts +3 -3
- package/dist/core/prompt-section-seat.d.ts +54 -0
- package/dist/core/prompt-section-seat.js +49 -0
- package/dist/core/protocol-http-auth.d.ts +44 -0
- package/dist/core/protocol-http-auth.js +45 -0
- package/dist/core/read-face-seats.d.ts +30 -0
- package/dist/core/read-face-seats.js +38 -0
- package/dist/core/redaction-passes.d.ts +82 -0
- package/dist/core/redaction-passes.js +378 -0
- package/dist/core/remote-env.d.ts +4 -3
- package/dist/core/resume-startup-cause.d.ts +56 -0
- package/dist/core/resume-startup-cause.js +43 -0
- package/dist/core/rule-denial-sentence.d.ts +2 -2
- package/dist/core/runner/announce-once-ledger.js +2 -2
- package/dist/core/runner/classifier-ladder.d.ts +17 -13
- package/dist/core/runner/classifier-ladder.js +12 -10
- package/dist/core/runner/construction-doors.js +9 -20
- package/dist/core/runner/contracts.d.ts +4 -4
- package/dist/core/runner/gate-exit.js +1 -1
- package/dist/core/runner/memory-consolidation-write-judge.d.ts +53 -9
- package/dist/core/runner/memory-consolidation-write-judge.js +39 -1
- package/dist/core/runner/permission-rule-lanes.d.ts +8 -13
- package/dist/core/runner/person-path-rows.d.ts +4 -3
- package/dist/core/runner/policy-throw.js +4 -4
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
- package/dist/core/runner/prepare-caps-and-workflow.js +8 -6
- package/dist/core/runner/prepare-gate-stations.js +3 -3
- package/dist/core/runner/prepare-hands-readface.js +1 -19
- package/dist/core/runner/prepare-inherited-gate.js +10 -9
- package/dist/core/runner/prepare-policy-chain.d.ts +3 -5
- package/dist/core/runner/prepare-policy-chain.js +10 -13
- package/dist/core/runner/prepare-project-context.d.ts +4 -4
- package/dist/core/runner/prepare-project-context.js +2 -1
- package/dist/core/runner/prepare-prompt-assembly.js +1 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -1
- package/dist/core/runner/prepare-prompt-inputs.js +4 -4
- package/dist/core/runner/prepare-protocol-tools.d.ts +4 -3
- package/dist/core/runner/prepare-protocol-tools.js +3 -2
- package/dist/core/runner/prepare-task.js +6 -4
- package/dist/core/runner/prepare-turn-wiring.d.ts +16 -13
- package/dist/core/runner/prepare-turn-wiring.js +8 -7
- package/dist/core/runner/run-harness-handlers.js +2 -2
- package/dist/core/runner/run-leg.js +10 -9
- package/dist/core/runner/run-settle-and-teardown.js +5 -5
- package/dist/core/runner/run-turn-boundary.js +3 -2
- package/dist/core/runner/runtask.d.ts +1 -2
- package/dist/core/runner/runtask.js +8 -12
- package/dist/core/runner/session-rule-policy.d.ts +15 -11
- package/dist/core/runner/session-rule-policy.js +43 -33
- package/dist/core/runner/stream-lifecycle-verbs.js +2 -2
- package/dist/core/runner/stream-settle-backstop.js +2 -1
- package/dist/core/runner/sub-agent-deny-lists.d.ts +12 -8
- package/dist/core/runner/sub-agent-deny-lists.js +23 -12
- package/dist/core/runner/terminal-account.js +3 -3
- package/dist/core/runner/tool-output-projection.js +3 -8
- package/dist/core/runner/tool-result-media-seat.d.ts +19 -0
- package/dist/core/runner/tool-result-media-seat.js +13 -0
- package/dist/core/runner/turn-attachments.d.ts +13 -12
- package/dist/core/runner/turn-attachments.js +11 -7
- package/dist/core/runner-deps.d.ts +4 -3
- package/dist/core/safety-merge-corpus.js +21 -21
- package/dist/core/secret-env.js +1 -1
- package/dist/core/session-policy-store.d.ts +89 -72
- package/dist/core/session-policy-store.js +100 -93
- package/dist/core/shell-read-silence.d.ts +38 -4
- package/dist/core/shell-read-silence.js +102 -0
- package/dist/core/side-query.d.ts +7 -8
- package/dist/core/skill-grant-binding.d.ts +32 -11
- package/dist/core/skill-grant-binding.js +11 -3
- package/dist/core/skill-grant.d.ts +4 -3
- package/dist/core/skill-grant.js +5 -4
- package/dist/core/skills-directory.d.ts +12 -1
- package/dist/core/skills-directory.js +17 -8
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +3 -0
- package/dist/core/swappable-deps.d.ts +15 -1
- package/dist/core/swappable-deps.js +25 -1
- package/dist/core/synthetic-read-verdict.d.ts +16 -14
- package/dist/core/synthetic-read-verdict.js +28 -8
- package/dist/core/task-notification.d.ts +13 -3
- package/dist/core/task-notification.js +12 -1
- package/dist/core/task-spec.d.ts +14 -15
- package/dist/core/tool-name-readings.d.ts +0 -4
- package/dist/core/tool-name-readings.js +0 -1
- package/dist/core/tool-policy.d.ts +40 -9
- package/dist/core/tool-policy.js +25 -7
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/core/untrusted-egress.d.ts +60 -11
- package/dist/core/untrusted-egress.js +120 -9
- package/dist/core/untrusted-text.js +16 -0
- package/dist/core/wiring-manifest.d.ts +22 -8
- package/dist/core/wiring-manifest.js +12 -4
- package/dist/engine/harness/agent-harness.d.ts +2 -1
- package/dist/engine/harness/agent-harness.js +4 -1
- package/dist/engine/harness/types.d.ts +3 -0
- package/dist/engine/llm/types.d.ts +7 -9
- package/dist/engine/loop/agent-loop.d.ts +8 -3
- package/dist/engine/loop/agent-loop.js +26 -9
- package/dist/engine/loop/types.d.ts +10 -0
- package/dist/index.d.ts +17 -5
- package/dist/index.js +14 -5
- package/dist/orchestration/run-workflow-tool.js +8 -8
- package/dist/orchestration/workflow.js +2 -2
- package/dist/prompt-assembly/assemble.d.ts +3 -1
- package/dist/prompt-assembly/assemble.js +6 -0
- package/dist/prompts/cowork.d.ts +42 -0
- package/dist/prompts/cowork.js +529 -0
- package/dist/prompts/default.d.ts +14 -1
- package/dist/prompts/default.js +3 -0
- package/dist/stores/file/session-policy-store.d.ts +6 -3
- package/dist/stores/file/session-policy-store.js +5 -16
- package/dist/tools/artifact/artifact-tool.js +2 -0
- package/dist/tools/fs/image-admission.d.ts +57 -4
- package/dist/tools/fs/image-admission.js +85 -0
- package/dist/tools/fs/path-containment.d.ts +2 -0
- package/dist/tools/fs/path-containment.js +5 -0
- package/dist/tools/fs/shell-read-check.d.ts +31 -6
- package/dist/tools/fs/shell-read-check.js +2 -4
- package/dist/tools/fs/shell-read-environment.d.ts +10 -25
- package/dist/tools/fs/shell-read-environment.js +6 -18
- package/dist/tools/fs/shell-read-establish.d.ts +3 -2
- package/dist/tools/fs/shell-read-establish.js +5 -5
- package/dist/tools/fs/shell-read-judge.d.ts +38 -8
- package/dist/tools/fs/shell-read-judge.js +12 -10
- package/dist/tools/web.js +2 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +94 -10
- package/wire-changes/core@7.50.2.json +42 -0
- package/wire-changes/core@7.51.0.json +2421 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { judgeSessionRulesWrite, readMountedToolNames, readSessionRulesShape, SessionPolicyError, } from "../../core/session-policy-store.js";
|
|
4
4
|
import { atomicWriteFile, ensureDir, sanitizeScope } from "./fs-atomic.js";
|
|
5
5
|
import { guardCorruptSink } from "./corrupt-read-sink.js";
|
|
6
6
|
import { assertAdoptionBootGate, readRootAdoptionFile } from "./adoption/marker.js";
|
|
@@ -52,13 +52,12 @@ export function judgeSessionPolicyRowShape(row) {
|
|
|
52
52
|
return bad("rev is not a positive number");
|
|
53
53
|
if (row.gen !== undefined && (typeof row.gen !== "number" || !Number.isInteger(row.gen) || row.gen < 0))
|
|
54
54
|
return bad("gen is not a whole number");
|
|
55
|
-
const strArr = (x) => x === undefined || (Array.isArray(x) && x.every((e) => typeof e === "string"));
|
|
56
|
-
if (!strArr(row.toolAllow) || !strArr(row.toolDeny) || !strArr(row.commandAllow) || !strArr(row.commandDeny) || !strArr(row.allowDirs)) {
|
|
57
|
-
return bad("malformed rule field");
|
|
58
|
-
}
|
|
59
55
|
const { __sid: _sid, __principal: _principal, ...clean } = row;
|
|
60
56
|
void _sid;
|
|
61
57
|
void _principal;
|
|
58
|
+
const shaped = readSessionRulesShape(clean);
|
|
59
|
+
if ("fault" in shaped)
|
|
60
|
+
return bad(shaped.sentence);
|
|
62
61
|
return { rules: clean };
|
|
63
62
|
}
|
|
64
63
|
export function attributeSessionPolicyRow(dir, file, row) {
|
|
@@ -184,17 +183,7 @@ export class FileSessionPolicyStore {
|
|
|
184
183
|
}
|
|
185
184
|
async putRules(sessionId, principal, rules, opts) {
|
|
186
185
|
const prior = this.read(sessionId, principal);
|
|
187
|
-
|
|
188
|
-
throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts.expectedRev}, have ${prior?.rev ?? 0})`);
|
|
189
|
-
}
|
|
190
|
-
const clean = normalizeRules(stripRev(rules));
|
|
191
|
-
assertSessionRuleNamesCurrent(clean, this.mountedToolNames);
|
|
192
|
-
if (prior && opts?.operator !== true) {
|
|
193
|
-
const loosens = loosenReasons(prior, clean, this.mountedToolNames);
|
|
194
|
-
if (loosens.length > 0) {
|
|
195
|
-
throw new SessionPolicyError("loosen_forbidden", `a non-operator session-rule write may only tighten; refused: ${loosens.join("; ")}`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
186
|
+
const clean = judgeSessionRulesWrite(prior, rules, opts, this.mountedToolNames);
|
|
198
187
|
const mark = this.readMark(sessionId, principal);
|
|
199
188
|
if (mark?.__lineage === "lost") {
|
|
200
189
|
throw new SessionPolicyError("corrupt", `session rules for ${JSON.stringify(sessionId)} have no lineage evidence (the erase left a lost-lineage mark) — refusing to restart the key's lineage by guessing; delete the mark to restart it`);
|
|
@@ -5,6 +5,7 @@ import { Type } from "typebox";
|
|
|
5
5
|
import { ARTIFACT_ACTION_CONCURRENCY_SAFE, ARTIFACT_COMMIT_THROW_OUTCOME, ARTIFACT_FAILURE_RESULT_CODE, ARTIFACT_HOST_ERROR_CODE, ARTIFACT_LIST_SCOPES, ARTIFACT_PLANE_CONTENT_ORIGIN, ARTIFACT_PUBLISH_CONTEXTS, } from "../../core/artifact-host.js";
|
|
6
6
|
import { ARTIFACT_TOOL_NAME, toolFace } from "../../core/tool-catalog-entries.js";
|
|
7
7
|
import { resolveAsk } from "../../core/tool-policy.js";
|
|
8
|
+
import { NOT_BINDABLE } from "../../core/skill-grant-binding.js";
|
|
8
9
|
import { approvalCardOf } from "../../core/tool-roster.js";
|
|
9
10
|
import { errorResult } from "../../core/tools.js";
|
|
10
11
|
import { inlineUntrusted } from "../../core/untrusted-text.js";
|
|
@@ -345,6 +346,7 @@ export function createArtifactTool(opts) {
|
|
|
345
346
|
message: `Read a shared artifact: ${inlineUntrusted(url, 300)}${title}${version} — an artifact another person shared with this user; approving covers re-reads of it for the rest of this run.`,
|
|
346
347
|
requiresRealApproval: true,
|
|
347
348
|
approvalCard: approvalCardOf({ toolName: ARTIFACT_TOOL_NAME }, toolFace("artifact").renderHints?.approvalCard),
|
|
349
|
+
...NOT_BINDABLE,
|
|
348
350
|
}, onAsk, ctx.signal);
|
|
349
351
|
if (resolved.action !== "allow")
|
|
350
352
|
return { ok: false, reason: resolved.message ?? "not approved" };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The ONE admission an image's bytes pass before they reach the model as an `ImageContent` block —
|
|
3
|
-
* whether a file supplied them (the Read tool's image lane)
|
|
4
|
-
* arm). Four steps, in this order:
|
|
3
|
+
* whether a file supplied them (the Read tool's image lane), a caller did (`TaskSpec.images`' inline
|
|
4
|
+
* arm, a user attachment) or a tool returned them (the tool-result seat below). Four steps, in this order:
|
|
5
5
|
*
|
|
6
6
|
* ① the claimed type is one the content API accepts (png / jpeg / gif / webp — the four
|
|
7
7
|
* {@link imageMagicMatches} can verify);
|
|
@@ -14,10 +14,19 @@
|
|
|
14
14
|
*
|
|
15
15
|
* The answer is structured; each carrier words its own refusal (the Read tool speaks to the model, the
|
|
16
16
|
* task door to the caller). The block it admits is built from the bytes it checked — never from a
|
|
17
|
-
* caller's string it did not decode.
|
|
17
|
+
* caller's string it did not decode. A document's bytes pass the same shape of admission
|
|
18
|
+
* ({@link admitDocumentBytes}: `application/pdf`, the whole-send cap, the `%PDF-` signature).
|
|
19
|
+
*
|
|
20
|
+
* THE TOOL-RESULT SEAT ({@link admitToolResultMedia}): every executed call's result passes the loop's one
|
|
21
|
+
* post-execution half (`finalizeToolOutcome`) — an engine tool's, an MCP tool's converted content, a host
|
|
22
|
+
* tool's own return, a resumed approved call's, and whatever a `tool_result` hook put in their place — so
|
|
23
|
+
* the media blocks of every supply are admitted there, once, after the last hand that can change them and
|
|
24
|
+
* before the frame and the transcript read them. A block whose bytes the admission refuses becomes a text
|
|
25
|
+
* block that names the tool, the refusal and the size — never the bytes, and never silently nothing.
|
|
18
26
|
*/
|
|
19
|
-
import type { ImageContent } from "../../internal/llm.js";
|
|
27
|
+
import type { DocumentContent, ImageContent } from "../../internal/llm.js";
|
|
20
28
|
import { type ImageDimensions, type ImageDownsampler } from "../../core/mcp.js";
|
|
29
|
+
import { type MediaBlock } from "../../core/media-byte-cap.js";
|
|
21
30
|
/** The image types the content API accepts — the only ones a signature check can verify. */
|
|
22
31
|
export declare const ADMITTED_IMAGE_MIME_TYPES: readonly ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
23
32
|
/** The raw-byte read cap for an image, with or without a downsampler. */
|
|
@@ -49,3 +58,47 @@ export type ImageAdmission = {
|
|
|
49
58
|
base64: string;
|
|
50
59
|
};
|
|
51
60
|
export declare function admitImageBytes(bytes: Uint8Array, claimedMime: string, downsampler: ImageDownsampler | undefined): Promise<ImageAdmission>;
|
|
61
|
+
/** The document types the content API accepts — the one a signature check can verify. */
|
|
62
|
+
export declare const ADMITTED_DOCUMENT_MIME_TYPES: readonly ["application/pdf"];
|
|
63
|
+
export type DocumentAdmission = {
|
|
64
|
+
ok: true;
|
|
65
|
+
block: DocumentContent;
|
|
66
|
+
originalSize: number;
|
|
67
|
+
} | {
|
|
68
|
+
ok: false;
|
|
69
|
+
reason: "mime_unsupported" | "magic_mismatch";
|
|
70
|
+
rawBytes: number;
|
|
71
|
+
} | {
|
|
72
|
+
ok: false;
|
|
73
|
+
reason: "raw_too_large";
|
|
74
|
+
rawBytes: number;
|
|
75
|
+
cap: number;
|
|
76
|
+
};
|
|
77
|
+
/** A document's bytes: the claimed type is `application/pdf`, the raw bytes are within the whole-send cap
|
|
78
|
+
* ({@link PDF_TARGET_RAW_SIZE}, the Read tool's), and they start with `%PDF-`. */
|
|
79
|
+
export declare function admitDocumentBytes(bytes: Uint8Array, claimedMime: string): DocumentAdmission;
|
|
80
|
+
/** Why the tool-result seat refused a media block: the admissions' own words (an image's four — a document's are among
|
|
81
|
+
* them), or `undecodable` — the block carried no base64 string to decode. */
|
|
82
|
+
export type MediaRefusalReason = "undecodable" | "mime_unsupported" | "raw_too_large" | "magic_mismatch" | "base64_too_large";
|
|
83
|
+
/** One media block the seat refused: its type, the refusal, its decoded size, the bound it exceeded (the raw-byte cap for
|
|
84
|
+
* `raw_too_large`, the base64 length limit for `base64_too_large`), and its media type when it can be one. */
|
|
85
|
+
export interface ToolResultMediaRefusal {
|
|
86
|
+
type: MediaBlock["type"];
|
|
87
|
+
reason: MediaRefusalReason;
|
|
88
|
+
bytes: number;
|
|
89
|
+
limit?: number;
|
|
90
|
+
mimeType?: string;
|
|
91
|
+
}
|
|
92
|
+
/** What the seat made of one result's content: every media block rebuilt from its checked bytes or replaced, in its slot,
|
|
93
|
+
* by its placeholder; and the refusals. */
|
|
94
|
+
export interface ToolResultMediaAdmission {
|
|
95
|
+
content: unknown[];
|
|
96
|
+
refused: ToolResultMediaRefusal[];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The tool-result seat's admission over one executed call's content: `undefined` when no block claims a media type (the
|
|
100
|
+
* content stands as it is, and nothing is awaited); otherwise every block is read once into a new array, each media
|
|
101
|
+
* block admitted — rebuilt from the bytes it checked, or replaced in its slot by a placeholder naming `toolName` and the
|
|
102
|
+
* refusal. Total: a content whose blocks cannot be read is not media this seat can admit or refuse, and is left as it is.
|
|
103
|
+
*/
|
|
104
|
+
export declare function admitToolResultMedia(content: unknown, toolName: string): Promise<ToolResultMediaAdmission> | undefined;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
|
|
2
|
+
import { claimedMediaType } from "../../core/media-byte-cap.js";
|
|
3
|
+
import { readMember } from "../../core/thrown-value.js";
|
|
2
4
|
import { imageMagicMatches } from "./safety.js";
|
|
3
5
|
import { MAX_IMAGE_READ_BYTES, MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES } from "./fs-shared.js";
|
|
6
|
+
import { PDF_TARGET_RAW_SIZE, pdfMagicMatches } from "./pdf.js";
|
|
4
7
|
export const ADMITTED_IMAGE_MIME_TYPES = Object.freeze(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
5
8
|
export function imageReadCap(downsampler) {
|
|
6
9
|
return downsampler ? MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES : MAX_IMAGE_READ_BYTES;
|
|
@@ -34,3 +37,85 @@ export async function admitImageBytes(bytes, claimedMime, downsampler) {
|
|
|
34
37
|
(dimensions.originalWidth !== dimensions.displayWidth || dimensions.originalHeight !== dimensions.displayHeight);
|
|
35
38
|
return { ok: true, block: { type: "image", data: base64, mimeType }, originalSize: rawBytes, ...(dimensions !== undefined ? { dimensions } : {}), resized };
|
|
36
39
|
}
|
|
40
|
+
export const ADMITTED_DOCUMENT_MIME_TYPES = Object.freeze(["application/pdf"]);
|
|
41
|
+
export function admitDocumentBytes(bytes, claimedMime) {
|
|
42
|
+
const rawBytes = bytes.byteLength;
|
|
43
|
+
if (!ADMITTED_DOCUMENT_MIME_TYPES.includes(claimedMime))
|
|
44
|
+
return { ok: false, reason: "mime_unsupported", rawBytes };
|
|
45
|
+
if (rawBytes > PDF_TARGET_RAW_SIZE)
|
|
46
|
+
return { ok: false, reason: "raw_too_large", rawBytes, cap: PDF_TARGET_RAW_SIZE };
|
|
47
|
+
if (!pdfMagicMatches(bytes))
|
|
48
|
+
return { ok: false, reason: "magic_mismatch", rawBytes };
|
|
49
|
+
return { ok: true, block: { type: "document", data: Buffer.from(bytes).toString("base64"), mimeType: claimedMime }, originalSize: rawBytes };
|
|
50
|
+
}
|
|
51
|
+
const mediaTypeEcho = (mime) => typeof mime === "string" && /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}$/.test(mime) ? mime : undefined;
|
|
52
|
+
const ADMITTED_FOR = { image: ADMITTED_IMAGE_MIME_TYPES, document: ADMITTED_DOCUMENT_MIME_TYPES };
|
|
53
|
+
const REFUSAL_CLAUSE = {
|
|
54
|
+
undecodable: () => "it carried no base64 payload",
|
|
55
|
+
mime_unsupported: (r) => `${r.mimeType ?? "its media type"} is not a ${r.type} type the model accepts (${ADMITTED_FOR[r.type].join(", ")})`,
|
|
56
|
+
raw_too_large: (r) => `its ${r.bytes} bytes exceed the ${r.limit}-byte cap`,
|
|
57
|
+
magic_mismatch: (r) => `its ${r.bytes} bytes do not carry the ${r.mimeType} signature`,
|
|
58
|
+
base64_too_large: (r) => `its base64 encoding exceeds the ${r.limit}-character provider limit (${r.bytes} bytes)`,
|
|
59
|
+
};
|
|
60
|
+
const placeholderOf = (toolName, r) => ({
|
|
61
|
+
type: "text",
|
|
62
|
+
text: `[${r.type} from the ${JSON.stringify(toolName)} tool not shown: ${REFUSAL_CLAUSE[r.reason](r)} (media admission: ${r.reason})]`,
|
|
63
|
+
});
|
|
64
|
+
const claimsMedia = (block) => claimedMediaType(block) !== undefined;
|
|
65
|
+
async function admitBlock(block) {
|
|
66
|
+
const type = claimedMediaType(block);
|
|
67
|
+
if (type === undefined)
|
|
68
|
+
return { block };
|
|
69
|
+
const data = readMember(block, "data");
|
|
70
|
+
const mime = readMember(block, "mimeType");
|
|
71
|
+
const echo = mediaTypeEcho(mime);
|
|
72
|
+
const refusal = (reason, bytes, limit) => ({
|
|
73
|
+
refusal: { type, reason, bytes, ...(limit !== undefined ? { limit } : {}), ...(echo !== undefined ? { mimeType: echo } : {}) },
|
|
74
|
+
});
|
|
75
|
+
if (typeof data !== "string")
|
|
76
|
+
return refusal("undecodable", 0);
|
|
77
|
+
let a;
|
|
78
|
+
try {
|
|
79
|
+
const bytes = Buffer.from(data, "base64");
|
|
80
|
+
const claimed = typeof mime === "string" ? mime : "";
|
|
81
|
+
a = type === "image" ? await admitImageBytes(bytes, claimed, undefined) : admitDocumentBytes(bytes, claimed);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return refusal("undecodable", 0);
|
|
85
|
+
}
|
|
86
|
+
if (a.ok)
|
|
87
|
+
return { block: a.block };
|
|
88
|
+
if (a.reason === "raw_too_large")
|
|
89
|
+
return refusal(a.reason, a.rawBytes, a.cap);
|
|
90
|
+
if (a.reason === "base64_too_large")
|
|
91
|
+
return refusal(a.reason, a.rawBytes, MCP_IMAGE_MAX_BASE64);
|
|
92
|
+
return refusal(a.reason, a.rawBytes);
|
|
93
|
+
}
|
|
94
|
+
export function admitToolResultMedia(content, toolName) {
|
|
95
|
+
let blocks;
|
|
96
|
+
try {
|
|
97
|
+
if (!Array.isArray(content))
|
|
98
|
+
return undefined;
|
|
99
|
+
blocks = Array.from(content);
|
|
100
|
+
if (!blocks.some(claimsMedia))
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return (async () => {
|
|
107
|
+
const refused = [];
|
|
108
|
+
const out = [];
|
|
109
|
+
for (const b of blocks) {
|
|
110
|
+
const r = await admitBlock(b);
|
|
111
|
+
if ("block" in r) {
|
|
112
|
+
out.push(r.block);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
refused.push(r.refusal);
|
|
116
|
+
out.push(placeholderOf(toolName, r.refusal));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { content: out, refused };
|
|
120
|
+
})();
|
|
121
|
+
}
|
|
@@ -40,9 +40,30 @@ declare function unestablishedDetails(): {
|
|
|
40
40
|
/** The structured details a refusal or a mandated card carries: the deny card is the read faces' own (`violationDetails`,
|
|
41
41
|
* safety.ts), the containment and the unestablished cards are minted above. */
|
|
42
42
|
export type ShellReadRefusalDetails = NonNullable<ReturnType<typeof violationDetails>> | ReturnType<typeof containmentDetails> | ReturnType<typeof unestablishedDetails>;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
/**
|
|
44
|
+
* What the station answers for ONE call: a VOUCH — the finding `none`, nothing beside it but the facts — or a QUESTION, a
|
|
45
|
+
* disposition the decision table gives its finding ({@link import("./shell-read-judge.js").dispositionsOfRow}). A vouch carries no
|
|
46
|
+
* finding, no silence mark and no card: the table vouches on the `none` row alone
|
|
47
|
+
* ({@link import("./shell-read-judge.js").VouchOnlyOnTheNoneRow}), so a vouch beside anything else is a contradiction — one this
|
|
48
|
+
* type cannot spell, and one every reader of a station's answer refuses where an untyped station spells it anyway
|
|
49
|
+
* ({@link import("../../core/shell-read-silence.js").readStationAnswer}).
|
|
50
|
+
*/
|
|
51
|
+
export type ShellReadVerdict = ShellReadVouch | ShellReadQuestion;
|
|
52
|
+
/** The station has no question about the call. */
|
|
53
|
+
export interface ShellReadVouch extends ShellReadVerdictFacts {
|
|
54
|
+
readonly disposition: "vouch";
|
|
55
|
+
readonly finding: "none";
|
|
56
|
+
readonly environmentSilent?: never;
|
|
57
|
+
readonly rowsUnread?: never;
|
|
58
|
+
readonly face?: never;
|
|
59
|
+
readonly sentence?: never;
|
|
60
|
+
readonly details?: never;
|
|
61
|
+
readonly readRootCandidate?: never;
|
|
62
|
+
readonly unestablished?: never;
|
|
63
|
+
}
|
|
64
|
+
/** The station refuses the call or puts a question about it. */
|
|
65
|
+
export interface ShellReadQuestion extends ShellReadVerdictFacts {
|
|
66
|
+
readonly disposition: Exclude<ShellReadDisposition, "vouch">;
|
|
46
67
|
readonly finding: ShellReadFinding;
|
|
47
68
|
/** The environment did not answer the judgment — a resolver, a listing or an identity lookup gave no answer, a bound was
|
|
48
69
|
* reached, the judge itself did not answer: present on the `unanswered` finding the fold or the fallback gives. The gate
|
|
@@ -66,6 +87,13 @@ export interface ShellReadVerdict {
|
|
|
66
87
|
readonly details?: ShellReadRefusalDetails;
|
|
67
88
|
/** `mandated` on an `outside_roots` finding: the directory whose grant would clear this ask. */
|
|
68
89
|
readonly readRootCandidate?: ReadRootGrantCandidate;
|
|
90
|
+
/** A FACT beside the disposition: some read of the command could not be established (an `unestablishable` or `declined`
|
|
91
|
+
* finding anywhere in it, whichever finding won). A mode that releases the call releases it past the read-deny table. */
|
|
92
|
+
readonly unestablished?: true;
|
|
93
|
+
}
|
|
94
|
+
/** The FACTS a station's answer may carry beside either disposition — about what the command writes, read by the modes that
|
|
95
|
+
* answer file commands and by the write classes, never by the read table — and the admission the applying reader activates. */
|
|
96
|
+
export interface ShellReadVerdictFacts {
|
|
69
97
|
/** A FACT beside the disposition, for the mode that answers a file command itself: every write target this command
|
|
70
98
|
* names was established inside the boundary's WRITE roots at every seat the program writes it (the entry, where it
|
|
71
99
|
* lands, or both — `WriteSeat`, shell-read-write-targets.ts), off the write-protection table and off every deployment
|
|
@@ -91,9 +119,6 @@ export interface ShellReadVerdict {
|
|
|
91
119
|
readonly unplacedOptions?: {
|
|
92
120
|
readonly program: "cp" | "mv";
|
|
93
121
|
};
|
|
94
|
-
/** A FACT beside the disposition: some read of the command could not be established (an `unestablishable` or `declined`
|
|
95
|
-
* finding anywhere in it, whichever finding won). A mode that releases the call releases it past the read-deny table. */
|
|
96
|
-
readonly unestablished?: true;
|
|
97
122
|
/** The station's admission of this call's judged target, activated only by the caller that APPLIES this verdict (the
|
|
98
123
|
* gate's fold, the resumed leg's belt): a verdict nobody received — a wait that elapsed, a judgment that threw —
|
|
99
124
|
* admits nothing, whenever it completes. */
|
|
@@ -288,7 +288,7 @@ function verdictOf(tool, seat, fold, judged, boundary) {
|
|
|
288
288
|
const redirectsOnly = fold.finding === "outside_roots" && fold.unestablished !== true && outsideFindingsAreRedirections(judged);
|
|
289
289
|
const disposition = decideShellRead(fold.finding, seat, { redirectsOnly, narrowedReadFace: fold.unplacedRead === true && boundary.face !== "open" });
|
|
290
290
|
if (disposition === "vouch")
|
|
291
|
-
return { disposition, finding:
|
|
291
|
+
return { disposition, finding: "none" };
|
|
292
292
|
const paths = outsidePathsOf(judged);
|
|
293
293
|
const unestablished = fold.unestablished === true ? { unestablished: true } : {};
|
|
294
294
|
const face = faceOf(fold);
|
|
@@ -354,14 +354,12 @@ async function runShellReadStation(tool, command, declared, seat, readers, estab
|
|
|
354
354
|
const boundary = { ...declared, roots: established.roots };
|
|
355
355
|
const onPlan = await applyAlsoInside(judgePlan(plan, established, boundary), boundary);
|
|
356
356
|
const judgedNow = () => {
|
|
357
|
-
const unfinished = door.
|
|
357
|
+
const unfinished = door.incomplete();
|
|
358
358
|
return unfinished.length > 0 ? { ...onPlan, incomplete: unfinished } : onPlan;
|
|
359
359
|
};
|
|
360
360
|
let judged = judgedNow();
|
|
361
361
|
let judgedVerdict = verdictOf(tool, seat, foldShellReadFindings(judged), judged, boundary);
|
|
362
|
-
const probesFrom = door.mark();
|
|
363
362
|
const guarded = judgedVerdict.disposition === "refuse" ? { unplaced: false, writeProtected: false } : await writeGuardsOf(judged, boundary, door.fs, signal);
|
|
364
|
-
door.silence(probesFrom);
|
|
365
363
|
judged = judgedNow();
|
|
366
364
|
judgedVerdict = verdictOf(tool, seat, foldShellReadFindings(judged), judged, boundary);
|
|
367
365
|
const unplaced = unplacedOptionsOf(judged);
|
|
@@ -10,15 +10,10 @@
|
|
|
10
10
|
* cannot escape the record.
|
|
11
11
|
*
|
|
12
12
|
* What is NOT a failure: a conclusive absence (`not_found`, `not_directory` — the answer is "nothing there") and an adapter
|
|
13
|
-
* that
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* command, not about a word or a station: the walks and probes that found the resolver silent are marked
|
|
18
|
-
* ({@link ObservedEnvironment.silence}), and {@link ObservedEnvironment.settled} — read by whoever made the door, after EVERY
|
|
19
|
-
* lookup the command makes (the establishment, the write guards) has asked — takes the marked records back only when no lookup
|
|
20
|
-
* was ever answered. A resolver that answered once has shown it can; a silence after that is a failure, wherever it falls. A
|
|
21
|
-
* resolver that declared its answers authoritative is never read as an absence.
|
|
13
|
+
* that has no filesystem surface (`not_supported` — its answer that there is no file face: a chain it answers so keeps the
|
|
14
|
+
* lexical reading). Everything else is, and is recorded wherever it falls — a resolver that answered nothing at all in the
|
|
15
|
+
* command included: a lookup that failed (`permission_denied`, `timeout`, `unknown`, a throw) is a resolver failing, whose
|
|
16
|
+
* identity of the words cannot be judged, never an adapter without a file face.
|
|
22
17
|
*/
|
|
23
18
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
24
19
|
/** One thing the station could not finish: an environment lookup that gave no answer, or a bound the station stopped at. */
|
|
@@ -42,24 +37,15 @@ export type ObservedFilesystem = ExecutionEnv & {
|
|
|
42
37
|
/** The observed door for one command. */
|
|
43
38
|
export interface ObservedEnvironment {
|
|
44
39
|
readonly fs: ObservedFilesystem;
|
|
45
|
-
/** Whether the environment DECLARES its `canonicalPath` the authority on where a path lands:
|
|
46
|
-
*
|
|
40
|
+
/** Whether the environment DECLARES its `canonicalPath` the authority on where a path lands: a chain it does not answer at all
|
|
41
|
+
* is then not placed (the establishment's reading; the door records the lookups alike either way). */
|
|
47
42
|
readonly authoritative: boolean;
|
|
48
|
-
/** What the door recorded, in the order it happened
|
|
49
|
-
|
|
50
|
-
settled(): Incomplete[];
|
|
43
|
+
/** What the door recorded, in the order it happened; call it after the last lookup of the command. */
|
|
44
|
+
incomplete(): Incomplete[];
|
|
51
45
|
/** The station stopped at its own bound (more directories or names than it enumerates, a link chain past its hop bound). */
|
|
52
46
|
bound(detail: string): void;
|
|
53
|
-
/** Records that came from outside the door
|
|
54
|
-
|
|
55
|
-
adopt(records: readonly Incomplete[], answered?: boolean): void;
|
|
56
|
-
/** Whether the resolver answered a path question (a path, or a conclusive "nothing there") at least once. */
|
|
57
|
-
answered(): boolean;
|
|
58
|
-
/** Where a walk's (or a probe's) records start: pass it to {@link silence} once the lookups are done. */
|
|
59
|
-
mark(): number;
|
|
60
|
-
/** The lookups since `from` found the resolver silent: their records are taken back by {@link settled} only when the
|
|
61
|
-
* resolver never answered anything in the command (and never where it is {@link authoritative}). */
|
|
62
|
-
silence(from: number): void;
|
|
47
|
+
/** Records that came from outside the door: the declared roots' own lookup and the home views a read could not be placed against. */
|
|
48
|
+
adopt(records: readonly Incomplete[]): void;
|
|
63
49
|
}
|
|
64
50
|
/** The declared roots, canonicalized ONCE per establisher (through the same door), with what that lookup could not finish —
|
|
65
51
|
* merged into every establishment. `known`: the caller already holds them as canonical identities and they are used verbatim
|
|
@@ -67,7 +53,6 @@ export interface ObservedEnvironment {
|
|
|
67
53
|
export declare function canonicalRootsOnce(env: ExecutionEnv, readRoots: readonly string[], known: boolean): (signal?: AbortSignal) => Promise<{
|
|
68
54
|
readonly roots: readonly string[];
|
|
69
55
|
readonly incomplete: readonly Incomplete[];
|
|
70
|
-
readonly answered: boolean;
|
|
71
56
|
}>;
|
|
72
57
|
/** Wrap `env` so every lookup that fails inconclusively is recorded; the result a caller sees is the environment's own. */
|
|
73
58
|
export declare function observeEnvironment(env: ExecutionEnv): ObservedEnvironment;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { canonicalPathIsAuthoritative } from "../../core/remote-env.js";
|
|
2
2
|
const OBSERVED = Symbol("observedFilesystem");
|
|
3
3
|
const CONCLUSIVE = new Set(["not_found", "not_directory", "not_supported"]);
|
|
4
|
-
const ABSENCES = new Set(["not_found", "not_directory"]);
|
|
5
4
|
export function canonicalRootsOnce(env, readRoots, known) {
|
|
6
|
-
let once = known ? Promise.resolve({ roots: readRoots, incomplete: []
|
|
5
|
+
let once = known ? Promise.resolve({ roots: readRoots, incomplete: [] }) : undefined;
|
|
7
6
|
return (signal) => (once ??= (async () => {
|
|
8
7
|
const seen = observeEnvironment(env);
|
|
9
8
|
const roots = [];
|
|
@@ -11,7 +10,7 @@ export function canonicalRootsOnce(env, readRoots, known) {
|
|
|
11
10
|
const c = await seen.fs.canonicalPath(r, signal);
|
|
12
11
|
roots.push(c.ok ? c.value : r);
|
|
13
12
|
}
|
|
14
|
-
const result = { roots, incomplete: seen.
|
|
13
|
+
const result = { roots, incomplete: seen.incomplete() };
|
|
15
14
|
if (result.incomplete.length > 0)
|
|
16
15
|
once = undefined;
|
|
17
16
|
return result;
|
|
@@ -20,11 +19,8 @@ export function canonicalRootsOnce(env, readRoots, known) {
|
|
|
20
19
|
export function observeEnvironment(env) {
|
|
21
20
|
const authoritative = canonicalPathIsAuthoritative(env);
|
|
22
21
|
const records = [];
|
|
23
|
-
|
|
24
|
-
const note = (op, path, what, code) => void records.push({ op, detail: `"${path}" ${what} (${code})`, silent: false });
|
|
22
|
+
const note = (op, path, what, code) => void records.push({ op, detail: `"${path}" ${what} (${code})` });
|
|
25
23
|
const watch = (op, path, what, ask) => new Promise((resolve) => resolve(ask())).then((r) => {
|
|
26
|
-
if (op === "canonicalPath" && (r.ok || ABSENCES.has(r.error.code)))
|
|
27
|
-
answers += 1;
|
|
28
24
|
if (!r.ok && !CONCLUSIVE.has(r.error.code))
|
|
29
25
|
note(op, path, what, r.error.code);
|
|
30
26
|
return r;
|
|
@@ -56,16 +52,8 @@ export function observeEnvironment(env) {
|
|
|
56
52
|
return {
|
|
57
53
|
fs,
|
|
58
54
|
authoritative,
|
|
59
|
-
|
|
60
|
-
bound: (detail) => void records.push({ op: "bound", detail
|
|
61
|
-
adopt: (adopted,
|
|
62
|
-
for (const r of adopted)
|
|
63
|
-
records.push({ ...r, silent: false });
|
|
64
|
-
if (answered)
|
|
65
|
-
answers += 1;
|
|
66
|
-
},
|
|
67
|
-
mark: () => records.length,
|
|
68
|
-
silence: (from) => (authoritative ? undefined : void records.slice(from).forEach((r) => void (r.silent = true))),
|
|
69
|
-
answered: () => answers > 0,
|
|
55
|
+
incomplete: () => records.map(({ op, detail }) => ({ op, detail })),
|
|
56
|
+
bound: (detail) => void records.push({ op: "bound", detail }),
|
|
57
|
+
adopt: (adopted) => void records.push(...adopted.map(({ op, detail }) => ({ op, detail }))),
|
|
70
58
|
};
|
|
71
59
|
}
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
* target, so the target is established where the link lands (read one hop at a time, bounded) — or not at
|
|
18
18
|
* all when the environment cannot read the link.
|
|
19
19
|
* 2. An unanswerable filesystem manufactures no refusal on its own: a chain no ancestor answered degrades
|
|
20
|
-
* to the lexical spelling on an environment that declares no authority for its resolver
|
|
21
|
-
*
|
|
20
|
+
* to the lexical spelling on an environment that declares no authority for its resolver, and is `unestablishable` on
|
|
21
|
+
* one that does (`canonicalPathIsAuthoritative`); only `not_supported` (no file face) is no failure — any other non-answer
|
|
22
|
+
* is recorded at the door and the command is `unanswered` (shell-read-environment.ts).
|
|
22
23
|
* 3. An ISOLATED environment stays lexical, disclosed rather than silently degraded: the sandbox is the
|
|
23
24
|
* containment there and `canonicalPath` is an RPC into a filesystem that is not this host's
|
|
24
25
|
* (`boundaryIsTheSandbox`). The split is the DECLARED isolation capability, never the remote interface's
|
|
@@ -174,7 +174,8 @@ function writtenByText(plan, boundary) {
|
|
|
174
174
|
}
|
|
175
175
|
function unfollowedBackupReads(j, boundary, cause) {
|
|
176
176
|
const spellings = sedBackupNames(j.followed.operand.spelled, j.followed.suffix).flatMap((name) => lexicalReadsOf({ ...j.candidate, spelled: name }, boundary).flatMap((r) => [...(r.target !== undefined ? [r.target] : []), ...r.spellings]));
|
|
177
|
-
|
|
177
|
+
const read = { candidate: j.candidate, established: "unestablishable", spellings: [...new Set(spellings)], cause };
|
|
178
|
+
return (j.seats ?? ["landing"]).map((seat) => writtenAt(read, j, seat));
|
|
178
179
|
}
|
|
179
180
|
function writtenAt(read, j, seat) {
|
|
180
181
|
if (read.candidate.face !== "write" || !(j.written ?? j.seats ?? ["landing"]).includes(seat))
|
|
@@ -205,8 +206,8 @@ export function createShellReadEstablisher(env, readRoots, opts) {
|
|
|
205
206
|
establish: async (plan, boundary, signal, door = observeEnvironment(env)) => {
|
|
206
207
|
if (lexicalOnly)
|
|
207
208
|
return establishShellReadsLexically(plan, boundary);
|
|
208
|
-
const { roots, incomplete
|
|
209
|
-
door.adopt(incomplete
|
|
209
|
+
const { roots, incomplete } = await canonicalRoots(signal);
|
|
210
|
+
door.adopt(incomplete);
|
|
210
211
|
return await withHomeViews(door, opts, await legOver(door)(plan, { ...boundary, roots }, roots, signal), signal);
|
|
211
212
|
},
|
|
212
213
|
reestablish: async (previous, boundary, signal) => {
|
|
@@ -229,7 +230,6 @@ function establishLegOver(seen, readRoots) {
|
|
|
229
230
|
const parts = p.split(SEP);
|
|
230
231
|
const tail = [];
|
|
231
232
|
let allAbsent = true;
|
|
232
|
-
const began = seen.mark();
|
|
233
233
|
while (parts.length > 0) {
|
|
234
234
|
const at = parts.join("/") || "/";
|
|
235
235
|
const canon = await env.canonicalPath(at, signal);
|
|
@@ -252,7 +252,7 @@ function establishLegOver(seen, readRoots) {
|
|
|
252
252
|
allAbsent = false;
|
|
253
253
|
tail.push(parts.pop());
|
|
254
254
|
}
|
|
255
|
-
return
|
|
255
|
+
return undefined;
|
|
256
256
|
};
|
|
257
257
|
const parentOf = (canonical) => normalizeAbsPathLexically(`${canonical}/..`);
|
|
258
258
|
const resolve = async (p, signal, writeTarget = false, linkHops = WRITE_LINK_HOPS) => {
|
|
@@ -52,14 +52,18 @@ export type RankTableCoversEveryFinding = AssertAllKeysHandled<Exclude<ShellRead
|
|
|
52
52
|
* the auto-mode classifier, a stored allow rule) but no answerless engine table may; `vouch` = the station
|
|
53
53
|
* has no question. */
|
|
54
54
|
export type ShellReadDisposition = "refuse" | "mandated" | "ordinary" | "vouch";
|
|
55
|
-
/** One cell of the decision table: a disposition, or — where the
|
|
56
|
-
*
|
|
55
|
+
/** One cell of the decision table: a disposition, or — where the FORM of the question decides it — its readings: `narrowed` (raised
|
|
56
|
+
* under the `roots` read face: the fence the deployment narrowed, and the engine default), `open` (under the open read face, or a
|
|
57
|
+
* written target alone) and, where it differs, `redirect` (every containment finding is an output redirection outside the write
|
|
58
|
+
* roots). */
|
|
57
59
|
export type ShellReadCell = ShellReadDisposition | {
|
|
58
60
|
readonly narrowed: ShellReadDisposition;
|
|
59
61
|
readonly open: ShellReadDisposition;
|
|
62
|
+
readonly redirect?: ShellReadDisposition;
|
|
60
63
|
};
|
|
61
|
-
/** A cell, read for
|
|
62
|
-
|
|
64
|
+
/** A cell, read for the form of the question ({@link ShellReadCellContext}): the redirect reading where the cell has one and every
|
|
65
|
+
* containment finding is a redirection, otherwise the read face's (absent ⇒ narrowed, the engine default). */
|
|
66
|
+
export declare function dispositionOfCell(cell: ShellReadCell, context?: ShellReadCellContext): ShellReadDisposition;
|
|
63
67
|
/**
|
|
64
68
|
* THE decision table — total over findings × seats; no cell is n/a. Row by row:
|
|
65
69
|
* · `deny_hit` — refused on every seat, for a spelling the station can establish: a deny row is a floor
|
|
@@ -67,7 +71,13 @@ export declare function dispositionOfCell(cell: ShellReadCell, narrowedReadFace:
|
|
|
67
71
|
* the `unestablishable` row) — the floor holds what it can place, and says so rather than guessing.
|
|
68
72
|
* · `outside_roots` — asked where a person can be asked (the full shell), refused where none can (the
|
|
69
73
|
* read-only face has no approval channel; a rewrite that leaves the roots after approval was seen by
|
|
70
|
-
* nobody).
|
|
74
|
+
* nobody). The full shell's ask is MANDATED, except for a command whose every containment finding is an OUTPUT
|
|
75
|
+
* REDIRECTION outside the write roots (the cell's `redirect` reading): the permission question a stored allow answers,
|
|
76
|
+
* the ORDINARY ask. The reference implementation's redirection check (CC 2.1.296 `lvr` (cli296.js:249645) @18502854,
|
|
77
|
+
* called @18508831) asks such a target as a plain permission ask that is not rule-overridable: a whole-tool `Bash` allow
|
|
78
|
+
* (`$pn` @18717070) and bypass (`$pn` @18716980) release it, a prefix allow does not (`vX`'s ask returns before the allow
|
|
79
|
+
* rules, @18567915). A command that also reads outside the roots, or writes a program operand there (`tee`, `cp`, …),
|
|
80
|
+
* keeps the mandate.
|
|
71
81
|
* · `unestablishable` — the floor could not say where the command reads, so it is not vouched anywhere: asked
|
|
72
82
|
* where a person can be asked (the full shell), refused where none can be asked. The full shell's cell is read by the read face
|
|
73
83
|
* the question was raised under: on a NARROWED (`roots`) face — the engine default — the deployment asked to be asked, so the
|
|
@@ -101,7 +111,11 @@ export declare const SHELL_READ_DECISION: {
|
|
|
101
111
|
readonly rejudge: "refuse";
|
|
102
112
|
};
|
|
103
113
|
readonly outside_roots: {
|
|
104
|
-
readonly full_shell:
|
|
114
|
+
readonly full_shell: {
|
|
115
|
+
readonly narrowed: "mandated";
|
|
116
|
+
readonly open: "mandated";
|
|
117
|
+
readonly redirect: "ordinary";
|
|
118
|
+
};
|
|
105
119
|
readonly readonly: "refuse";
|
|
106
120
|
readonly rejudge: "refuse";
|
|
107
121
|
};
|
|
@@ -142,15 +156,30 @@ export type DecisionTableCoversEverySeat = AssertAllKeysHandled<Exclude<ShellRea
|
|
|
142
156
|
/** The disposition of one command-level finding on one seat — the table, read. A seat word outside
|
|
143
157
|
* {@link SHELL_READ_SEATS} (an untyped caller, or one still passing a retired word) is refused loudly: an
|
|
144
158
|
* absent disposition would read as "no verdict", which no caller can act on safely. */
|
|
145
|
-
export declare function decideShellRead(finding:
|
|
159
|
+
export declare function decideShellRead<F extends ShellReadFinding>(finding: F, seat: ShellReadSeat, context?: ShellReadCellContext): RowDisposition<F>;
|
|
160
|
+
/** The dispositions the table's row for `F` holds — on any seat, under any form of the question (the table's own literal words). */
|
|
161
|
+
export type RowDisposition<F extends ShellReadFinding> = CellWords<(typeof SHELL_READ_DECISION)[F][ShellReadSeat]>;
|
|
146
162
|
/** What the table is read for beyond (finding, seat): the two facts of a judged command a cell can depend on. */
|
|
147
163
|
export interface ShellReadCellContext {
|
|
148
|
-
/** Every containment finding is an output redirection outside the write roots (
|
|
164
|
+
/** Every containment finding is an output redirection outside the write roots (a cell's `redirect` reading). Absent ⇒ no. */
|
|
149
165
|
readonly redirectsOnly?: boolean;
|
|
150
166
|
/** The question was raised under a NARROWED (`roots`) read face — a read candidate or the text's own structure while the read
|
|
151
167
|
* face runs the roots containment; `false` for the open face and for a written target alone. Absent ⇒ narrowed, the engine default. */
|
|
152
168
|
readonly narrowedReadFace?: boolean;
|
|
153
169
|
}
|
|
170
|
+
/** Every disposition the table gives `finding` — on any seat, under any form of the question; empty for a word outside
|
|
171
|
+
* {@link SHELL_READ_FINDINGS}. A disposition outside it is no reading of the table: what a reader of a station's answer
|
|
172
|
+
* checks the answer against. */
|
|
173
|
+
export declare function dispositionsOfRow(finding: string): ReadonlySet<ShellReadDisposition>;
|
|
174
|
+
/** The words a cell can read as. */
|
|
175
|
+
type CellWords<C> = C extends string ? C : C[keyof C];
|
|
176
|
+
/** The findings whose row the table vouches on, on any seat and under any form. */
|
|
177
|
+
type FindingsTheTableVouches = {
|
|
178
|
+
[F in ShellReadFinding]: "vouch" extends RowDisposition<F> ? F : never;
|
|
179
|
+
}[ShellReadFinding];
|
|
180
|
+
/** Compile-time fence: `never` while `vouch` is a cell of the `none` row alone — what lets a vouch carry the finding `none` and
|
|
181
|
+
* nothing else ({@link import("./shell-read-check.js").ShellReadVerdict}). */
|
|
182
|
+
export type VouchOnlyOnTheNoneRow = AssertAllKeysHandled<Exclude<FindingsTheTableVouches, "none">>;
|
|
154
183
|
/**
|
|
155
184
|
* The boundary a shell read is judged against: the declared roots (canonical), the working directory and
|
|
156
185
|
* home the plan's bases start from, the deny seat (the SAME `denyMatch` closure every read face mounts),
|
|
@@ -399,3 +428,4 @@ export declare function grantOfferFor(candidate: string, single: string | undefi
|
|
|
399
428
|
} | undefined;
|
|
400
429
|
/** The same judged plan under another boundary — the established reads are kept, only the judgment reruns. */
|
|
401
430
|
export declare function rejudge(judged: JudgedPlan, boundary: ShellReadBoundary): JudgedPlan;
|
|
431
|
+
export {};
|