@sema-agent/core 7.27.0 → 7.28.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/agents/builtin-agents.d.ts +6 -15
  3. package/dist/agents/cascade.d.ts +29 -33
  4. package/dist/agents/observer.d.ts +18 -20
  5. package/dist/agents/subagent.d.ts +58 -70
  6. package/dist/agents/tool-filter.d.ts +10 -12
  7. package/dist/agents/verify.d.ts +44 -49
  8. package/dist/brain/input-too-long.d.ts +10 -12
  9. package/dist/brain/media-degrade.d.ts +4 -5
  10. package/dist/brain/reasoning.d.ts +10 -12
  11. package/dist/brain/repetition.d.ts +7 -9
  12. package/dist/brain/request-params.d.ts +20 -25
  13. package/dist/brain/route-adjudicator.d.ts +4 -4
  14. package/dist/brain/route-conformance.d.ts +2 -2
  15. package/dist/brain/status-sink.d.ts +12 -14
  16. package/dist/brain/terminal-cause.d.ts +12 -15
  17. package/dist/brain/timeout.d.ts +17 -19
  18. package/dist/core/memory-engine/consolidation.d.ts +11 -15
  19. package/dist/core/memory-engine/engine.d.ts +45 -54
  20. package/dist/core/memory-engine/tools.d.ts +17 -23
  21. package/dist/core/permission-rule-model.js +5 -3
  22. package/dist/shell/lex.d.ts +279 -0
  23. package/dist/shell/lex.js +1253 -0
  24. package/dist/tools/fs/bash-program-position.d.ts +2 -4
  25. package/dist/tools/fs/bash-program-position.js +2 -1
  26. package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -2
  27. package/dist/tools/fs/bash-readonly-classifier.js +1 -1
  28. package/dist/tools/fs/shell-read-check.js +1 -1
  29. package/dist/tools/fs/shell-read-establish.js +11 -1
  30. package/dist/tools/fs/shell-read-plan.d.ts +7 -2
  31. package/dist/tools/fs/shell-read-plan.js +48 -10
  32. package/package.json +2 -2
  33. package/wire-changes/core@7.28.1.json +594 -0
  34. package/dist/tools/fs/bash-lexer.d.ts +0 -152
  35. package/dist/tools/fs/bash-lexer.js +0 -627
@@ -8,7 +8,7 @@
8
8
  * readable, minted into one sentence by {@link programPositionRefusalSentence}. Reading less than this reader
9
9
  * does (an option skipped, a chain member missed) is how one face used to vouch for a program another refused.
10
10
  */
11
- import type { SegmentTokens } from "./bash-lexer.js";
11
+ import { SHELL_CONTROL_PREFIXES, type SegmentTokens } from "../../shell/lex.js";
12
12
  /** The verdict every unresolvable / out-of-root / unbounded finding ends on. The findings differ in what
13
13
  * they found; the consequence is one consequence, and a copy that drifts reads as a second rule. */
14
14
  export declare const NOT_AUTO_ALLOWED = "\u2014 not auto-allowed";
@@ -35,9 +35,7 @@ export declare function writtenAssignmentPrefix(token: string): {
35
35
  readonly appends: boolean;
36
36
  readonly settlesWholeName: boolean;
37
37
  } | undefined;
38
- /** Shell control words that stand BEFORE a command: transparent for the boundary walk (the command behind them is
39
- * judged). */
40
- export declare const SHELL_CONTROL_PREFIXES: ReadonlySet<string>;
38
+ export { SHELL_CONTROL_PREFIXES };
41
39
  /** Shell block words that name no command of their own (a loop head, a block close, a case head). */
42
40
  export declare const SHELL_BLOCK_WORDS: ReadonlySet<string>;
