@sema-agent/core 7.49.2 → 7.49.4

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.
@@ -0,0 +1,92 @@
1
+ /**
2
+ * THE STANDARD DEVICES — the spellings a shell command names for its own standard streams and terminal, which the
3
+ * containment question is not asked of, and which write nothing durable: ONE table, ONE predicate, read by every layer that
4
+ * would otherwise treat the word as a path — the shell read station's entries (an output or input redirection's target, a read
5
+ * operand, the content a program writes through an operand, a `git diff` operand) and the shape tier's two readings of "does
6
+ * this redirection write" (the read-only reader, the classify faces' data-free redirections).
7
+ *
8
+ * The table is the reference implementation's own device predicate (CC 2.1.296 `wwe`, cli296.js @18511591): the null
9
+ * device, the three standard streams, the terminal, any `/dev/fd/<digits>`, and `/proc/self/fd/{0,1,2}`. A word is such a
10
+ * spelling AS WRITTEN (quotes removed, nothing folded: `/dev/./null` is a path, not the null device).
11
+ *
12
+ * What exempts a word is more than its spelling, on both counts:
13
+ * · it is I/O THROUGH the node — a redirection, a read operand, the content a program writes by opening the path
14
+ * (`ReadCandidate.through`). A program that creates, unlinks, renames or retargets the path's directory ENTRY
15
+ * (`rm`, `mv`, `mkdir`, `touch`, an in-place edit) is judged as it judges any path, and so is a word of a program the
16
+ * station has no row for (`ln`, `chmod`): nothing says that program only opens it;
17
+ * · no redirection of the same text can have rebound the descriptor the spelling names. A descriptor spelling is a
18
+ * name for whatever that descriptor is bound to WHEN THE WORD IS OPENED: `3< f` or `exec 3>f` before it makes
19
+ * `/dev/fd/3` the file `f`, and `1<f > /dev/stdout` reopens `f` for writing. Such a descriptor is judged at its landing
20
+ * like any path. (One step stricter than the reference, which exempts the spelling whatever the text does.) A text the
21
+ * consumer cannot read in full may bind anything, so it exempts no descriptor spelling; the null device and the
22
+ * terminal name no descriptor and are exempt always.
23
+ *
24
+ * Beside the table, the generator devices (`/dev/zero`, `/dev/random`, `/dev/urandom`, `/dev/full`) are not a stream of the
25
+ * process: a read of one is no question only while the program read model knows the read is bounded
26
+ * (`byteBoundIsSmall`: `head -c N`), the same fact the read-only classifier's endless-device arm reads.
27
+ */
28
+ import type { ReadCandidate, ReadCandidateProvenance, ShellReadPlan } from "./shell-read-plan.js";
29
+ /** Is this word, as written, a standard device? `descriptor`: the descriptor number it names, when it names one. */
30
+ export declare function standardDeviceOf(spelled: string): {
31
+ readonly descriptor?: number;
32
+ } | undefined;
33
+ /** The devices that PRODUCE data without end: read in bounded bytes they cost nothing, read unbounded they never finish. */
34
+ export declare const GENERATOR_DEVICES: ReadonlySet<string>;
35
+ /** A redirection, as far as binding a descriptor goes: its operator and the number written in front of it (the lexer's
36
+ * `LexedRedirect` and the plan's `ShellReadRedirect` both are one). */
37
+ export interface RedirectionBinding {
38
+ readonly op: string;
39
+ readonly fd?: number;
40
+ }
41
+ /** What a text does to its descriptors: how many redirections bind each (a count, so the one that IS the use of a spelling
42
+ * can be taken out of it), and whether it may bind any it does not show. */
43
+ export interface LineDescriptors {
44
+ /** Some part of the text the consumer cannot read in full may bind any descriptor (an unreadable text, a program or an
45
+ * `eval` / `source` text it does not see, a numbered here-document whose number the lexer drops, a redirection whose number
46
+ * the reader keeps no record of). */
47
+ unknown: boolean;
48
+ /** A `{name}` word — bash allocates a descriptor from 10 up for a redirection written `{name}>f`. */
49
+ named: boolean;
50
+ readonly bound: Map<number, number>;
51
+ }
52
+ export declare const newLineDescriptors: () => LineDescriptors;
53
+ /** The descriptors one redirection binds: the number written in front of it, else the operator's own (`>` family: 1,
54
+ * `<` family: 0, `&>`: both output streams). */
55
+ export declare function descriptorsBoundBy(r: RedirectionBinding): readonly number[];
56
+ export declare function bindDescriptors(into: LineDescriptors, r: RedirectionBinding): void;
57
+ export declare const isNamedDescriptorWord: (raw: string) => boolean;
58
+ /**
59
+ * THE predicate for a word and the text it stands in: is it a standard device (the table, as written) whose descriptor — if it
60
+ * names one — no redirection of the text rebinds? `self`: the descriptors the redirection that IS the use binds (a word that
61
+ * is not a redirection's target passes none).
62
+ */
63
+ export declare function standardDeviceUnrebound(spelled: string, text: LineDescriptors, self: readonly number[]): boolean;
64
+ /**
65
+ * The provenances whose word a program OPENS (or a redirection opens): a read operand or option value, an input
66
+ * redirection's source, an output redirection's target, and a word of a program the deployment did not list as a reader but
67
+ * whose row the station holds (`unlisted_word`, unless it waits for the filesystem — {@link ReadCandidate.speculative}: a
68
+ * program with no row may do anything with the word), and the root of a tree walk (`grep -r`, `diff`: a standard device is
69
+ * no directory to walk — an option that writes beyond the operands, `find -delete`, takes the exemption back, see
70
+ * `judgedCandidatesOf`). Not a directory a program changes into (`cd_target`), not a program, an assignment's value or a
71
+ * mount source.
72
+ */
73
+ export declare const OPENED_PROVENANCES: ReadonlySet<ReadCandidateProvenance>;
74
+ /**
75
+ * Does the containment question go unasked of this candidate? The one predicate every entry of the station reads: the plan
76
+ * marked the word a standard device no redirection of its text rebinds ({@link ReadCandidate.device}), and — for a word the
77
+ * program WRITES — the program writes its content through the path ({@link ReadCandidate.through}), not its entry.
78
+ */
79
+ export declare function passesUnasked(candidate: ReadCandidate): boolean;
80
+ /**
81
+ * What the whole text of a plan binds: every redirection of every segment — a single one, an `exec`'s, a `{name}` one included —
82
+ * and whether the text may bind more than it shows. The ONE computation behind the station's rebinding rule
83
+ * ({@link markStandardDevices}) and the shape tier's reading of a program's device operands (`tee /dev/fd/3`).
84
+ */
85
+ export declare function descriptorsOfPlan(plan: ShellReadPlan): LineDescriptors;
86
+ /**
87
+ * Mark every candidate of a whole plan (the nested plans included) that names a standard device the containment question
88
+ * is not asked of ({@link ReadCandidate.device}): the word is the table's, it is a word a program or a redirection opens,
89
+ * and no redirection of the text rebinds the descriptor it names. Computed once, over the text as a whole: whether a
90
+ * descriptor is rebound is a fact of the line, not of a segment.
91
+ */
92
+ export declare function markStandardDevices(plan: ShellReadPlan): ShellReadPlan;
@@ -0,0 +1,109 @@
1
+ const STANDARD_DEVICES = [
2
+ { spelled: /^\/dev\/null$/ },
3
+ { spelled: /^\/dev\/stdin$/, descriptor: () => 0 },
4
+ { spelled: /^\/dev\/stdout$/, descriptor: () => 1 },
5
+ { spelled: /^\/dev\/stderr$/, descriptor: () => 2 },
6
+ { spelled: /^\/dev\/tty$/ },
7
+ { spelled: /^\/dev\/fd\/(\d+)$/, descriptor: (m) => Number(m[1]) },
8
+ { spelled: /^\/proc\/self\/fd\/([0-2])$/, descriptor: (m) => Number(m[1]) },
9
+ ];
10
+ export function standardDeviceOf(spelled) {
11
+ for (const row of STANDARD_DEVICES) {
12
+ const match = row.spelled.exec(spelled);
13
+ if (match !== null)
14
+ return row.descriptor === undefined ? {} : { descriptor: row.descriptor(match) };
15
+ }
16
+ return undefined;
17
+ }
18
+ export const GENERATOR_DEVICES = new Set(["/dev/zero", "/dev/random", "/dev/urandom", "/dev/full"]);
19
+ export const newLineDescriptors = () => ({ unknown: false, named: false, bound: new Map() });
20
+ export function descriptorsBoundBy(r) {
21
+ if (r.fd !== undefined)
22
+ return [r.fd];
23
+ if (r.op === "&>" || r.op === "&>>")
24
+ return [1, 2];
25
+ return [r.op === "<" || r.op === "<<<" || r.op === "<&" ? 0 : 1];
26
+ }
27
+ export function bindDescriptors(into, r) {
28
+ for (const fd of descriptorsBoundBy(r))
29
+ into.bound.set(fd, (into.bound.get(fd) ?? 0) + 1);
30
+ }
31
+ const NAMED_DESCRIPTOR_WORD = /^\{[A-Za-z_][A-Za-z0-9_]*\}$/;
32
+ export const isNamedDescriptorWord = (raw) => NAMED_DESCRIPTOR_WORD.test(raw);
33
+ function reboundBy(text, descriptor, self) {
34
+ if (text.unknown || (text.named && descriptor >= 3))
35
+ return true;
36
+ return (text.bound.get(descriptor) ?? 0) - (self.includes(descriptor) ? 1 : 0) > 0;
37
+ }
38
+ export function standardDeviceUnrebound(spelled, text, self) {
39
+ const device = standardDeviceOf(spelled);
40
+ return device !== undefined && (device.descriptor === undefined || !reboundBy(text, device.descriptor, self));
41
+ }
42
+ export const OPENED_PROVENANCES = new Set(["operand", "option_value", "stdin_redirect", "write_redirect_target", "unlisted_word", "traversal_root"]);
43
+ export function passesUnasked(candidate) {
44
+ return candidate.device === true && (candidate.face !== "write" || candidate.through === true);
45
+ }
46
+ const SHELL_CODE_VERBS = new Set(["eval", "source", "."]);
47
+ function collectDescriptors(plan, into) {
48
+ if (plan.unreadable !== undefined)
49
+ into.unknown = true;
50
+ for (const segment of plan.segments) {
51
+ if (segment.declines.length > 0 || segment.unreadProgramText !== undefined)
52
+ into.unknown = true;
53
+ if (segment.verb !== undefined && SHELL_CODE_VERBS.has(segment.verb.basename) && segment.nested.length === 0)
54
+ into.unknown = true;
55
+ if (segment.reads.some((c) => c.provenance === "program_path" && c.expands === "unresolvable"))
56
+ into.unknown = true;
57
+ for (const r of segment.redirects)
58
+ bindDescriptors(into, r);
59
+ if (segment.argv.some((w) => isNamedDescriptorWord(w.raw)))
60
+ into.named = true;
61
+ const text = segment.text.replace(/\\\r?\n/g, "");
62
+ if (/(?:^|[^<])<<(?!<)/.test(text)) {
63
+ if (/\d<<(?!<)/.test(text))
64
+ into.unknown = true;
65
+ else
66
+ into.bound.set(0, (into.bound.get(0) ?? 0) + 1);
67
+ }
68
+ for (const n of segment.nested)
69
+ collectDescriptors(n, into);
70
+ }
71
+ }
72
+ export function descriptorsOfPlan(plan) {
73
+ const into = newLineDescriptors();
74
+ collectDescriptors(plan, into);
75
+ return into;
76
+ }
77
+ export function markStandardDevices(plan) {
78
+ const text = descriptorsOfPlan(plan);
79
+ const mark = (c, self) => {
80
+ if (c.device === true || c.expands !== false || c.denyOnly === true || c.speculative !== undefined || !OPENED_PROVENANCES.has(c.provenance))
81
+ return c;
82
+ return standardDeviceUnrebound(c.spelled, text, self) ? { ...c, device: true } : c;
83
+ };
84
+ const marked = new Map();
85
+ const walk = (p) => {
86
+ const seen = marked.get(p);
87
+ if (seen !== undefined)
88
+ return seen;
89
+ const out = markedPlan(p);
90
+ marked.set(p, out);
91
+ return out;
92
+ };
93
+ const markedPlan = (p) => ({
94
+ ...p,
95
+ segments: p.segments.map((s) => {
96
+ const useOf = (c) => {
97
+ const r = s.redirects.find((x) => x.target !== undefined && x.target.raw === c.raw && x.target.provenance === c.provenance);
98
+ return r === undefined ? [] : descriptorsBoundBy(r);
99
+ };
100
+ return {
101
+ ...s,
102
+ reads: s.reads.map((c) => mark(c, c.provenance === "stdin_redirect" ? useOf(c) : [])),
103
+ redirects: s.redirects.map((r) => (r.target === undefined ? r : { ...r, target: mark(r.target, descriptorsBoundBy(r)) })),
104
+ nested: s.nested.map(walk),
105
+ };
106
+ }),
107
+ });
108
+ return walk(plan);
109
+ }
@@ -4,6 +4,7 @@ import { boundaryIsTheSandbox } from "../../core/remote-env.js";
4
4
  import { resolveOperandSpellings } from "./bash-readonly-classifier.js";
