@sema-agent/core 2.7.0 → 2.9.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.
Files changed (45) hide show
  1. package/dist/agents/send-message-tool.d.ts +2 -0
  2. package/dist/agents/send-message-tool.js +38 -30
  3. package/dist/agents/subagent.js +15 -3
  4. package/dist/brain/circuit-breaker.js +18 -8
  5. package/dist/brain/retry.d.ts +1 -0
  6. package/dist/brain/retry.js +29 -7
  7. package/dist/brain/stream-engine.d.ts +1 -0
  8. package/dist/brain/stream-engine.js +74 -12
  9. package/dist/core/auto-compaction.js +9 -1
  10. package/dist/core/background-agent-store.d.ts +2 -0
  11. package/dist/core/background-agent-store.js +20 -0
  12. package/dist/core/mcp.js +8 -5
  13. package/dist/core/runner/prepare-task.js +25 -8
  14. package/dist/core/runner/runtask.js +20 -7
  15. package/dist/core/runner/tool-disclosure.d.ts +8 -3
  16. package/dist/core/runner/tool-disclosure.js +22 -8
  17. package/dist/core/skills-directory.d.ts +1 -1
  18. package/dist/core/skills-directory.js +257 -28
  19. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -1
  20. package/dist/core/store-contracts/file-snapshot-store-contract.js +11 -3
  21. package/dist/core/task-registry-agent.d.ts +2 -1
  22. package/dist/core/task-registry-agent.js +47 -54
  23. package/dist/core/task-registry.d.ts +1 -0
  24. package/dist/core/task-registry.js +1 -1
  25. package/dist/core/tools.d.ts +6 -2
  26. package/dist/core/tools.js +3 -2
  27. package/dist/core/types.d.ts +5 -1
  28. package/dist/engine/compaction/compaction.js +71 -20
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/internal/harness-types.d.ts +1 -1
  32. package/dist/internal/harness.d.ts +1 -1
  33. package/dist/internal/harness.js +1 -1
  34. package/dist/orchestration/run-workflow-tool.d.ts +1 -0
  35. package/dist/orchestration/run-workflow-tool.js +4 -1
  36. package/dist/orchestration/workflow.d.ts +1 -1
  37. package/dist/orchestration/workflow.js +2 -2
  38. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  39. package/dist/tools/fs/bash-readonly-classifier.js +113 -33
  40. package/dist/tools/fs/fs-bash.d.ts +2 -1
  41. package/dist/tools/fs/fs-bash.js +26 -6
  42. package/dist/tools/fs/fs-shared.d.ts +1 -0
  43. package/dist/tools/fs/fs-shared.js +44 -2
  44. package/dist/tools/fs/index.js +1 -0
  45. package/package.json +1 -1
@@ -82,13 +82,81 @@ function resolveOperandLexically(base, operand, homeDir) {
82
82
  return undefined;
83
83
  return normalizeAbsPathLexicalEitherFamily(`${base.replace(/[/\\]+$/, "")}/${raw}`);
84
84
  }