43
41
  /** A control word is a keyword only when bash sees it BARE: `'if'` / `"do"` / `\\!` are command names (a program
@@ -1,3 +1,4 @@
1
+ import { SHELL_CONTROL_PREFIXES } from "../../shell/lex.js";
1
2
  export const NOT_AUTO_ALLOWED = "— not auto-allowed";
2
3
  export function programBasename(word) {
3
4
  return word.includes("/") ? (word.split("/").pop() ?? word) : word;
@@ -12,7 +13,7 @@ export function writtenAssignmentPrefix(token) {
12
13
  const bracket = written.indexOf("[");
13
14
  return { prefix, name: bracket < 0 ? written : written.slice(0, bracket), appends, settlesWholeName: !appends && bracket < 0 };
14
15
  }
15
- export const SHELL_CONTROL_PREFIXES = new Set(["!", "if", "then", "elif", "else", "while", "until", "do", "time"]);
16
+ export { SHELL_CONTROL_PREFIXES };
16
17
  export const SHELL_BLOCK_WORDS = new Set(["for", "select", "case", "esac", "fi", "done", "function", "coproc", "[[", "]]", "in"]);
17
18
  export function isBareControlWord(toks, i, words) {
18
19
  const word = toks.folded[i];
@@ -1,4 +1,4 @@
1
- import { type SegmentTokens, type ShellSegmentConnector } from "./bash-lexer.js";
1
+ import { type SegmentTokens, type ShellSegmentConnector } from "../../shell/lex.js";
2
2
  /**
3
3
  * Bare command names `bash_readonly` permits out of the box, ALSO the default reversible set for the
4
4
  * The {@link import("./fs-bash.js").bashReversibilityProbe} classifier. **Coarse first filter, NOT a security
@@ -31,7 +31,7 @@ import { type SegmentTokens, type ShellSegmentConnector } from "./bash-lexer.js"
31
31
  * value-taking options defeat the stdin-floor operand count without a per-verb option-value model
32
32
  * — those are a follow-on slice, not a silent drop.
33
33
  */
34
- export type { ShellSegmentConnector } from "./bash-lexer.js";
34
+ export type { ShellSegmentConnector } from "../../shell/lex.js";
35
35
  export declare const BASH_READONLY_DEFAULT_ALLOW: readonly string[];
36
36
  /**
37
37
  * The verbs the CLASSIFY face admits ON TOP of the shared list ({@link bashClassifyDefaultAllow}) —
@@ -1,5 +1,5 @@
1
1
  import { isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf } from "./safety.js";
2
- import { lexShellText, literalTildeSpelling, ansiCQuoteClose, dollarOpensQuote, quoteMask, wordsOf, } from "./bash-lexer.js";
2
+ import { lexShellText, literalTildeSpelling, ansiCQuoteClose, dollarOpensQuote, quoteMask, wordsOf, } from "../../shell/lex.js";
3
3
  import { COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programPositionRefusalSentence, ASSIGNMENT_PREFIX, readProgramPosition, writtenAssignmentPrefix, } from "./bash-program-position.js";
4
4
  export const BASH_READONLY_DEFAULT_ALLOW = [
5
5
  "ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
@@ -8,7 +8,7 @@ import { planShellReads } from "./shell-read-plan.js";
8
8
  import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
9
9
  import { allFindings, decideShellRead, foldShellReadFindings, judgeEstablishedPlan, readRootGrantCandidateOf, } from "./shell-read-judge.js";
10
10
  import { normalizeReadRootCandidate } from "../../core/read-root-candidate.js";
11
- import { wordsOf, lexShellText } from "./bash-lexer.js";
11
+ import { wordsOf, lexShellText } from "../../shell/lex.js";
12
12
  function containmentDetails(paths) {
13
13
  return { type: "readonly_out_of_root", code: "readonly_out_of_root", paths };
14
14
  }
@@ -98,7 +98,17 @@ function lexicalReadsOf(candidate, boundary) {
98
98
  }
99
99
  const bases = basesFor(candidate, boundary);
100
100
  if (bases === "unmodelled") {
101
- return [{ candidate, established: "unestablishable", spellings: [candidate.spelled], cause: `"${candidate.raw}" is relative to a working directory this command's text does not settle` }];
101
+ const floors = candidate.floorBases ?? [boundary.cwd ?? boundary.roots[0] ?? ""];
102
+ const spellings = [candidate.spelled];
103
+ for (const floorBase of floors) {
104
+ const viaFloor = floorBase === "" ? undefined : resolveOperandSpellings(floorBase, candidate.spelled, boundary.homeDir);
105
+ if (viaFloor === undefined)
106
+ continue;
107
+ spellings.push(viaFloor.folded);
108
+ if (viaFloor.spelled !== undefined)
109
+ spellings.push(viaFloor.spelled);
110
+ }
111
+ return [{ candidate, established: "unestablishable", spellings, cause: `"${candidate.raw}" is relative to a working directory this command's text does not settle` }];
102
112
  }
103
113
  const out = [];
104
114
  const seen = new Set();
@@ -4,7 +4,7 @@
4
4
  * The read boundary today re-derives "which words may be read" at several sites (the compound walk, the
5
5
  * containment-only gate, the refused-command whitelist, the execute-time recheck), each with its own idea
6
6
  * of launchers, option slots, redirections and re-entry. This module is the single extraction those sites
7
- * are to consume: a pure, zero-I/O function over the lenient lexer (`bash-lexer.ts`) and the program-position
7
+ * are to consume: a pure, zero-I/O function over the lenient lexer (`src/shell/lex.ts`) and the program-position
8
8
  * reader (`bash-program-position.ts`), returning every path word a command MAY read, each tagged with its
9
9
  * PROVENANCE (why it is a candidate) and the working-directory candidates it is relative to. Establishment
10
10
  * (`shell-read-establish.ts`) and judgment are downstream; nothing here decides allow/ask/deny.
@@ -26,7 +26,7 @@
26
26
  * otherwise loses the base for what follows.
27
27
  * Every ambiguity resolves toward MORE candidates, never fewer.
28
28
  */