5
5
  import { judgedCandidatesOf, sedBackupNames } from "./shell-read-write-targets.js";
6
6
  import { movedReads, withHomeViews } from "./shell-read-identity.js";
7
+ import { passesUnasked } from "./shell-read-device.js";
7
8
  const GLOB_METACHAR_RE = /[*?[]/;
8
9
  export const MAX_GLOB_DIRS = 256;
9
10
  export const MAX_GLOB_MATCHES = 8192;
@@ -569,7 +570,7 @@ function establishLegOver(seen, readRoots) {
569
570
  out.push({ ...(await canonicalReadOf(candidate, m.path, [m.path], "expansion", folded, m.canonical, signal)), node: m.node, walked: true });
570
571
  };
571
572
  for (const [i, lexical] of lexicalReadsOf(candidate, boundary).entries()) {
572
- if (lexical.established !== "lexical") {
573
+ if (lexical.established !== "lexical" || passesUnasked(candidate)) {
573
574
  out.push(writtenAt(lexical, writtenSeats, writtenSeats.written.includes("landing") ? "landing" : "entry"));
574
575
  continue;
575
576
  }
@@ -1,6 +1,7 @@
1
1
  import { hasWin32AliasedComponent, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
2
2
  import { globInWord } from "./bash-readonly-classifier.js";
3
3
  import { readOfText } from "./shell-read-word.js";
4
+ import { passesUnasked } from "./shell-read-device.js";
4
5
  import { skipsRootsContainment } from "./containment-face.js";
5
6
  export const SHELL_READ_FINDINGS = Object.freeze(["deny_hit", "outside_roots", "unestablishable", "declined", "identity_changed", "unanswered", "none"]);
6
7
  export const SHELL_READ_SEATS = Object.freeze(["full_shell", "readonly", "rejudge"]);
@@ -57,6 +58,8 @@ export function judgeRead(read, boundary) {
57
58
  const containment = containmentOf(read, boundary);
58
59
  if (read.candidate.denyOnly === true)
59
60
  return { read, kind: "none", containment };
61
+ if (passesUnasked(read.candidate))
62
+ return { read, kind: "none", containment: "open" };
60
63
  const provenance = read.candidate.provenance;
61
64
  if (provenance === "env_assignment" || (provenance === "program_path" && read.candidate.expands !== "unresolvable"))
62
65
  return { read, kind: "none", containment };
@@ -84,10 +87,8 @@ function containmentOf(read, boundary) {
84
87
  return "unknown";
85
88
  return withinAnyRoot(boundary.roots, read.target) ? "inside" : "outside";
86
89
  }
87
- const NULL_DEVICE = "/dev/null";
88
90
  function writeContainmentOf(read, boundary) {
89
- const device = read.written?.seat === "landing" && read.candidate.expands === false && read.candidate.spelled === NULL_DEVICE;
90
- if (read.written === undefined || read.candidate.denyOnly === true || device)
91
+ if (read.written === undefined || read.candidate.denyOnly === true || passesUnasked(read.candidate))
91
92
  return "open";
92
93
  const roots = boundary.writeRoots;
93
94
  if (roots === undefined || read.target === undefined)
@@ -84,6 +84,10 @@ export interface ReadCandidate {
84
84
  * word) that is a candidate at all only where it exists as a file. Where the filesystem confirms it is not there (and
85
85
  * nothing in the text writes or creates it), it is no read; a lexical establishment keeps it whole. */
86
86
  readonly speculative?: "containment" | "whole";
87
+ /** A standard device a program or redirection OPENS, its descriptor (if any) unrebound by the text ({@link markStandardDevices}). */
88
+ readonly device?: true;
89
+ /** Only beside `face: "write"`: the program writes the word's CONTENT through the path, not its ENTRY ({@link passesUnasked}). */
90
+ readonly through?: true;
87
91
  }
88
92
  /** How the shell reached one segment — what the base-candidate walk needs from the connectors. */
89
93
  export interface SegmentReach {
@@ -97,6 +101,7 @@ export interface SegmentReach {
97
101
  export interface ShellReadRedirect {
98
102
  readonly op: LexedRedirect["op"];
99
103
  readonly direction: "in" | "out" | "dup";
104
+ readonly fd?: number;
100
105
  readonly target?: ReadCandidate;
101
106
  }
102
107
  export interface ShellReadSegment {
@@ -10,6 +10,7 @@ import { shellWordsOf } from "../../shell/shell-words.js";
10
10
  import { isAbsoluteForFamily } from "./safety.js";
11
11
  import { codeBodies, launchedInput, reentryText, scriptFileAt, stdinTextsOf } from "./shell-reentry-text.js";
12
12
  import { heldAt, plannedOnce, rebasePlan, startedAt, stdinFeeds } from "./shell-read-start.js";
13
+ import { markStandardDevices } from "./shell-read-device.js";
13
14
  export const READ_CANDIDATE_PROVENANCES = Object.freeze([
14
15
  "operand",
15
16
  "option_value",
@@ -27,7 +28,8 @@ export const READ_CANDIDATE_PROVENANCES = Object.freeze([
27
28
  const NESTING_MAX = 4;
28
29
  const LOOP_END = shellWordsOf("loop_end"), LOOP_CONDITION = shellWordsOf("loop_condition"), COPROC = shellWordsOf("background"), RESERVED = shellWordsOf("rule_reserved");
29
30
  export function planShellReads(command, decl, depth = 0, start) {
30
- return planText(command, decl, depth, start).plan;
31
+ const plan = planText(command, decl, depth, start).plan;
32
+ return depth === 0 ? markStandardDevices(plan) : plan;
31
33
  }
32
34
  function planText(command, decl, depth, start, from) {
33
35
  const shape = { grouped: false, backgrounded: false, strayRedirection: false };
@@ -209,7 +211,7 @@ function planText(command, decl, depth, start, from) {
209
211
  ...(f.stdin !== undefined ? { stdin: f.stdin } : {}),
210
212
  declines: f.declines,
211
213
  reads: dedupe(f.reads).map(programRead),
212
- redirects: f.redirects.map((r) => (r.target === undefined ? { op: r.op, direction: r.direction } : { op: r.op, direction: r.direction, target: withBases(r.target) })),
214
+ redirects: f.redirects.map((r) => ({ op: r.op, direction: r.direction, ...(r.fd !== undefined ? { fd: r.fd } : {}), ...(r.target !== undefined ? { target: withBases(r.target) } : {}) })),
213
215
  ...(move !== undefined ? { baseMove: { target: typeof move.target === "string" ? move.target : withBases(move.target) } } : {}),
214
216
  bases,
215
217
  nested: f.nested.map((n) => (f.elsewhere?.has(n) === true ? n : rebasePlan(n, decl.cwd, moved && f.shellSide?.has(n) !== true ? "unmodelled" : bases, bases === "unmodelled" ? floor : bases))),
@@ -669,13 +671,14 @@ function relativeSpelling(spelled) {
669
671
  return !(isAbsoluteForFamily("own", spelled) || spelled.startsWith("~"));
670
672
  }
671
673
  function redirectFact(r, table, homeDir) {
674
+ const fd = r.fd !== undefined ? { fd: r.fd } : {};
672
675
  if (r.target === undefined)
673
- return { op: r.op, direction: "dup" };
676
+ return { op: r.op, direction: "dup", ...fd };
674
677
  if (r.op === "<")
675
- return { op: r.op, direction: "in", target: bare(readsOf([r.target], table, homeDir)[0], "stdin_redirect") };
678
+ return { op: r.op, direction: "in", ...fd, target: bare(readsOf([r.target], table, homeDir)[0], "stdin_redirect") };
676
679
  if (r.op === "<<<")
677
- return { op: r.op, direction: "in" };
678
- return { op: r.op, direction: "out", target: bare(readsOf([r.target], table, homeDir)[0], "write_redirect_target") };
680
+ return { op: r.op, direction: "in", ...fd };
681
+ return { op: r.op, direction: "out", ...fd, target: bare(readsOf([r.target], table, homeDir)[0], "write_redirect_target") };
679
682
  }
680
683
  function commandStringValue(row, toks, option) {
681
684
  const at = toks.findIndex((r) => r.folded === option);
@@ -9,6 +9,7 @@
9
9
  * program writes is its row's.
10
10
  */
11
11
  import type { ReadCandidate, ShellReadSegment } from "./shell-read-plan.js";
12
+ import { type LineDescriptors } from "./shell-read-device.js";
12
13
  import { type WriteSeat } from "./program-read-model.js";
13
14
  export type { WriteSeat } from "./program-read-model.js";
14
15
  /** A word of a program's argv: its raw spelling (the plan's candidates are keyed by it) and its folded spelling. */
@@ -46,6 +47,19 @@ export interface JudgedCandidate {
46
47
  };
47
48
  readonly onlyIfExists?: ReadCandidate;
48
49
  }
50
+ /** Is any of these words an option word (one starting with `-`)? The reference implementation's reading of a copy or a move: a
51
+ * command with flags is not one whose paths it can validate (`--target-directory`, `--remove-destination`, a backup suffix
52
+ * all change what is written where). */
53
+ export declare const anyOptionWord: (words: readonly string[]) => boolean;
54
+ /**
55
+ * Does this invocation write ONLY standard devices — what the shape tier reads as "writes nothing durable" for a PROGRAM (a
56
+ * redirection is the reader's own)? Every word any implementation's reading of the row writes ({@link writeReadings}; a word the
57
+ * reading names written at no seat, a copy's source, is read) is one the program writes THROUGH ({@link writesThrough}) and
58
+ * is a standard device no redirection of the line rebinds ({@link standardDeviceUnrebound}) — the station's own predicate
59
+ * (`passesUnasked`) over the same row facts, for the one entry a text-only reader has: `tee /dev/tty`, `cp a.txt /dev/stdout`.
60
+ * No program is named here: which programs may ask this is the caller's.
61
+ */
62
+ export declare function writesOnlyStandardDevices(program: string, args: readonly string[], line: LineDescriptors): boolean;
49
63
  /** Every candidate a segment hands the judge: its reads minus the write targets, plus its `cd` target (the plan already
50
64
  * lists it among the reads), plus its WRITE targets for the write face ({@link writeTargetsOf}), plus the deny-floor
51
65
  * shadow of every unplaced home word ({@link denyFloorShadowsOf}) on both faces. A word the program only writes keeps
@@ -1,6 +1,7 @@
1
1
  import { denyFloorShadowsOf } from "./shell-read-judge.js";
2
+ import { GENERATOR_DEVICES, OPENED_PROVENANCES, passesUnasked, standardDeviceUnrebound } from "./shell-read-device.js";
2
3
  import { renamed } from "./shell-read-word.js";
3
- import { implementationsOf, programRow, selectedOptions, writeReadings } from "./program-read-model.js";
4
+ import { byteBoundIsSmall, implementationsOf, programRow, readArgRoles, selectedOptions, subcommandOf, writeReadings } from "./program-read-model.js";
4
5
  import { programBasename, readProgramPosition } from "./bash-program-position.js";
5
6
  export function programArgs(segment) {
6
7
  const words = segment.argv.map((w) => ({ raw: w.raw, spelled: w.folded }));
@@ -11,6 +12,25 @@ export function sedBackupNames(name, suffix) {
11
12
  const appended = `${name}${suffix}`;
12
13
  return suffix.includes("*") ? [suffix.replace(/\*/g, () => name), appended] : [appended];
13
14
  }
15
+ export const anyOptionWord = (words) => words.some((w) => w.startsWith("-"));
16
+ function writesThrough(roles, row, words) {
17
+ const args = words.map((w) => w.spelled);
18
+ if (roles.length === 0 || readArgRoles(row, args).some((e) => e.kind === "option" && e.reading === "speculative"))
19
+ return false;
20
+ return roles.every((role) => role.through === true || (role.through === "plain" && !anyOptionWord(args)));
21
+ }
22
+ export function writesOnlyStandardDevices(program, args, line) {
23
+ const row = programRow(program);
24
+ if (row === undefined)
25
+ return false;
26
+ const words = args.map((a) => ({ raw: a, spelled: a }));
27
+ const readings = writeReadings(row, [...args]);
28
+ return (readings.length > 0 &&
29
+ readings.every((r) => {
30
+ const written = r.written.filter((w) => w.role.writtenAt?.length !== 0);
31
+ return !r.unexplained && written.length > 0 && writesThrough(written.map((w) => w.role), row, words) && written.every((w) => standardDeviceUnrebound(w.value ?? args[w.at], line, []));
32
+ }));
33
+ }
14
34
  const WORD_PROVENANCES = new Set(["operand", "unlisted_word", "option_value"]);
15
35
  function writeOwnersOf(segment) {
16
36
  const pos = readProgramPosition(segment.argv);
@@ -30,18 +50,19 @@ function writeOwnersOf(segment) {
30
50
  function writeTargetsOf(segment) {
31
51
  const out = [];
32
52
  const writeOnly = new Set();
33
- const write = (c, seats, written, child, onlyIfExists) => void out.push({ candidate: { ...c, face: "write" }, ...(seats !== undefined ? { seats } : {}), ...(written !== undefined ? { written } : {}), ...(child === true ? { child } : {}), ...(onlyIfExists !== undefined ? { onlyIfExists } : {}) });
53
+ const write = (c, seats, written, child, onlyIfExists, through) => void out.push({ candidate: { ...c, face: "write", ...(through === true ? { through } : {}) }, ...(seats !== undefined ? { seats } : {}), ...(written !== undefined ? { written } : {}), ...(child === true ? { child } : {}), ...(onlyIfExists !== undefined ? { onlyIfExists } : {}) });
34
54
  for (const r of segment.redirects)
35
55
  if (r.direction === "out" && r.target !== undefined)
36
- write(r.target);
56
+ write(r.target, undefined, undefined, undefined, undefined, true);
37
57
  for (const c of segment.reads)
38
58
  if (c.provenance === "write_redirect_target")
39
- write(c);
59
+ write(c, undefined, undefined, undefined, undefined, true);
40
60
  const owners = writeOwnersOf(segment).map(({ row, words, program }) => {
41
61
  const args = words.map((w) => w.spelled);
42
62
  const all = writeReadings(row, args);
43
- const moved = [...new Set(implementationsOf(row).flatMap((view) => selectedOptions(view, args, "one-implementation")).flatMap((o) => o.writesAt ?? []))];
44
- return { words, program, all, readings: all.filter((r) => r.written.length > 0 || (r.unexplained && r.operandRole !== undefined)), override: moved.length > 0 ? moved : undefined };
63
+ const selected = implementationsOf(row).flatMap((view) => selectedOptions(view, args, "one-implementation"));
64
+ const moved = [...new Set(selected.flatMap((o) => o.writesAt ?? []))];
65
+ return { row, words, program, all, readings: all.filter((r) => r.written.length > 0 || (r.unexplained && r.operandRole !== undefined)), override: moved.length > 0 ? moved : undefined, beyondOperands: selected.some((o) => o.effect === "writes" || o.effect === "executes") };
45
66
  });
46
67
  const edited = new Map();
47
68
  for (const c of segment.reads) {
@@ -77,7 +98,7 @@ function writeTargetsOf(segment) {
77
98
  const seats = stated ? [...new Set(roles.flatMap((role) => o.override ?? role.seats ?? ["landing"]))] : undefined;
78
99
  const written = unexplained || roles.some((role) => role.writtenAt === undefined) ? undefined : [...new Set(roles.flatMap((role) => role.writtenAt ?? []))];
79
100
  const speculative = readings < o.all.length && roles.every((role) => role.effect === "edit-in-place") ? c : undefined;
80
- write(c, seats, written, undefined, speculative);
101
+ write(c, seats, written, undefined, speculative, !unexplained && writesThrough(roles, o.row, o.words) ? true : undefined);
81
102
  if (roles.some((role) => role.effect === "edit-in-place") && c.expands === false)
82
103
  edited.set(o, [...(edited.get(o) ?? []), { candidate: c, ...(speculative !== undefined ? { onlyIfExists: speculative } : {}) }]);
83
104
  }
@@ -85,11 +106,25 @@ function writeTargetsOf(segment) {
85
106
  writeOnly.add(c);
86
107
  }
87
108
  for (const o of owners) {
88
- for (const c of copyDestinationChildren(segment, o.words, o.readings))
109
+ for (const c of copyDestinationChildren(segment, o.row, o.words, o.readings))
89
110
  write(c, ["landing", "entry"], undefined, true);
90
111
  out.push(...backupTargetsOf(edited.get(o) ?? [], o.readings, o.override));
91
112
  }
92
- return { targets: out, writeOnly };
113
+ const readWords = new Set();
114
+ for (const o of owners) {
115
+ if (!o.program)
116
+ continue;
117
+ const args = o.words.map((w) => w.spelled);
118
+ const found = subcommandOf(o.row, args);
119
+ const sub = found !== undefined && found.row.operands !== undefined ? found : undefined;
120
+ const at = sub === undefined ? 0 : sub.at + 1;
121
+ for (const e of readArgRoles(sub?.row ?? o.row, args.slice(at))) {
122
+ const role = e.kind === "operand" || e.kind === "value" ? e.role : undefined;
123
+ if (role?.kind === "path" && (role.effect === "read" || role.writtenAt?.length === 0))
124
+ readWords.add(o.words[at + e.at].raw);
125
+ }
126
+ }
127
+ return { targets: out, writeOnly, beyondOperands: owners.some((o) => o.beyondOperands), boundedRead: owners.some((o) => o.program && byteBoundIsSmall(o.row, o.words.map((w) => w.spelled))), readWords };
93
128
  }
94
129
  function backupTargetsOf(edited, readings, override) {
95
130
  const suffixes = [...new Set(readings.flatMap((r) => r.suffixes))];
@@ -98,7 +133,7 @@ function backupTargetsOf(edited, readings, override) {
98
133
  ? [{ candidate: { ...renamed(f, `${f.spelled}${suffix}`), face: "write" }, seats: ["entry"], followed: { operand: f, suffix }, ...(onlyIfExists !== undefined ? { onlyIfExists } : {}) }]
99
134
  : sedBackupNames(f.spelled, suffix).map((spelled) => ({ candidate: { ...renamed(f, spelled), face: "write" }, seats: ["entry"], ...(onlyIfExists !== undefined ? { onlyIfExists } : {}) }))));
100
135
  }
101
- function copyDestinationChildren(segment, words, readings) {
136
+ function copyDestinationChildren(segment, row, words, readings) {
102
137
  const candidateOf = (raw) => segment.reads.find((c) => c.raw === raw && WORD_PROVENANCES.has(c.provenance));
103
138
  const children = new Map();
104
139
  for (const r of readings) {
@@ -109,6 +144,9 @@ function copyDestinationChildren(segment, words, readings) {
109
144
  const destCandidate = candidateOf(dest.raw);
110
145
  if (destCandidate !== undefined && destCandidate.expands !== false)
111
146
  continue;
147
+ const destRole = r.written.find((w) => w.at === r.copy.destination.at)?.role;
148
+ if (destCandidate !== undefined && destRole !== undefined && passesUnasked({ ...destCandidate, face: "write", ...(writesThrough([destRole], row, words) ? { through: true } : {}) }))
149
+ continue;
112
150
  for (const at of r.copy.sources) {
113
151
  const src = words[at];
114
152
  const source = candidateOf(src.raw);
@@ -123,8 +161,21 @@ function copyDestinationChildren(segment, words, readings) {
123
161
  return [...children.values()];
124
162
  }
125
163
  export function judgedCandidatesOf(segment) {
126
- const { targets: writes, writeOnly } = writeTargetsOf(segment);
127
- const reads = segment.reads.flatMap((c) => (c.provenance === "write_redirect_target" ? [] : [writeOnly.has(c) ? { ...c, denyOnly: true } : c]));
164
+ const { targets: writes, writeOnly, beyondOperands, boundedRead, readWords } = writeTargetsOf(segment);
165
+ const throughWritten = new Set(writes.filter((w) => w.candidate.through === true && w.candidate.device === true).map((w) => w.candidate.raw));
166
+ const opened = (c) => {
167
+ if (c.provenance === "unlisted_word" && c.device === true && !readWords.has(c.raw) && !throughWritten.has(c.raw)) {
168
+ const { device: _unread, ...judged } = c;
169
+ return judged;
170
+ }
171
+ if (boundedRead && c.device !== true && c.expands === false && c.speculative === undefined && c.denyOnly !== true && GENERATOR_DEVICES.has(c.spelled) && OPENED_PROVENANCES.has(c.provenance) && (c.provenance !== "unlisted_word" || readWords.has(c.raw)))
172
+ return { ...c, device: true };
173
+ if (!beyondOperands || c.device !== true || c.provenance === "stdin_redirect")
174
+ return c;
175
+ const { device: _device, ...judged } = c;
176
+ return judged;
177
+ };
178
+ const reads = segment.reads.flatMap((c) => (c.provenance === "write_redirect_target" ? [] : [opened(writeOnly.has(c) ? { ...c, denyOnly: true } : c)]));
128
179
  const plain = (cs) => cs.map((candidate) => ({ candidate }));
129
180
  return [...plain(reads), ...plain(denyFloorShadowsOf(reads)), ...writes, ...plain(denyFloorShadowsOf(writes.map((w) => w.candidate)))];
130
181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.49.2",
3
+ "version": "7.49.4",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -0,0 +1,142 @@
1
+ {
2
+ "repo": "core",
3
+ "version": "7.49.3",
4
+ "from": "f1c55b07b7533fc1afdb428c6148d87e44744c2b",
5
+ "to": "2a6b9343850cb0c77213c7c9d8b52619b6a24148",
6
+ "added": [],
7
+ "removed": [],
8
+ "changed": [],
9
+ "narrowed": [],
10
+ "closedSetMembers": [],
11
+ "closedSetsUnreadable": [],
12
+ "typeKeys": [
13
+ {
14
+ "type": "ReadCandidate",
15
+ "at": [
16
+ "src/tools/fs/shell-read-plan.ts:140"
17
+ ],
18
+ "added": [
19
+ "device",
20
+ "through"
21
+ ],
22
+ "addedDetail": [
23
+ {
24
+ "key": "device",
25
+ "optional": true
26
+ },
27
+ {
28
+ "key": "through",
29
+ "optional": true
30
+ }
31
+ ],
32
+ "addedRequired": [],
33
+ "removed": [],
34
+ "breaking": false,
35
+ "criteriaCovered": false
36
+ },
37
+ {
38
+ "type": "ShellReadRedirect",
39
+ "at": [
40
+ "src/tools/fs/shell-read-plan.ts:189"
41
+ ],
42
+ "via": "ShellReadSegment.redirects",
43
+ "added": [
44
+ "fd"
45
+ ],
46
+ "addedDetail": [
47
+ {
48
+ "key": "fd",
49
+ "optional": true
50
+ }
51
+ ],
52
+ "addedRequired": [],
53
+ "removed": [],
54
+ "breaking": false,
55
+ "criteriaCovered": false
56
+ }
57
+ ],
58
+ "typeKeysUnreadableArms": [],
59
+ "typeKeysFlattenedCount": 223,
60
+ "signatures": [
61
+ {
62
+ "name": "readOnlyShellVerdict",
63
+ "at": [
64
+ "src/core/read-only-shell.ts:111"
65
+ ],
66
+ "from": [
67
+ "(command: string, shape?: ShellCommandShape, cwd?: string): ReadOnlyShellVerdict"
68
+ ],
69
+ "to": [
70
+ "(command: string, shape?: ShellCommandShape, cwd?: string, bound?: (() => LineDescriptors) | LineDescriptors): ReadOnlyShellVerdict"
71
+ ],
72
+ "changes": [
73
+ {
74
+ "aspect": "param",
75
+ "detail": "+optional param `bound?: (() => LineDescriptors) | LineDescriptors`",
76
+ "breaking": false
77
+ }
78
+ ],
79
+ "breaking": false,
80
+ "criteriaCovered": false
81
+ }
82
+ ],
83
+ "declarationMoves": [],
84
+ "mintMoves": [],
85
+ "criteriaObserves": [
86
+ {
87
+ "kind": "typeMember",
88
+ "name": "AskRequest.classes",
89
+ "assertions": [
90
+ "G1.a",
91
+ "G2.a",
92
+ "G7.a"
93
+ ]
94
+ },
95
+ {
96
+ "kind": "typeMember",
97
+ "name": "AskRequest.mandated",
98
+ "assertions": [
99
+ "G1.a",
100
+ "G7.a",
101
+ "G8.a"
102
+ ]
103
+ },
104
+ {
105
+ "kind": "typeMember",
106
+ "name": "AskRequest.requiresRealApproval",
107
+ "assertions": [
108
+ "G8.a"
109
+ ]
110
+ },
111
+ {
112
+ "kind": "typeMember",
113
+ "name": "RedactionReport.findings",
114
+ "assertions": [
115
+ "G4.a",
116
+ "G5.a",
117
+ "G6.a"
118
+ ]
119
+ },
120
+ {
121
+ "kind": "typeMember",
122
+ "name": "ShellReadVerdict.finding",
123
+ "assertions": [
124
+ "G3.a"
125
+ ]
126
+ }
127
+ ],
128
+ "consumersHint": {
129
+ "cli": [
130
+ "ReadCandidate",
131
+ "readOnlyShellVerdict"
132
+ ],
133
+ "server": [
134
+ "ReadCandidate",
135
+ "readOnlyShellVerdict"
136
+ ],
137
+ "sdk": [],
138
+ "note": "scanned cli@d23f3258 server@1922afc1 sdk@f1b30d4 — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
139
+ },
140
+ "breakingCount": 0,
141
+ "breakingItems": []
142
+ }