85
- function collectSegmentBoundaryFindings(toks, boundary) {
86
- const name = toks[0];
85
+ function tokenizeSegment(segment) {
86
+ const raw = segment
87
+ .trim()
88
+ .split(/\s+/)
89
+ .filter((t) => t.length > 0);
90
+ return { folded: raw.map(foldQuoteRemovalToken), raw };
91
+ }
92
+ function hasUnquotedExpansionMetachar(rawToken) {
93
+ let open;
94
+ for (const ch of rawToken) {
95
+ if (open === undefined && (ch === '"' || ch === "'")) {
96
+ open = ch;
97
+ continue;
98
+ }
99
+ if (open === ch) {
100
+ open = undefined;
101
+ continue;
102
+ }
103
+ if (open === undefined && (ch === "{" || ch === "}" || ch === "$" || ch === "`"))
104
+ return true;
105
+ if (open === '"' && (ch === "$" || ch === "`"))
106
+ return true;
107
+ }
108
+ return false;
109
+ }
110
+ function isGrepPatternFlagToken(tok) {
111
+ if (tok.startsWith("--"))
112
+ return tok.startsWith("--regexp") || tok.startsWith("--file");
113
+ return /^-[A-Za-z]*[ef]/.test(tok);
114
+ }
115
+ function grepClusterValueOwner(tok) {
116
+ if (!/^-[A-Za-z]/.test(tok) || tok.startsWith("--"))
117
+ return undefined;
118
+ for (const ch of tok.slice(1)) {
119
+ if (ch === "e" || ch === "f")
120
+ return ch;
121
+ if (!/[A-Za-z]/.test(ch))
122
+ return undefined;
123
+ }
124
+ return undefined;
125
+ }
126
+ function attachedOptionPayloads(tok) {
127
+ const out = [];
128
+ if (tok.startsWith("--")) {
129
+ const eq = tok.indexOf("=");
130
+ if (eq > 0 && eq + 1 < tok.length)
131
+ out.push(tok.slice(eq + 1));
132
+ return out;
133
+ }
134
+ for (let k = 1; k <= tok.length; k++) {
135
+ if (!/^[A-Za-z]*$/.test(tok.slice(1, k)))
136
+ break;
137
+ const payload = tok.slice(k);
138
+ if (payload.length > 0)
139
+ out.push(payload);
140
+ }
141
+ return out;
142
+ }
143
+ function collectSegmentBoundaryFindings(tokens, boundary) {
144
+ const name = tokens.folded[0];
87
145
  if (NO_PATH_OPERAND_COMMANDS.has(name))
88
146
  return [];
89
- const args = toks.slice(1);
147
+ const args = tokens.folded.slice(1);
90
148
  const findings = [];
91
149
  const candidates = [];
150
+ for (const rawArg of tokens.raw.slice(1)) {
151
+ if (!hasUnquotedExpansionMetachar(rawArg))
152
+ continue;
153
+ return [
154
+ {
155
+ kind: "unresolvable",
156
+ reason: `"${name}" is given the argument "${rawArg}", which the shell expands (brace/variable/command expansion) before the command runs — the path it would actually read cannot be resolved statically, so it is not auto-allowed`,
157
+ },
158
+ ];
159
+ }
92
160
  if (name === "cd") {
93
161
  const target = args.find((t) => !t.startsWith("-") || t === "-");
94
162
  if (target === undefined) {
@@ -100,7 +168,7 @@ function collectSegmentBoundaryFindings(toks, boundary) {
100
168
  candidates.push(target);
101
169
  }
102
170
  else {
103
- const patternSuppliedByFlag = name === "grep" && args.some((t) => t === "-e" || t.startsWith("-e") || t === "-f" || t.startsWith("-f") || t.startsWith("--regexp") || t.startsWith("--file"));
171
+ const patternSuppliedByFlag = name === "grep" && args.some(isGrepPatternFlagToken);
104
172
  let sawOperand = false;
105
173
  for (let k = 0; k < args.length; k++) {
106
174
  const t = args[k];
@@ -108,17 +176,21 @@ function collectSegmentBoundaryFindings(toks, boundary) {
108
176
  k++;
109
177
  continue;
110
178
  }
179
+ if (name === "cut" && (/^-d./.test(t) || /^--(output-)?delimiter=/.test(t)))
180
+ continue;
181
+ if (name === "grep" && (grepClusterValueOwner(t) === "e" || /^--regexp=/.test(t)))
182
+ continue;
111
183
  if (t.startsWith("-") && t !== "-") {
112
- const eq = t.indexOf("=");
113
- const value = eq > 0 ? t.slice(eq + 1) : "";
114
- if (value.length > 0 && isAbsolutePathToken(value))
115
- candidates.push(value);
184
+ for (const payload of attachedOptionPayloads(t)) {
185
+ if (isAbsolutePathToken(payload) || isPathShapedToken(payload))
186
+ candidates.push(payload);
187
+ }
116
188
  continue;
117
189
  }
118
- if (t === "-")
119
- continue;
120
190
  const isGrepPatternSlot = name === "grep" && !patternSuppliedByFlag && !sawOperand;
121
191
  sawOperand = true;
192
+ if (t === "-")
193
+ continue;
122
194
  if (isGrepPatternSlot)
123
195
  continue;
124
196
  if (isPathShapedToken(t))
@@ -220,17 +292,17 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
220
292
  const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity };
221
293
  const foldedSegments = [];
222
294
  for (let si = 0; si < segments.length; si++) {
223
- const toks = segments[si].trim().split(/\s+/).filter((t) => t.length > 0)
224
- .map(foldQuoteRemovalToken);
295
+ const segmentTokens = tokenizeSegment(segments[si]);
296
+ const toks = segmentTokens.folded;
225
297
  if (toks.length === 0)
226
298
  continue;
227
- foldedSegments.push(toks);
299
+ foldedSegments.push(segmentTokens);
228
300
  const name = toks[0];
229
301
  const nonOption = toks.slice(1).filter((t) => !t.startsWith("-"));
230
302
  if (!(pipeFed[si] ?? false)) {
231
303
  const floor = STDIN_FILE_FLOOR[name];
232
304
  const restArgs = toks.slice(1);
233
- const grepPatternSuppliedByFlag = name === "grep" && restArgs.some((t) => t === "-e" || t.startsWith("-e") || t === "-f" || t.startsWith("--regexp") || t.startsWith("--file"));
305
+ const grepPatternSuppliedByFlag = name === "grep" && restArgs.some(isGrepPatternFlagToken);
234
306
  let sawNonFlagOperand = false;
235
307
  let hasStdinDash = false;
236
308
  if (name !== "tr") {
@@ -273,27 +345,35 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
273
345
  return { reason: "reads a device/special file that is either unbounded (/dev/zero, /dev/stdin, /proc/<pid>/fd/0, … — blocks the pipeline until the tool timeout) or process-private (/proc/<pid>/environ, /proc/<pid>/mem, …) — not auto-allowed" };
274
346
  }
275
347
  }
276
- if (boundary !== undefined) {
277
- const outside = [];
278
- for (const toks of foldedSegments) {
279
- for (const finding of collectSegmentBoundaryFindings(toks, boundary)) {
280
- if (finding.kind === "unresolvable")
281
- return { reason: finding.reason };
282
- if (!outside.some((o) => o.path === finding.path))
283
- outside.push(finding);
284
- }
285
- }
286
- if (outside.length > 0) {
287
- const paths = outside.map((o) => `"${o.path}"`).join(", ");
288
- const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
289
- return {
290
- reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} — not auto-allowed`,
291
- outOfRootRead: true,
292
- outOfRootPaths: outside.map((o) => o.path),
293
- };
348
+ if (boundary !== undefined)
349
+ return evaluateReadBoundary(foldedSegments, boundary);
350
+ return {};
351
+ }
352
+ function evaluateReadBoundary(foldedSegments, boundary) {
353
+ const outside = [];
354
+ for (const toks of foldedSegments) {
355
+ for (const finding of collectSegmentBoundaryFindings(toks, boundary)) {
356
+ if (finding.kind === "unresolvable")
357
+ return { reason: finding.reason };
358
+ if (!outside.some((o) => o.path === finding.path))
359
+ outside.push(finding);
294
360
  }
295
361
  }
296
- return {};
362
+ if (outside.length === 0)
363
+ return {};
364
+ const paths = outside.map((o) => `"${o.path}"`).join(", ");
365
+ const allowed = boundary.roots.length > 0 ? boundary.roots.join(", ") : "(none)";
366
+ return {
367
+ reason: `"${outside[0].command}" reads ${paths}, outside the allowed directories for this session: ${allowed} — not auto-allowed`,
368
+ outOfRootRead: true,
369
+ outOfRootPaths: outside.map((o) => o.path),
370
+ };
371
+ }
372
+ export function classifySimpleCommandReadBoundary(command, boundary) {
373
+ const toks = tokenizeSegment(command);
374
+ if (toks.folded.length === 0)
375
+ return {};
376
+ return evaluateReadBoundary([toks], boundary);
297
377
  }
298
378
  export function classifyCompoundReadonly(command, allow, boundary) {
299
379
  return classifyCompoundReadonlyDetailed(command, allow, boundary).reason;
@@ -31,9 +31,10 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
31
31
  bashDefaultTimeoutMs?: number;
32
32
  bashMaxTimeoutMs?: number;
33
33
  }): AgentTool;
34
- export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption, timeoutOpts?: {
34
+ export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption, opts?: {
35
35
  bashDefaultTimeoutMs?: number;
36
36
  bashMaxTimeoutMs?: number;
37
+ additionalRoots?: readonly string[];
37
38
  }): AgentTool;
38
39
  export declare function createEnvTaskOutputTool(env: ExecutionEnv): AgentTool;
39
40
  export declare function createEnvTaskStopTool(env: ExecutionEnv, registry?: TaskRegistry): AgentTool;
@@ -7,8 +7,8 @@ import { delimitUntrusted } from "../../core/untrusted-text.js";
7
7
  import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
8
8
  import { imageMagicMatches, withinAnyRoot } from "./safety.js";
9
9
  import { ghRateLimitHint } from "./gh-rate-limit.js";
10
- import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, } from "./fs-shared.js";
11
- import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly } from "./bash-readonly-classifier.js";
10
+ import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, } from "./fs-shared.js";
11
+ import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyCompoundReadonly, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.js";
12
12
  export function bashReversibilityProbe(allow, boundary) {
13
13
  const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
14
14
  return (args) => {
@@ -627,16 +627,32 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
627
627
  },
628
628
  });
629
629
  }
630
- export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, timeoutOpts) {
631
- const timeoutCaps = resolveBashTimeoutCaps(timeoutOpts);
630
+ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, opts) {
631
+ const timeoutCaps = resolveBashTimeoutCaps(opts);
632
632
  const timeoutCapsSecView = bashTimeoutCapsSec(timeoutCaps);
633
633
  const sample = [...allow].slice(0, 6).join(", ");
634
+ const readRoots = [rootCanonical, ...(opts?.additionalRoots ?? [])];
635
+ const overflowSpoolFence = createShellOverflowSpoolFence(env);
636
+ const readsOnlyEngineOverflowSpool = async (verdict) => {
637
+ if (verdict.outOfRootRead !== true)
638
+ return false;
639
+ const paths = verdict.outOfRootPaths ?? [];
640
+ if (paths.length === 0)
641
+ return false;
642
+ for (const p of paths) {
643
+ if (!(await overflowSpoolFence(p)))
644
+ return false;
645
+ }
646
+ return true;
647
+ };
634
648
  return defineTool({
635
649
  name: "Bash",
636
650
  contract: { contractId: "core.bash_readonly@1", implementationRevision: "1" },
637
651
  description: `Run a SINGLE read-only inspection command (e.g. ${sample}, …) and return its output. Shell ` +
638
652
  "operators (pipes, redirects, ;, &&, command substitution, subshells) are rejected and only " +
639
- "allowlisted commands run. Still subject to the deployment's approval policy.",
653
+ "allowlisted commands run. Reads are confined to the workspace root(s): a path outside them is " +
654
+ "refused, as is one that cannot be resolved statically (e.g. a `~` path). Still subject to the " +
655
+ "deployment's approval policy.",
640
656
  parameters: Type.Object({
641
657
  command: Type.String({ description: "A single allowlisted read-only command (no shell operators)." }),
642
658
  timeout: Type.Optional(Type.Number({ description: `Timeout in milliseconds (default ${timeoutCaps.defaultMs}, max ${timeoutCaps.maxMs}; requests above the max are capped to it).` })),
@@ -647,7 +663,11 @@ export function createBashReadonlyTool(env, rootCanonical, allow, execClamp, tim
647
663
  const reason = coarseReadonlyCheck(command, allow);
648
664
  if (reason)
649
665
  return errorResult(`Error (Bash): ${reason}`);
650
- return runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
666
+ const boundary = classifySimpleCommandReadBoundary(command, { roots: readRoots, cwd: rootCanonical });
667
+ if (boundary.reason !== undefined && !(await readsOnlyEngineOverflowSpool(boundary))) {
668
+ return errorResult(`Error (Bash): ${boundary.reason}. bash_readonly is confined to the workspace roots; it has no approval path, so the call is refused rather than escalated.`, { code: "readonly_out_of_root", paths: boundary.outOfRootPaths ?? [] });
669
+ }
670
+ return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, execClamp, ctx.toolCallId, true);
651
671
  },
652
672
  });
653
673
  }
@@ -47,6 +47,7 @@ export declare const FILE_PATH_PARAMS: {
47
47
  };
48
48
  export declare function clipShellOutput(s: string): string;
49
49
  export declare function writeShellOverflowFile(env: ExecutionEnv, stdout: string, stderr: string): Promise<string | undefined>;
50
+ export declare function createShellOverflowSpoolFence(env: ExecutionEnv): (path: string) => Promise<boolean>;
50
51
  export declare function shellRecoveryHint(path: string, readOnly: boolean | undefined): string;
51
52
  export declare const FILE_STATE_TRAILER = " (file state is current in your context \u2014 no need to Read it back)";
52
53
  export declare const CWD_SENTINEL = "__cc_cwd_9f2c1b__";
@@ -52,7 +52,10 @@ export const BASH_MAX_TIMEOUT_SEC = 600;
52
52
  export const BASH_DEFAULT_TIMEOUT_MS = BASH_DEFAULT_TIMEOUT_SEC * 1000;
53
53
  export const BASH_MAX_TIMEOUT_MS = BASH_MAX_TIMEOUT_SEC * 1000;
54
54
  function validTimeoutMs(n) {
55
- return n !== undefined && Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
55
+ if (n === undefined || !Number.isFinite(n))
56
+ return undefined;
57
+ const floored = Math.floor(n);
58
+ return floored >= 1 ? floored : undefined;
56
59
  }
57
60
  export function resolveBashTimeoutCaps(opts) {
58
61
  const defaultMs = validTimeoutMs(opts?.bashDefaultTimeoutMs) ??
@@ -80,7 +83,7 @@ export function clipShellOutput(s) {
80
83
  return clipWithFilePointer(s, bashMaxOutputChars());
81
84
  }
82
85
  export async function writeShellOverflowFile(env, stdout, stderr) {
83
- const tf = await env.createTempFile({ prefix: "bash-output-", suffix: ".log" });
86
+ const tf = await env.createTempFile({ prefix: SHELL_OVERFLOW_FILE_PREFIX, suffix: SHELL_OVERFLOW_FILE_SUFFIX });
84
87
  if (!tf.ok)
85
88
  return undefined;
86
89
  const body = stderr.length > 0 ? `${stdout}${stdout.length > 0 && !stdout.endsWith("\n") ? "\n" : ""}--- stderr ---\n${stderr}` : stdout;
@@ -90,6 +93,45 @@ export async function writeShellOverflowFile(env, stdout, stderr) {
90
93
  const canon = await env.canonicalPath(tf.value);
91
94
  return canon.ok ? canon.value : tf.value;
92
95
  }
96
+ const SHELL_OVERFLOW_FILE_PREFIX = "bash-output-";
97
+ const SHELL_OVERFLOW_FILE_SUFFIX = ".log";
98
+ function toPosixPathKey(p) {
99
+ return p.replace(/\\/g, "/").replace(/(.)\/+$/, "$1");
100
+ }
101
+ function isShellOverflowFileName(base) {
102
+ if (!base.startsWith(SHELL_OVERFLOW_FILE_PREFIX) || !base.endsWith(SHELL_OVERFLOW_FILE_SUFFIX))
103
+ return false;
104
+ const middle = base.slice(SHELL_OVERFLOW_FILE_PREFIX.length, base.length - SHELL_OVERFLOW_FILE_SUFFIX.length);
105
+ return /^[0-9A-Za-z][0-9A-Za-z._-]*$/.test(middle);
106
+ }
107
+ export function createShellOverflowSpoolFence(env) {
108
+ let tempArea;
109
+ const learnTempArea = async () => {
110
+ const probe = await env.createTempDir();
111
+ if (!probe.ok)
112
+ return undefined;
113
+ const canon = await env.canonicalPath(probe.value);
114
+ const resolved = toPosixPathKey(canon.ok ? canon.value : probe.value);
115
+ await env.remove(probe.value, { recursive: true, force: true });
116
+ const cut = resolved.lastIndexOf("/");
117
+ return cut > 0 ? resolved.slice(0, cut) : undefined;
118
+ };
119
+ return async (path) => {
120
+ const target = toPosixPathKey(path);
121
+ const cut = target.lastIndexOf("/");
122
+ if (cut <= 0 || !isShellOverflowFileName(target.slice(cut + 1)))
123
+ return false;
124
+ tempArea ??= learnTempArea();
125
+ const area = await tempArea;
126
+ if (area === undefined)
127
+ return false;
128
+ const parent = target.slice(0, cut);
129
+ if (parent === area)
130
+ return true;
131
+ const up = parent.lastIndexOf("/");
132
+ return up > 0 && parent.slice(0, up) === area;
133
+ };
134
+ }
93
135
  export function shellRecoveryHint(path, readOnly) {
94
136
  const quoted = shellQuote(path);
95
137
  const example = readOnly ? `tail -c 50000 ${quoted}` : `sed -n 'START,ENDp' ${quoted}`;
@@ -38,6 +38,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
38
38
  ? createBashReadonlyTool(env, rootCanonical, new Set(bashReadonlyAllow ?? BASH_READONLY_DEFAULT_ALLOW), opts.execClamp, {
39
39
  ...(opts.bashDefaultTimeoutMs !== undefined ? { bashDefaultTimeoutMs: opts.bashDefaultTimeoutMs } : {}),
40
40
  ...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
41
+ ...(additionalRoots !== undefined ? { additionalRoots } : {}),
41
42
  })
42
43
  : createBashTool(env, rootCanonical, commitCoAuthor, cwdRef, {
43
44
  taskRegistry: opts.taskRegistry,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",