29
- import { type LenientConnector, type LexedRedirect, type LexedWord } from "./bash-lexer.js";
29
+ import { type LenientConnector, type LexedRedirect, type LexedWord } from "../../shell/lex.js";
30
30
  import { type ShellScanDecline } from "./bash-program-position.js";
31
31
  /** WHY a word is a read candidate — a closed set, registered in `docs/CLOSED-SETS.md`. */
32
32
  export declare const READ_CANDIDATE_PROVENANCES: readonly ["operand", "option_value", "stdin_redirect", "write_redirect_target", "env_assignment", "unlisted_word", "traversal_root", "cd_target", "program_path", "script_slot_file"];
@@ -55,6 +55,11 @@ export interface ReadCandidate {
55
55
  * EMPTY for an absolute or home-rooted spelling; `"unmodelled"` for a relative word in a segment whose base the
56
56
  * text does not settle (an unfollowable move before it, a loop this parser cannot count) — it cannot be placed. */
57
57
  readonly bases: readonly string[] | "unmodelled";
58
+ /** Only beside `bases: "unmodelled"`: the working directories the text was certainly standing in when it LOST
59
+ * track (the reachable candidate set at the unprovable move; the declared cwd when nothing had moved yet).
60
+ * A deny row is a floor, so the judge resolves the spelling against these for deny rows ONLY — never to
61
+ * establish a read. `cd sub && cd $'\x2e' && cat <deny row under sub>` (bash: `cd .`) is refused through it. */
62
+ readonly floorBases?: readonly string[];
58
63
  }
59
64
  /** How the shell reached one segment — what the base-candidate walk needs from the connectors. */
