@sema-agent/core 7.23.2 → 7.23.3

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 CHANGED
@@ -1,3 +1,21 @@
1
+ # Changelog
2
+
3
+ ## 7.23.3 — 2026-09-19
4
+
5
+ ### Read boundary — an operand the shell EXPANDS is judged by the deployment's deny judge, at the one moment the expansion exists (#934; P1; execute-time recheck; @cli @server @client-core)
6
+ - **Before.** Under `shellGate:"classify"` + `readFace:"roots"` with a deny table wired, `cat .en?` cost ZERO cards and returned `.env`'s bytes; so did `cat secrets*/app.txt` and `grep . secret?/app.txt`, in the headless and the wired-approver deployment alike. Three passes each declined, and each was right to: the classify walk judges an unexpanded glob on its UNEXPANDED spelling (`<root>/.en?` is on no deny table) and parks it as `undecidedPaths`; the read-only command tables then cleared that ordinary card (`cat` is a listed reader; the table knows nothing about paths); and the execute-time canonical recheck (#775) — the one pass that HOLDS a filesystem and does expand the pattern — asked only "does this land outside the roots?". **A deny-listed expansion is in-root by definition**, so the containment question can never be the one that catches it. Not a 7.23.1 regression: the same reading since 7.22.x.
7
+ - **After — one rule: an EXPANSION IS AN OPERAND.** The recheck leg hands every path it establishes to `BashReadonlyRootBoundary.denyMatch` — the SAME function object the approval-time walk holds, forwarded, never a second matcher — and asks it BEFORE containment, the order the walk itself applies (deny outranks; no root grant clears a deny row). Rules: ±0 judges, −1 drop. The expander's property-6 shortcut used to DELETE an in-root non-symlink match outright; what that shortcut buys is the `realpath` call, so the match is now handed over WITH the canonical location the enumeration already established, and the judge answers both questions on it at the same cost. The deny question is asked of the FOLDED view, because that is the form the walk decides on — `cat secrets/../sub/a.txt` reads `sub/a.txt` and is not refused.
8
+ - **Two views per path, and which is NEW is the whole of the change.** The operand's own spelling — already judged for a literal candidate, NEVER judged for an expansion the shell produces after the verdict — and the canonical TARGET, which no lexical pass can reach on either list. The second closes the deny seat's stated residual ② (an in-root symlink whose target is guarded: `cat notes.txt` → `<root>/.env` also disclosed at zero asks), which that seat's own contract already named this leg as the closer of. Judging the spelling of expansions ALONE would have been a second rule.
9
+ - **The same leak had a second site, and the face was the reason.** With `readFace:"open"` the leg exited before doing anything — a CONTAINMENT-shaped exit applied to the whole pass — so `cat .en?` and `cat notes.txt` disclosed the file at zero asks there too, on a face whose deny seat's own contract reads "the deny half above keeps judging in both faces" (the LITERAL `cat .env` was mandated there all along, which is what proves the floor is real). Which half still has a question is now one value the face decides member by member: containment OFF under open (an out-of-roots operand rides `checkedPaths` there as an ACCEPTED candidate; re-judging it would make the open face stricter than the closed one), deny ON in both. With neither question the leg does not run, so an open face with no deny table is answered exactly as before. Rules: −1 face special case. Both directions pinned — the deny cells AND the containment controls (an out-of-root read, plain and globbed, still runs under open with a deny table wired).
10
+ - **A third site, found by the adversarial round and confirmed by construction.** The expander had one more containment shortcut: when a glob's LAST directory canonicalized OUTSIDE the roots, it pushed that DIRECTORY and discarded the matches. Containment reached the right verdict through the directory, which is why the shortcut survived — and the deny half never can, because a deny row is about the FILE. Measured on the parent: under the open face, `cat vendor/.en?` (an in-root directory symlink pointing out) and `cat <outside>/.en?` both disclosed the file at zero asks. The expander now hands over the matches themselves with whatever canonical facts it already holds; containment may use those as shortcuts, it may not replace the operands. Consequence, disclosed: the closed face's `readonly_out_of_root` refusal for such a glob now names the FILES (`<outside>/secret`, `<outside>/second`) instead of the directory — strictly more precise, and `test/backlog775-glob-canonical-recheck.test.ts` A1-a is re-pinned to it. A consumer counting `details.paths` entries for this refusal sees one row per escaping match rather than one per escaping directory.
11
+ - **Precedence, made one rule.** An expansion the leg cannot establish used to RETURN immediately, which ranked it above a deny row a later operand would have produced — the one order this leg is not free to choose. It is now recorded like every other objection and the three are ranked once, at the end: deny > unestablished > containment. Every such command was refused before and is refused now; only which sentence it carries can change.
12
+ - **Not the other repair.** Widening the classify seat's weak rung to `boundaryUnread` would turn every in-root glob into an ordinary card — a narrowing for every deployment, deny table or not. RB-474b's ordinary card, the classify seat's card shape and the off/bypass seats are untouched.
13
+ - **Reach.** Only a deployment with a deny judge wired (`readDenyPatterns`, or a selected built-in tier — default OFF) sees anything: without one the boundary carries no `denyMatch` and this leg's answer is byte-identical, on both read faces. `bash_readonly` is unaffected — that face wires no deny judge at all (v1 ruling). No second adjudication: a deny row the walk RAISED returns a verdict carrying no candidates, so an approver's yes still runs the command. An execute-time deny refusal is NOT clearable by a stored allow rule or by the auto classifier — it is past the approval step, which is what makes it stronger than the accepted reach #891 states for a retired ordinary card.
14
+ - **Stated reach on the open face.** The leg's fail-closed arm travels with the deny half: under `readFace:"open"` WITH a deny table, a candidate this leg cannot ESTABLISH — a glob nobody can expand (an uncompilable range, an unenumerable directory, an expansion past the enumeration caps), or a literal whose chain the resolver could neither answer nor rule absent — is now refused rather than run, because a location that cannot be established cannot be deny-judged either. The closed face always answered that way; an open face with no deny table does not run the leg at all and is unchanged. Measured in both directions.
15
+ - **Still open, stated rather than assumed gone.** A DECLARED isolated sandbox keeps the lexical-only leg (property 3): its `canonicalPath` is an RPC into a namespace this process cannot resolve, so no expansion can be established there and the deny half stays at the approval walk's unexpanded-spelling reading. The sandbox is that deployment's declared floor. A RECURSIVE verb's directory operand (`grep -r . <dir>`) is judged on the operand, never on its subtree — measured at one mandated ask with no disclosure, so it is asked rather than leaked, but what a yes then opens is the execution environment's floor.
16
+ - **headless.** Restrictive direction: a deny-listed read that reached a headless run through a glob, or through an in-root symlink, is now refused instead of executed. The refusal is the fs boundary's own card (`read_path_denied`, with `target` and the matched `pattern`), the same one `Read`/`Grep`/`Glob` mint — one rule, learned once.
17
+ - Pins: `test/backlog934-glob-expansion-deny-judge.test.ts` (3 shapes × 2 approver arms, judge-identity via a case-folded pattern, the symlink view, the wire card, and five controls: RB-474b unchanged, no deny table, #775 unflipped, two `..`-over-fire shapes, and the approved deny row).
18
+
1
19
  ## 7.23.2 — 2026-09-19
2
20
 
3
21
  ### Rendering a thrown value is one total rule — a hook could kill the engine with its choice of rejection value (#926; P1, failure face; @cli @server @client-core: text only, no wire key)
@@ -19,7 +37,7 @@
19
37
  - **Difference from 7.23.0.** 7.23.0 refused these shapes with a MANDATE (no classifier, no stored rule, confirm-or-refuse). This version is strictly looser than that and strictly tighter than 7.23.1: an ordinary card an answerer may settle.
20
38
  - **Reach.** Any deployment that wires a read boundary (`readFace` + roots, with or without `readDenyPatterns`) under `shellGate:"classify"`: a shell command whose operands the walk could not read — a stream-fed operand list, an unreadable program position — now reaches the person/classifier instead of being auto-allowed by the read-only tables. In-root spellings of the same shapes (`echo sub/x.txt | xargs grep .`) are in that set too: the boundary declined for them as well, and "in root" is a fact nobody established. Commands with no decline are untouched — `git status`, `ls -la` and every other read-only shape the tables clear keep costing zero questions.
21
39
  - **headless.** Restrictive direction, and the reason this is a P1: a headless classify-tier run that executed these stream-fed reads with no approver now refuses them (an ask with no approver is a refusal). A deployment relying on that auto-allow should widen its roots, store an allow rule for the command, or wire an approver/classifier.
22
- - **NOT FIXED here, and visible rather than assumed gone (measured on this tree AND on 7.23.1's).** The other half of "the boundary could not read where this command reads" — an UNEXPANDED GLOB (`undecidedPaths`) — is a plain card by ruling (RB-474b) and is still cleared by the read-only tables. Its containment half is caught at execute time by the canonical recheck (#775); its DENY half is not, because that recheck judges roots only. `cat .en?` under `readDenyPatterns:["**/.env"]` discloses `.env` at zero asks on 7.23.1 and on this version alike. Tracked separately; this version neither widens nor narrows it.
40
+ - **The other half, measured here and FIXED in this same version by #934 below.** "The boundary could not read where this command reads" has a second family — an UNEXPANDED GLOB (`undecidedPaths`), a plain card by ruling (RB-474b) and still cleared by the read-only tables. Its containment half was already caught at execute time by the canonical recheck (#775); its DENY half was not, because that recheck judged roots only, so `cat .en?` under `readDenyPatterns:["**/.env"]` disclosed `.env` at zero asks on 7.23.1 and on this tree alike. This version does not change the CARD (RB-474b stands); it gives the execute-time recheck the boundary's own deny judge — see the #934 section.
23
41
  - Pins: `test/backlog930-decline-not-read-only-clearable.test.ts` (14 shapes × 2 arms, + the C-R67 (B) surface, the sandbox arm with its own three-way control, the off seat's mandate, the read-only arm still alive, and the `xargs cat` / in-root controls).
24
42
 
25
43
  ## 7.23.1 — 2026-09-18
@@ -284,10 +284,12 @@ export interface BashReadonlyRootBoundary {
284
284
  * undecided contract. What REMAINS of ① is the form table's stated open set (see the KNOWN OPEN
285
285
  * SET note at {@link RECURSIVE_READ_FORMS}: a traversal verb absent there that a deployment
286
286
  * allowlists classifies by its ordinary form and can auto-run a traversal unjudged);
287
- * ② LEXICAL only — an in-root symlink whose target is a
288
- * guarded path reads as its innocent spelling here, exactly as it does for the containment half
289
- * (the enforcing/canonicalizing recheck is both shell faces' execute leg, via checkedPaths; the
290
- * classify auto-allow lane has no I/O seat by contract). The structured read faces judge BOTH
287
+ * ② LEXICAL only HERE — an in-root symlink whose target is a
288
+ * guarded path reads as its innocent spelling at this seat, exactly as it does for the containment half,
289
+ * because the classify auto-allow lane has no I/O seat by contract. The CANONICAL view is judged by the
290
+ * full shell's execute-time recheck, which asks THIS judge (the same function object, forwarded) where
291
+ * each candidate really lands — a `checkedPaths` literal and a `undecidedPaths` pattern's expansions
292
+ * alike. The `bash_readonly` face wires no deny seat at all, so its own recheck carries nothing. The structured read faces judge BOTH
291
293
  * views (canonical + lexical); what this seat is and is not is stated once, at
292
294
  * `shell.read_boundary.friction_floor`. The `bash_readonly` face deliberately does NOT wire this seat (its
293
295
  * roots containment + command allowlist double gate is the deployment's own read-safety
@@ -8,7 +8,7 @@ import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_MISSING_ID_MESS
8
8
  import { hasBackgroundShell } from "../../core/background-shell.js";
9
9
  import { delimitUntrusted } from "../../core/untrusted-text.js";
10
10
  import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
11
- import { imageMagicMatches, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
11
+ import { imageMagicMatches, normalizeAbsPathLexically, violationDetails, withinAnyRoot } from "./safety.js";
12
12
  import { boundaryIsTheSandbox, canonicalPathIsAuthoritative, hasDestroy, isIsolated } from "../../core/remote-env.js";
13
13
  import { ghRateLimitHint } from "./gh-rate-limit.js";
14
14
  import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
@@ -772,7 +772,10 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
772
772
  };
773
773
  const canonicalEscape = async (command, signal) => {
774
774
  const resolvedBoundary = typeof taskOpts.readBoundary === "function" ? taskOpts.readBoundary() : taskOpts.readBoundary;
775
- if (resolvedBoundary === undefined || resolvedBoundary.face === "open")
775
+ if (resolvedBoundary === undefined)
776
+ return undefined;
777
+ const ask = { deny: resolvedBoundary.denyMatch, containment: resolvedBoundary.face !== "open" };
778
+ if (ask.deny === undefined && !ask.containment)
776
779
  return undefined;
777
780
  const faces = [
778
781
  classifyCompoundReadonlyDetailed(command, recheckReaders, resolvedBoundary),
@@ -781,7 +784,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
781
784
  const literal = [...new Set(faces.flatMap((v) => [...(v.checkedPaths ?? []), ...(v.checkedSpellings ?? [])]))];
782
785
  const asked = new Set(faces.flatMap((v) => v.outOfRootPaths ?? []));
783
786
  const patterns = [...new Set(faces.flatMap((v) => [...(v.undecidedPaths ?? []), ...(v.undecidedSpellings ?? [])]))].filter((p) => !asked.has(normalizeAbsPathLexically(p)));
784
- return await canonicalEscapeRefusal(canonicalBoundaryFor(resolvedBoundary.roots), { literal, patterns }, resolvedBoundary.roots, FULL_SHELL_FACE_ESCAPE_PROSE, signal);
787
+ return await canonicalEscapeRefusal(canonicalBoundaryFor(resolvedBoundary.roots), { literal, patterns }, resolvedBoundary.roots, FULL_SHELL_FACE_ESCAPE_PROSE, ask, signal);
785
788
  };
786
789
  return defineTool({
787
790
  name: "Bash",
@@ -1189,6 +1192,7 @@ function createCanonicalReadBoundary(env, readRoots) {
1189
1192
  let frontier = [p.slice(0, segs[first].start).replace(/[\\/]+$/, "") || "/"];
1190
1193
  let dirsUsed = 0;
1191
1194
  let matches = 0;
1195
+ const canonicalOfMatch = new Map();
1192
1196
  for (let j = first; j < segs.length; j++) {
1193
1197
  const seg = segs[j].text;
1194
1198
  if (!GLOB_METACHAR_RE.test(seg)) {
@@ -1222,40 +1226,48 @@ function createCanonicalReadBoundary(env, readRoots) {
1222
1226
  if (re.test(dot))
1223
1227
  next.push(joinCandidate(dir, dot));
1224
1228
  }
1225
- let dirInside = false;
1226
- if (isLast) {
1227
- if (canonDir !== undefined && !withinAnyRoot(roots, canonDir.value)) {
1228
- next.push(canonDir.value);
1229
- continue;
1230
- }
1231
- dirInside = canonDir !== undefined;
1232
- }
1229
+ const canonDirHere = isLast ? canonDir?.value : undefined;
1233
1230
  for (const entry of listed.value) {
1234
1231
  if (!globSegmentMatches(re, seg, entry.name))
1235
1232
  continue;
1236
1233
  if (++matches > MAX_GLOB_MATCHES) {
1237
1234
  return { unverifiable: `"${p}" expands to more than ${MAX_GLOB_MATCHES} names, too many to enumerate to verify` };
1238
1235
  }
1239
- if (isLast && dirInside && entry.kind !== "symlink")
1240
- continue;
1241
- next.push(joinCandidate(dir, entry.name));
1236
+ const candidate = joinCandidate(dir, entry.name);
1237
+ if (canonDirHere !== undefined && entry.kind !== "symlink")
1238
+ canonicalOfMatch.set(candidate, joinCandidate(canonDirHere, entry.name));
1239
+ next.push(candidate);
1242
1240
  }
1243
1241
  }
1244
1242
  frontier = next;
1245
1243
  if (frontier.length === 0)
1246
- return { paths: [] };
1244
+ return { paths: [], canonical: canonicalOfMatch };
1247
1245
  }
1248
- return { paths: frontier };
1246
+ return { paths: frontier, canonical: canonicalOfMatch };
1249
1247
  };
1250
1248
  return {
1251
- escapesAfterResolution: async (paths, signal) => {
1249
+ escapesAfterResolution: async (paths, ask, signal) => {
1250
+ const deny = ask.deny;
1252
1251
  if (lexicalOnly || (paths.literal.length === 0 && paths.patterns.length === 0))
1253
1252
  return { escaping: [] };
1254
1253
  const roots = await canonicalRoots(signal);
1255
1254
  const escaping = [];
1256
1255
  let unresolvable;
1257
- const judge = async (p) => {
1258
- const real = await resolve(p, signal);
1256
+ let denied;
1257
+ const denyHit = (p) => {
1258
+ if (deny === undefined || denied !== undefined)
1259
+ return false;
1260
+ const folded = normalizeAbsPathLexically(p);
1261
+ const pattern = deny(folded);
1262
+ if (pattern === null)
1263
+ return false;
1264
+ denied = { path: folded, pattern };
1265
+ return true;
1266
+ };
1267
+ const judge = async (p, canonicalKnown) => {
1268
+ if (denyHit(p))
1269
+ return;
1270
+ const real = canonicalKnown !== undefined ? { value: canonicalKnown, exact: true } : await resolve(p, signal);
1259
1271
  if (real === undefined) {
1260
1272
  if (resolverIsAuthoritative)
1261
1273
  unresolvable ??= `"${p}" cannot be resolved to verify where it reads`;
@@ -1265,18 +1277,36 @@ function createCanonicalReadBoundary(env, readRoots) {
1265
1277
  unresolvable ??= `"${p}" cannot be resolved to verify where it reads`;
1266
1278
  return;
1267
1279
  }
1268
- if (!withinAnyRoot(roots, real.value) && !escaping.includes(real.value))
1280
+ if (denyHit(real.value))
1281
+ return;
1282
+ if (ask.containment && !withinAnyRoot(roots, real.value) && !escaping.includes(real.value))
1269
1283
  escaping.push(real.value);
1270
1284
  };
1271
- for (const p of paths.literal)
1285
+ for (const p of paths.literal) {
1286
+ if (denied !== undefined)
1287
+ break;
1272
1288
  await judge(p);
1289
+ }
1273
1290
  for (const p of paths.patterns) {
1291
+ if (denied !== undefined)
1292
+ break;
1274
1293
  const expanded = await expandGlobCandidate(p, roots, signal);
1275
- if (expanded !== undefined && "unverifiable" in expanded)
1276
- return { escaping, unverifiable: expanded.unverifiable };
1277
- for (const t of expanded !== undefined && expanded.paths.length > 0 ? expanded.paths : [p])
1278
- await judge(t);
1294
+ if (expanded !== undefined && "unverifiable" in expanded) {
1295
+ unresolvable ??= expanded.unverifiable;
1296
+ continue;
1297
+ }
1298
+ if (expanded !== undefined && expanded.paths.length > 0) {
1299
+ for (const t of expanded.paths) {
1300
+ if (denied !== undefined)
1301
+ break;
1302
+ await judge(t, expanded.canonical.get(t));
1303
+ }
1304
+ }
1305
+ else
1306
+ await judge(p);
1279
1307
  }
1308
+ if (denied !== undefined)
1309
+ return { escaping, denied };
1280
1310
  return unresolvable === undefined ? { escaping } : { escaping, unverifiable: unresolvable };
1281
1311
  },
1282
1312
  canonicalOf: async (path, signal) => {
@@ -1300,8 +1330,14 @@ function createCanonicalReadBoundary(env, readRoots) {
1300
1330
  },
1301
1331
  };
1302
1332
  }
1303
- async function canonicalEscapeRefusal(canonical, candidates, readRoots, face, signal) {
1304
- const resolved = await canonical.escapesAfterResolution(candidates, signal);
1333
+ async function canonicalEscapeRefusal(canonical, candidates, readRoots, face, ask, signal) {
1334
+ const resolved = await canonical.escapesAfterResolution(candidates, ask, signal);
1335
+ if (resolved.denied !== undefined) {
1336
+ const content = `Error (Bash): reading "${resolved.denied.path}" is refused: the target matches the sensitive-path ` +
1337
+ `read deny list (pattern "${resolved.denied.pattern}"). This list guards credential-class paths and ` +
1338
+ `applies regardless of the containment roots. ${DENY_REFUSAL_NOTE}`;
1339
+ return errorResult(content, violationDetails({ code: "read_path_denied", message: content, target: resolved.denied.path, pattern: resolved.denied.pattern }));
1340
+ }
1305
1341
  if (resolved.unverifiable !== undefined) {
1306
1342
  return errorResult(`Error (Bash): ${resolved.unverifiable}, so this command cannot be confirmed to read only inside the allowed directories for this session: ${readRoots.join(", ")}. ` +
1307
1343
  face.note, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths: [] });
@@ -1313,6 +1349,8 @@ async function canonicalEscapeRefusal(canonical, candidates, readRoots, face, si
1313
1349
  }
1314
1350
  return undefined;
1315
1351
  }
1352
+ const DENY_REFUSAL_NOTE = "The read boundary judged this command's written spelling before the call was approved; what the shell " +
1353
+ "would actually read was established only at execution, and a deny-listed read is refused here rather than escalated.";
1316
1354
  const READONLY_FACE_ESCAPE_PROSE = {
1317
1355
  subject: "a path this command reads",
1318
1356
  verdictPhrase: ` ${NOT_AUTO_ALLOWED}`,
@@ -1376,7 +1414,7 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
1376
1414
  return errorResult(`Error (Bash): ${boundary.reason}. ${BASH_READONLY_CONFINEMENT_NOTE}`, { type: "readonly_out_of_root", code: "readonly_out_of_root", paths: boundary.outOfRootPaths ?? [] });
1377
1415
  }
1378
1416
  }
1379
- const escape = await canonicalEscapeRefusal(canonicalBoundary, { literal: [...(boundary.checkedPaths ?? []), ...(boundary.checkedSpellings ?? [])], patterns: [...(boundary.undecidedPaths ?? []), ...(boundary.undecidedSpellings ?? [])] }, readRoots, READONLY_FACE_ESCAPE_PROSE, ctx.signal);
1417
+ const escape = await canonicalEscapeRefusal(canonicalBoundary, { literal: [...(boundary.checkedPaths ?? []), ...(boundary.checkedSpellings ?? [])], patterns: [...(boundary.undecidedPaths ?? []), ...(boundary.undecidedSpellings ?? [])] }, readRoots, READONLY_FACE_ESCAPE_PROSE, { deny: undefined, containment: true }, ctx.signal);
1380
1418
  if (escape !== undefined)
1381
1419
  return escape;
1382
1420
  const readOnlyTick = opts?.onToolProgress === undefined ? undefined : (t) => describeSafely(opts.onToolProgress, { toolCallId: ctx.toolCallId, toolName: "Bash", ...t });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.23.2",
3
+ "version": "7.23.3",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -0,0 +1,25 @@
1
+ {
2
+ "repo": "core",
3
+ "version": "7.23.3",
4
+ "from": "b48d37a658cfaa46695e76339021dc1226a9ba57",
5
+ "to": "v7.23.3",
6
+ "added": [],
7
+ "removed": [],
8
+ "changed": [],
9
+ "narrowed": [],
10
+ "closedSetMembers": [],
11
+ "closedSetsUnreadable": [],
12
+ "typeKeys": [],
13
+ "declarationMoves": [],
14
+ "mintMoves": [],
15
+ "criteriaObserves": [],
16
+ "consumersHint": {
17
+ "cli": [],
18
+ "server": [],
19
+ "sdk": [],
20
+ "note": "scanned cli@bef85633 server@1da4e102 sdk@0a6aeb7 — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
21
+ },
22
+ "breakingCount": 0,
23
+ "breakingItems": [],
24
+ "toResolvedAt": "912d7792e95edd36fb9b6bb68866661ac36f7f6a"
25
+ }