60
65
  export interface SegmentReach {
@@ -1,4 +1,4 @@
1
- import { lexShellText, substitutionsInHeredocBody, wordsOf, literalTildeSpelling, SUBSTITUTION_PLACEHOLDER, } from "./bash-lexer.js";
1
+ import { lexShell, PARSE_FAILURE_SENTENCE, UNRESOLVED_SENTENCE, UNRESOLVED_VIEW, substitutionsInHeredocBody, wordsOf, literalTildeSpelling, SUBSTITUTION_PLACEHOLDER, } from "../../shell/lex.js";
2
2
  import { LAUNCHER_TABLE, SHELL_BLOCK_WORDS, programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, writtenAssignmentPrefix, } from "./bash-program-position.js";
3
3
  import { ASSIGNMENT_VALUE_NOT_PATHS, INTERPRETER_INLINE_PROGRAMS, NO_PATH_OPERAND_COMMANDS, POLL_LOOP_MAX_BEATS, RECURSIVE_READ_FORMS, SHELL_REENTRY_PROGRAM_RE, UNMODELLED_BASE_MOVERS, argvProgramOf, assignmentWordPayload, attachedOptionPayloads, foldHomeVariableSpelling, resolveOperandLexically, hasUnquotedExpansionMetachar, hasUnquotedGlobMetachar, inlineProgramClause, interpreterKey, isPathShapedToken, readVerbOptionSlot, sedScriptSlotFiles, segmentSelectsRecursiveRead, slotValueCandidates, suppliesFirstOperandSlot, verbFirstOperandSlot, verbOptionsArePositionIndependent, } from "./bash-readonly-classifier.js";
4
4
  import { isAbsolutePathForm } from "./safety.js";
@@ -16,14 +16,25 @@ export const READ_CANDIDATE_PROVENANCES = [
16
16
  ];
17
17
  const NESTING_MAX = 4;
18
18
  const BASE_CANDIDATE_MAX = 256;
19
+ function firstRefusedUnresolvedSentence(lexed) {
20
+ for (const segment of lexed.segments) {
21
+ for (const token of segment.tokens) {
22
+ const word = token.kind === "word" ? token : token.kind === "redirect" ? token.target : undefined;
23
+ if (word?.unresolved !== undefined && UNRESOLVED_VIEW[word.unresolved] === "refuse")
24
+ return UNRESOLVED_SENTENCE[word.unresolved];
25
+ }
26
+ }
27
+ return undefined;
28
+ }
19
29
  export function planShellReads(command, decl, depth = 0) {
20
30
  const shape = { grouped: false, backgrounded: false, strayRedirection: false };
21
31
  if (depth > NESTING_MAX)
22
32
  return { segments: [], unreadable: `command text nested more than ${NESTING_MAX} levels deep`, shape };
23
33
  const homeReassigned = /(^|[\s;&|(){}`])(?:export\s+)?HOME\+?=/.test(command);
24
- const lexed = lexShellText(foldHomeVariableSpelling(command, homeReassigned ? undefined : decl.homeDir), { mode: "lenient" });
25
- if ("reject" in lexed)
26
- return { segments: [], unreadable: lexed.reject, shape };
34
+ const lexed = lexShell(foldHomeVariableSpelling(command, homeReassigned ? undefined : decl.homeDir), { mode: "lenient" });
35
+ if ("parseFailure" in lexed)
36
+ return { segments: [], unreadable: PARSE_FAILURE_SENTENCE[lexed.parseFailure.reason], shape };
37
+ const unreadable = firstRefusedUnresolvedSentence(lexed);
27
38
  const nestedPlan = (text) => planShellReads(text, decl, depth + 1);
28
39
  const projected = lexed.segments.map((segment) => projectSegment(segment));
29
40
  const facts = projected.map((p, si) => segmentFacts(lexed.segments[si], p, decl, nestedPlan));
@@ -34,6 +45,11 @@ export function planShellReads(command, decl, depth = 0) {
34
45
  const unmodelled = facts.map(() => false);
35
46
  let candidates = [{ base: decl.cwd, skippedCd: -1 }];
36
47
  let baseLost = false;
48
+ let floorBases;
49
+ const loseBase = (reachable) => {
50
+ baseLost = true;
51
+ floorBases ??= [...new Set(reachable.map((c) => c.base))];
52
+ };
37
53
  let andRunStart = 0;
38
54
  let crossedAllAnd = true;
39
55
  let crossedAnyOr = false;
@@ -68,7 +84,7 @@ export function planShellReads(command, decl, depth = 0) {
68
84
  if (move === undefined || baseLost)
69
85
  return;
70
86
  if (typeof move.target === "string" || move.target.expands !== false) {
71
- baseLost = true;
87
+ loseBase(reachable);
72
88
  return;
73
89
  }
74
90
  const target = move.target.spelled;
@@ -78,7 +94,7 @@ export function planShellReads(command, decl, depth = 0) {
78
94
  if (c.skippedCd < andRunStart) {
79
95
  const moved = joinBase(c.base, target, decl.homeDir);
80
96
  if (moved === undefined) {
81
- baseLost = true;
97
+ loseBase(reachable);
82
98
  return;
83
99
  }
84
100
  branched.push({ base: moved, skippedCd: c.skippedCd });
@@ -93,7 +109,7 @@ export function planShellReads(command, decl, depth = 0) {
93
109
  }
94
110
  const next = [...byBase.values()];
95
111
  if (next.length > BASE_CANDIDATE_MAX)
96
- baseLost = true;
112
+ loseBase(reachable);
97
113
  else
98
114
  candidates = next;
99
115
  };
@@ -116,7 +132,7 @@ export function planShellReads(command, decl, depth = 0) {
116
132
  if (frame.beats === undefined) {
117
133
  for (let k = frame.bodyStart; k < si; k++)
118
134
  unmodelled[k] = true;
119
- baseLost = true;
135
+ loseBase(candidates.filter((c) => c.skippedCd < andRunStart));
120
136
  continue;
121
137
  }
122
138
  for (let beat = 1; beat < frame.beats && !baseLost; beat++) {
@@ -133,7 +149,11 @@ export function planShellReads(command, decl, depth = 0) {
133
149
  if (f.empty)
134
150
  continue;
135
151
  const bases = unmodelled[si] ? "unmodelled" : [...basesOf[si]];
136
- const withBases = (c) => ({ ...c, bases: relativeSpelling(c.spelled) ? bases : [] });
152
+ const floor = unmodelled[si] && floorBases !== undefined && floorBases.length > 0 ? floorBases : undefined;
153
+ const withBases = (c) => {
154
+ const relative = relativeSpelling(c.spelled);
155
+ return { ...c, bases: relative ? bases : [], ...(relative && floor !== undefined ? { floorBases: floor } : {}) };
156
+ };
137
157
  segments.push({
138
158
  text: f.text,
139
159
  connector: f.connector,
@@ -151,7 +171,7 @@ export function planShellReads(command, decl, depth = 0) {
151
171
  reach: reaches[si] ?? { andJoined: false, orBroken: false, moveProvable: false },
152
172
  });
153
173
  }
154
- return { segments, shape };
174
+ return { segments, shape, ...(unreadable !== undefined ? { unreadable } : {}) };
155
175
  }
156
176
  function projectSegment(segment) {
157
177
  const tokens = [...segment.tokens];
@@ -178,6 +198,24 @@ function projectSegment(segment) {
178
198
  };
179
199
  }
180
200
  function segmentFacts(segment, p, decl, nestedPlan) {
201
+ const facts = segmentFactsRead(segment, p, decl, nestedPlan);
202
+ const unresolvedRaw = new Set();
203
+ for (const t of segment.tokens) {
204
+ const w = t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined;
205
+ if (w?.unresolved !== undefined && UNRESOLVED_VIEW[w.unresolved] === "refuse")
206
+ unresolvedRaw.add(w.raw);
207
+ }
208
+ if (unresolvedRaw.size === 0)
209
+ return facts;
210
+ const unsettle = (c) => (unresolvedRaw.has(c.raw) && c.expands !== "unresolvable" ? { ...c, expands: "unresolvable" } : c);
211
+ const move = facts.baseMove;
212
+ return {
213
+ ...facts,
214
+ reads: facts.reads.map(unsettle),
215
+ ...(move !== undefined && typeof move.target !== "string" ? { baseMove: { target: unsettle(move.target) } } : {}),
216
+ };
217
+ }
218
+ function segmentFactsRead(segment, p, decl, nestedPlan) {
181
219
  const nested = [];
182
220
  for (const body of segment.substitutions)
183
221
  nested.push(nestedPlan(body));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.27.0",
3
+ "version": "7.28.1",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -52,7 +52,7 @@
52
52
  "pretest": "npm run gate:debts",
53
53
  "test": "vitest run --exclude \"**/*.local.test.ts\"",
54
54
  "test:watch": "vitest",
55
- "gate:skips": "vitest run --exclude \"**/node_modules/**\" --exclude \"**/*.local.test.ts\" --reporter=default --reporter=json --outputFile.json=.skip-report.json && node scripts/verify-skip-baseline.mjs --report .skip-report.json",
55
+ "gate:skips": "vitest run --exclude \"**/node_modules/**\" --exclude \"**/*.local.test.ts\" --reporter=default --reporter=json --outputFile.json=.skip-report.json && node scripts/verify-skip-baseline.mjs --report .skip-report.json --full",
56
56
  "gate:live": "node scripts/run-live-gate.mjs",
57
57
  "smoke": "tsx src/examples/smoke.ts",
58
58
  "eval": "tsx eval/run.ts",