@warlock.js/ai-workspace 5.43.0 → 5.45.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 (86) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +14 -1
  3. package/cjs/index.cjs +1847 -141
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/backends/local.d.mts.map +1 -1
  6. package/esm/backends/local.mjs +36 -22
  7. package/esm/backends/local.mjs.map +1 -1
  8. package/esm/contracts/index.d.mts +5 -1
  9. package/esm/contracts/shell-approval.type.d.mts +88 -0
  10. package/esm/contracts/shell-approval.type.d.mts.map +1 -0
  11. package/esm/contracts/tool-io.type.d.mts +45 -1
  12. package/esm/contracts/tool-io.type.d.mts.map +1 -1
  13. package/esm/contracts/workspace-backend.contract.d.mts +11 -1
  14. package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
  15. package/esm/contracts/workspace-edit.type.d.mts +66 -0
  16. package/esm/contracts/workspace-edit.type.d.mts.map +1 -0
  17. package/esm/contracts/workspace-ops.contract.d.mts +7 -1
  18. package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
  19. package/esm/contracts/workspace-output.type.d.mts +57 -0
  20. package/esm/contracts/workspace-output.type.d.mts.map +1 -0
  21. package/esm/contracts/workspace-policy.type.d.mts +38 -1
  22. package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
  23. package/esm/contracts/workspace.contract.d.mts +23 -1
  24. package/esm/contracts/workspace.contract.d.mts.map +1 -1
  25. package/esm/edit/apply-replacement.mjs +58 -0
  26. package/esm/edit/apply-replacement.mjs.map +1 -0
  27. package/esm/edit/match.d.mts +16 -0
  28. package/esm/edit/match.d.mts.map +1 -0
  29. package/esm/edit/match.mjs +214 -0
  30. package/esm/edit/match.mjs.map +1 -0
  31. package/esm/errors.d.mts +12 -1
  32. package/esm/errors.d.mts.map +1 -1
  33. package/esm/errors.mjs +1 -0
  34. package/esm/errors.mjs.map +1 -1
  35. package/esm/hooks/after-edit.mjs +36 -0
  36. package/esm/hooks/after-edit.mjs.map +1 -0
  37. package/esm/index.d.mts +7 -2
  38. package/esm/index.mjs +3 -2
  39. package/esm/ops.d.mts +3 -1
  40. package/esm/ops.d.mts.map +1 -1
  41. package/esm/ops.mjs +213 -75
  42. package/esm/ops.mjs.map +1 -1
  43. package/esm/output/spill-dir.mjs +15 -0
  44. package/esm/output/spill-dir.mjs.map +1 -0
  45. package/esm/output/spill.mjs +223 -0
  46. package/esm/output/spill.mjs.map +1 -0
  47. package/esm/policy/approval-helpers.mjs +22 -0
  48. package/esm/policy/approval-helpers.mjs.map +1 -0
  49. package/esm/policy/policy.d.mts +3 -1
  50. package/esm/policy/policy.d.mts.map +1 -1
  51. package/esm/policy/policy.mjs +12 -4
  52. package/esm/policy/policy.mjs.map +1 -1
  53. package/esm/policy/powershell-aliases.mjs +275 -0
  54. package/esm/policy/powershell-aliases.mjs.map +1 -0
  55. package/esm/policy/powershell-parser.mjs +420 -0
  56. package/esm/policy/powershell-parser.mjs.map +1 -0
  57. package/esm/policy/shell-check.d.mts +32 -2
  58. package/esm/policy/shell-check.d.mts.map +1 -1
  59. package/esm/policy/shell-check.mjs +183 -31
  60. package/esm/policy/shell-check.mjs.map +1 -1
  61. package/esm/policy/shell-interpreter.d.mts +63 -0
  62. package/esm/policy/shell-interpreter.d.mts.map +1 -0
  63. package/esm/policy/shell-interpreter.mjs +123 -0
  64. package/esm/policy/shell-interpreter.mjs.map +1 -0
  65. package/esm/policy/shell-parser.d.mts +33 -0
  66. package/esm/policy/shell-parser.d.mts.map +1 -0
  67. package/esm/policy/shell-parser.mjs +3 -0
  68. package/esm/policy/shell-parser.mjs.map +1 -1
  69. package/esm/tools/glob.mjs +1 -0
  70. package/esm/tools/glob.mjs.map +1 -1
  71. package/esm/tools/run-shell.d.mts +10 -0
  72. package/esm/tools/run-shell.d.mts.map +1 -1
  73. package/esm/tools/run-shell.mjs +19 -1
  74. package/esm/tools/run-shell.mjs.map +1 -1
  75. package/esm/tools/run-tests.d.mts +7 -0
  76. package/esm/tools/run-tests.d.mts.map +1 -1
  77. package/esm/tools/run-tests.mjs +5 -4
  78. package/esm/tools/run-tests.mjs.map +1 -1
  79. package/esm/tools/write-file.mjs +3 -2
  80. package/esm/tools/write-file.mjs.map +1 -1
  81. package/esm/workspace.d.mts.map +1 -1
  82. package/esm/workspace.mjs +27 -5
  83. package/esm/workspace.mjs.map +1 -1
  84. package/llms-full.txt +70 -17
  85. package/package.json +3 -3
  86. package/skills/use-a-workspace/SKILL.md +70 -17
@@ -0,0 +1,36 @@
1
+ //#region ../../packages/ai-workspace/src/hooks/after-edit.ts
2
+ /** How long a hook may take when the policy sets no `afterEditTimeoutMs`. */
3
+ const DEFAULT_HOOK_TIMEOUT_MS = 1e4;
4
+ /** Longest diagnostics text kept when the policy sets no `afterEditMaxChars`. */
5
+ const DEFAULT_MAX_CHARS = 4e3;
6
+ function wrap(text) {
7
+ return `[diagnostics]\n${text}\n[/diagnostics]`;
8
+ }
9
+ /**
10
+ * Run the policy's `afterEdit` hook for one call and return the text to put
11
+ * on the result's `diagnostics`, or `undefined` when there is none. A throw
12
+ * or a timeout yields "diagnostics unavailable"; it never rejects.
13
+ */
14
+ async function runAfterEdit(policy, event) {
15
+ const hook = policy.hooks?.afterEdit;
16
+ if (hook === void 0 || event.paths.length === 0) return;
17
+ const timeoutMs = policy.hooks?.afterEditTimeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS;
18
+ const maxChars = policy.hooks?.afterEditMaxChars ?? DEFAULT_MAX_CHARS;
19
+ let timer;
20
+ try {
21
+ const answer = await Promise.race([Promise.resolve().then(() => hook(event)), new Promise((_resolve, reject) => {
22
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("afterEdit hook timed out")), timeoutMs);
23
+ })]);
24
+ if (typeof answer !== "string" || answer.trim() === "") return;
25
+ const text = answer.trim();
26
+ return wrap(text.length > maxChars ? `${text.slice(0, maxChars)}\n[diagnostics truncated: showing ${maxChars} of ${text.length} characters]` : text);
27
+ } catch {
28
+ return wrap("diagnostics unavailable");
29
+ } finally {
30
+ clearTimeout(timer);
31
+ }
32
+ }
33
+
34
+ //#endregion
35
+ export { runAfterEdit };
36
+ //# sourceMappingURL=after-edit.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"after-edit.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/hooks/after-edit.ts"],"sourcesContent":["import type { WorkspaceAfterEditEvent, WorkspacePolicy } from \"../contracts\";\n\n/** How long a hook may take when the policy sets no `afterEditTimeoutMs`. */\nconst DEFAULT_HOOK_TIMEOUT_MS = 10_000;\n/** Longest diagnostics text kept when the policy sets no `afterEditMaxChars`. */\nconst DEFAULT_MAX_CHARS = 4_000;\n\nfunction wrap(text: string): string {\n return `[diagnostics]\\n${text}\\n[/diagnostics]`;\n}\n\n/**\n * Run the policy's `afterEdit` hook for one call and return the text to put\n * on the result's `diagnostics`, or `undefined` when there is none. A throw\n * or a timeout yields \"diagnostics unavailable\"; it never rejects.\n */\nexport async function runAfterEdit(\n policy: WorkspacePolicy,\n event: WorkspaceAfterEditEvent,\n): Promise<string | undefined> {\n const hook = policy.hooks?.afterEdit;\n\n if (hook === undefined || event.paths.length === 0) {\n return undefined;\n }\n\n const timeoutMs = policy.hooks?.afterEditTimeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS;\n const maxChars = policy.hooks?.afterEditMaxChars ?? DEFAULT_MAX_CHARS;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n try {\n const answer = await Promise.race([\n Promise.resolve().then(() => hook(event)),\n new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => reject(new Error(\"afterEdit hook timed out\")), timeoutMs);\n }),\n ]);\n\n if (typeof answer !== \"string\" || answer.trim() === \"\") {\n return undefined;\n }\n\n const text = answer.trim();\n\n return wrap(\n text.length > maxChars\n ? `${text.slice(0, maxChars)}\\n[diagnostics truncated: showing ${maxChars} of ${text.length} characters]`\n : text,\n );\n } catch {\n return wrap(\"diagnostics unavailable\");\n } finally {\n clearTimeout(timer);\n }\n}\n"],"mappings":";;AAGA,MAAM,0BAA0B;;AAEhC,MAAM,oBAAoB;AAE1B,SAAS,KAAK,MAAsB;CAClC,OAAO,kBAAkB,KAAK;AAChC;;;;;;AAOA,eAAsB,aACpB,QACA,OAC6B;CAC7B,MAAM,OAAO,OAAO,OAAO;CAE3B,IAAI,SAAS,UAAa,MAAM,MAAM,WAAW,GAC/C;CAGF,MAAM,YAAY,OAAO,OAAO,sBAAsB;CACtD,MAAM,WAAW,OAAO,OAAO,qBAAqB;CACpD,IAAI;CAEJ,IAAI;EACF,MAAM,SAAS,MAAM,QAAQ,KAAK,CAChC,QAAQ,QAAQ,CAAC,CAAC,WAAW,KAAK,KAAK,CAAC,GACxC,IAAI,SAAgB,UAAU,WAAW;GACvC,QAAQ,iBAAiB,uBAAO,IAAI,MAAM,0BAA0B,CAAC,GAAG,SAAS;EACnF,CAAC,CACH,CAAC;EAED,IAAI,OAAO,WAAW,YAAY,OAAO,KAAK,MAAM,IAClD;EAGF,MAAM,OAAO,OAAO,KAAK;EAEzB,OAAO,KACL,KAAK,SAAS,WACV,GAAG,KAAK,MAAM,GAAG,QAAQ,EAAE,oCAAoC,SAAS,MAAM,KAAK,OAAO,gBAC1F,IACN;CACF,QAAQ;EACN,OAAO,KAAK,yBAAyB;CACvC,UAAU;EACR,aAAa,KAAK;CACpB;AACF"}
package/esm/index.d.mts CHANGED
@@ -1,4 +1,8 @@
1
+ import { WorkspaceEditMatchTier, WorkspaceEditMatching } from "./edit/match.mjs";
2
+ import { WorkspaceOutputKeep, WorkspaceOutputPolicy, WorkspaceTruncation } from "./contracts/workspace-output.type.mjs";
1
3
  import { ApplyPatchFileResult, ApplyPatchInput, ApplyPatchResult, EditFileInput, EditFileResult, ExecOptions, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ListDirEntry, ListDirEntryType, ListDirInput, ListDirOptions, ListDirResult, MultiEditInput, MultiEditOperation, MultiEditResult, ReadFileInput, ReadFileResult, RunShellBackgroundResult, RunShellInput, RunShellResult, RunTestsInput, ShellJobInfo, ShellJobStatus, ShellKillInput, ShellOutputInput, ShellOutputOptions, ShellOutputResult, StartShellOptions, TodoListResult, TodoWriteInput, WorkspaceTodo, WorkspaceTodoInput, WorkspaceTodoStatus, WorkspaceToolName, WriteFileInput, WriteFileResult } from "./contracts/tool-io.type.mjs";
4
+ import { WorkspaceCommandDecision, WorkspaceCommandRule, WorkspaceShellAsk, WorkspaceShellAskDecision, WorkspaceShellAskReason, WorkspaceShellAskRequest, WorkspaceShellClassification, WorkspaceShellOnAsk } from "./contracts/shell-approval.type.mjs";
5
+ import { WorkspaceAfterEditEvent, WorkspaceEditLineEndings, WorkspaceEditPolicy, WorkspaceEditTool, WorkspaceHooks } from "./contracts/workspace-edit.type.mjs";
2
6
  import { WorkspaceAllowCommand, WorkspaceBackendType, WorkspaceBackgroundPolicy, WorkspaceCommandCheckContext, WorkspaceCommandPattern, WorkspaceListPolicy, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellInterpreter, WorkspaceShellPolicy } from "./contracts/workspace-policy.type.mjs";
3
7
  import { WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult, WorkspaceBackendProcess, WorkspaceBackendStartOptions, WorkspaceBackendStat, WorkspaceOutputStream } from "./contracts/workspace-backend.contract.mjs";
4
8
  import { WorkspaceOps } from "./contracts/workspace-ops.contract.mjs";
@@ -6,7 +10,8 @@ import { Workspace, WorkspaceTools } from "./contracts/workspace.contract.mjs";
6
10
  import "./contracts/index.mjs";
7
11
  import { WorkspaceEditError, WorkspaceEditErrorOptions, WorkspaceEditFailure, WorkspaceJobError, WorkspaceJobErrorOptions, WorkspaceJobFailure, WorkspacePolicyError, WorkspacePolicyErrorOptions, WorkspacePolicyViolation, WorkspaceShellRefusalReason } from "./errors.mjs";
8
12
  import { ResolvedPath, buildEnv, isCommandAllowed, resolveInJail } from "./policy/policy.mjs";
9
- import { WorkspaceShellCheck, WorkspaceShellPath, WorkspaceShellPathAccess, checkShellCommand } from "./policy/shell-check.mjs";
13
+ import { PowerShellEdition, ResolvedShell, ResolvedShellInterpreter, ShellHost, resolveShell } from "./policy/shell-interpreter.mjs";
14
+ import { WorkspaceShellCheck, WorkspaceShellPath, WorkspaceShellPathAccess, checkShellCommand, classifyShellCommand } from "./policy/shell-check.mjs";
10
15
  import { ArgvAnalysis, analyzeArgvCommand, tokenizeCommand } from "./policy/tokenize-command.mjs";
11
16
  import { applyPatchWithOps } from "./patch/apply-patch-generic.mjs";
12
17
  import { FilePatch, FilePatchKind, PatchHunk, PatchLine, parsePatch } from "./patch/parse-patch.mjs";
@@ -29,5 +34,5 @@ import { TodoStore } from "./todos.mjs";
29
34
  import { makeTodoReadTool, makeTodoWriteTool } from "./tools/todo.mjs";
30
35
  import { makeWriteFileTool } from "./tools/write-file.mjs";
31
36
  import { workspace } from "./workspace.mjs";
32
- export { type ApplyPatchFileResult, type ApplyPatchInput, type ApplyPatchResult, type ArgvAnalysis, type EditFileInput, type EditFileResult, type ExecOptions, type FilePatch, type FilePatchKind, type GlobInput, type GlobResult, type GrepInput, type GrepMatch, type GrepResult, type ListDirEntry, type ListDirEntryType, type ListDirInput, type ListDirOptions, type ListDirResult, type MakeGlobToolOptions, type MakeGrepToolOptions, type MakeRunShellToolOptions, type MakeRunTestsToolOptions, type MockBackendSeed, type MockExecResult, type MultiEditInput, type MultiEditOperation, type MultiEditResult, type PatchHunk, type PatchLine, type ReadFileInput, type ReadFileResult, type ResolvedPath, type RunShellBackgroundResult, type RunShellInput, type RunShellResult, type RunTestsInput, type ShellJobInfo, type ShellJobStatus, type ShellKillInput, type ShellOutputInput, type ShellOutputOptions, type ShellOutputResult, type StartShellOptions, type TodoListResult, TodoStore, type TodoWriteInput, type Workspace, type WorkspaceAllowCommand, type WorkspaceBackend, type WorkspaceBackendExecOptions, type WorkspaceBackendExecResult, type WorkspaceBackendProcess, type WorkspaceBackendStartOptions, type WorkspaceBackendStat, type WorkspaceBackendType, type WorkspaceBackgroundPolicy, type WorkspaceCommandCheckContext, type WorkspaceCommandPattern, WorkspaceEditError, type WorkspaceEditErrorOptions, type WorkspaceEditFailure, WorkspaceJobError, type WorkspaceJobErrorOptions, type WorkspaceJobFailure, type WorkspaceListPolicy, type WorkspaceOps, type WorkspaceOutputStream, type WorkspacePolicy, WorkspacePolicyError, type WorkspacePolicyErrorOptions, type WorkspacePolicyViolation, type WorkspaceReadPolicy, type WorkspaceShellCheck, type WorkspaceShellInterpreter, type WorkspaceShellPath, type WorkspaceShellPathAccess, type WorkspaceShellPolicy, type WorkspaceShellRefusalReason, type WorkspaceTodo, type WorkspaceTodoInput, type WorkspaceTodoStatus, type WorkspaceToolName, type WorkspaceTools, type WriteFileInput, type WriteFileResult, analyzeArgvCommand, applyHunks, applyPatchWithOps, buildEnv, checkShellCommand, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeApplyPatchTool, makeEditFileTool, makeGlobTool, makeGrepTool, makeListDirTool, makeMultiEditTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeShellKillTool, makeShellOutputTool, makeTodoReadTool, makeTodoWriteTool, makeWriteFileTool, parsePatch, resolveInJail, tokenizeCommand, workspace };
37
+ export { type ApplyPatchFileResult, type ApplyPatchInput, type ApplyPatchResult, type ArgvAnalysis, type EditFileInput, type EditFileResult, type ExecOptions, type FilePatch, type FilePatchKind, type GlobInput, type GlobResult, type GrepInput, type GrepMatch, type GrepResult, type ListDirEntry, type ListDirEntryType, type ListDirInput, type ListDirOptions, type ListDirResult, type MakeGlobToolOptions, type MakeGrepToolOptions, type MakeRunShellToolOptions, type MakeRunTestsToolOptions, type MockBackendSeed, type MockExecResult, type MultiEditInput, type MultiEditOperation, type MultiEditResult, type PatchHunk, type PatchLine, type PowerShellEdition, type ReadFileInput, type ReadFileResult, type ResolvedPath, type ResolvedShell, type ResolvedShellInterpreter, type RunShellBackgroundResult, type RunShellInput, type RunShellResult, type RunTestsInput, type ShellHost, type ShellJobInfo, type ShellJobStatus, type ShellKillInput, type ShellOutputInput, type ShellOutputOptions, type ShellOutputResult, type StartShellOptions, type TodoListResult, TodoStore, type TodoWriteInput, type Workspace, type WorkspaceAfterEditEvent, type WorkspaceAllowCommand, type WorkspaceBackend, type WorkspaceBackendExecOptions, type WorkspaceBackendExecResult, type WorkspaceBackendProcess, type WorkspaceBackendStartOptions, type WorkspaceBackendStat, type WorkspaceBackendType, type WorkspaceBackgroundPolicy, type WorkspaceCommandCheckContext, type WorkspaceCommandDecision, type WorkspaceCommandPattern, type WorkspaceCommandRule, WorkspaceEditError, type WorkspaceEditErrorOptions, type WorkspaceEditFailure, type WorkspaceEditLineEndings, type WorkspaceEditMatchTier, type WorkspaceEditMatching, type WorkspaceEditPolicy, type WorkspaceEditTool, type WorkspaceHooks, WorkspaceJobError, type WorkspaceJobErrorOptions, type WorkspaceJobFailure, type WorkspaceListPolicy, type WorkspaceOps, type WorkspaceOutputKeep, type WorkspaceOutputPolicy, type WorkspaceOutputStream, type WorkspacePolicy, WorkspacePolicyError, type WorkspacePolicyErrorOptions, type WorkspacePolicyViolation, type WorkspaceReadPolicy, type WorkspaceShellAsk, type WorkspaceShellAskDecision, type WorkspaceShellAskReason, type WorkspaceShellAskRequest, type WorkspaceShellCheck, type WorkspaceShellClassification, type WorkspaceShellInterpreter, type WorkspaceShellOnAsk, type WorkspaceShellPath, type WorkspaceShellPathAccess, type WorkspaceShellPolicy, type WorkspaceShellRefusalReason, type WorkspaceTodo, type WorkspaceTodoInput, type WorkspaceTodoStatus, type WorkspaceToolName, type WorkspaceTools, type WorkspaceTruncation, type WriteFileInput, type WriteFileResult, analyzeArgvCommand, applyHunks, applyPatchWithOps, buildEnv, checkShellCommand, classifyShellCommand, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeApplyPatchTool, makeEditFileTool, makeGlobTool, makeGrepTool, makeListDirTool, makeMultiEditTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeShellKillTool, makeShellOutputTool, makeTodoReadTool, makeTodoWriteTool, makeWriteFileTool, parsePatch, resolveInJail, resolveShell, tokenizeCommand, workspace };
33
38
  import "./workspace.mjs";
package/esm/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { WorkspaceEditError, WorkspaceJobError, WorkspacePolicyError } from "./errors.mjs";
2
+ import { resolveShell } from "./policy/shell-interpreter.mjs";
2
3
  import { analyzeArgvCommand, tokenizeCommand } from "./policy/tokenize-command.mjs";
3
- import { checkShellCommand } from "./policy/shell-check.mjs";
4
+ import { checkShellCommand, classifyShellCommand } from "./policy/shell-check.mjs";
4
5
  import { buildEnv, isCommandAllowed, resolveInJail } from "./policy/policy.mjs";
5
6
  import { applyHunks } from "./patch/apply-hunks.mjs";
6
7
  import { parsePatch } from "./patch/parse-patch.mjs";
@@ -24,4 +25,4 @@ import { makeTodoReadTool, makeTodoWriteTool } from "./tools/todo.mjs";
24
25
  import { makeWriteFileTool } from "./tools/write-file.mjs";
25
26
  import { workspace } from "./workspace.mjs";
26
27
 
27
- export { TodoStore, WorkspaceEditError, WorkspaceJobError, WorkspacePolicyError, analyzeArgvCommand, applyHunks, applyPatchWithOps, buildEnv, checkShellCommand, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeApplyPatchTool, makeEditFileTool, makeGlobTool, makeGrepTool, makeListDirTool, makeMultiEditTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeShellKillTool, makeShellOutputTool, makeTodoReadTool, makeTodoWriteTool, makeWriteFileTool, parsePatch, resolveInJail, tokenizeCommand, workspace };
28
+ export { TodoStore, WorkspaceEditError, WorkspaceJobError, WorkspacePolicyError, analyzeArgvCommand, applyHunks, applyPatchWithOps, buildEnv, checkShellCommand, classifyShellCommand, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeApplyPatchTool, makeEditFileTool, makeGlobTool, makeGrepTool, makeListDirTool, makeMultiEditTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeShellKillTool, makeShellOutputTool, makeTodoReadTool, makeTodoWriteTool, makeWriteFileTool, parsePatch, resolveInJail, resolveShell, tokenizeCommand, workspace };
package/esm/ops.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { WorkspacePolicy } from "./contracts/workspace-policy.type.mjs";
1
+ import { WorkspaceCommandPattern, WorkspacePolicy } from "./contracts/workspace-policy.type.mjs";
2
2
  import { WorkspaceBackend } from "./contracts/workspace-backend.contract.mjs";
3
3
  import { WorkspaceOps } from "./contracts/workspace-ops.contract.mjs";
4
4
  import "./contracts/index.mjs";
@@ -18,6 +18,8 @@ import { ShellJobs } from "./shell-jobs.mjs";
18
18
  export interface OpsRuntime {
19
19
  /** The workspace's background commands. */
20
20
  jobs: ShellJobs;
21
+ /** Patterns the host approved with `remember`, shared by every view of the workspace. */
22
+ approvals?: WorkspaceCommandPattern[];
21
23
  }
22
24
  /**
23
25
  * Create the policy-enforced operation layer over a backend.
package/esm/ops.d.mts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ops.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/ops.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAiMiB;;EAEf,MAAM;;;;;;;;;;;;;;;;;;;;;wBAouBQ,UACd,SAAS,kBACT,QAAQ,iBACR,UAAU,aACT"}
1
+ {"version":3,"file":"ops.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/ops.ts"],"mappings":";;;;;;;;;;;;;;;;;iBA2MiB;;EAEf,MAAM;;EAEN,YAAY;;;;;;;;;;;;;;;;;;;;;wBA65BE,UACd,SAAS,kBACT,QAAQ,iBACR,UAAU,aACT"}
package/esm/ops.mjs CHANGED
@@ -1,8 +1,13 @@
1
1
  import { WorkspaceEditError, WorkspacePolicyError } from "./errors.mjs";
2
2
  import { globToRegExp, isInside, matchesAnyGlob } from "./policy/jail.mjs";
3
+ import { resolveShell } from "./policy/shell-interpreter.mjs";
3
4
  import { checkShellCommand } from "./policy/shell-check.mjs";
4
5
  import { buildCommandEnv, resolveInJail } from "./policy/policy.mjs";
5
6
  import { executePatch } from "./patch/execute-patch.mjs";
7
+ import { applyReplacement } from "./edit/apply-replacement.mjs";
8
+ import { runAfterEdit } from "./hooks/after-edit.mjs";
9
+ import { OutputSpiller, splitLines } from "./output/spill.mjs";
10
+ import { refusalReason, samePattern } from "./policy/approval-helpers.mjs";
6
11
  import { ShellJobs } from "./shell-jobs.mjs";
7
12
  import path from "node:path";
8
13
  import { fs } from "@warlock.js/fs";
@@ -139,6 +144,26 @@ var Ops = class {
139
144
  this.policy = policy;
140
145
  this.runtime = runtime;
141
146
  this.pathLocks = /* @__PURE__ */ new Map();
147
+ this.resolvedShell = resolveShell(policy.shell);
148
+ this.approvals = runtime.approvals ?? [];
149
+ this.spiller = new OutputSpiller(backend, policy);
150
+ }
151
+ /** The edit-policy settings applied to every replacement. */
152
+ editOptions() {
153
+ const edit = this.policy.edit;
154
+ return {
155
+ matching: edit?.matching ?? "tiered",
156
+ missHintLines: edit?.missHintLines ?? 12,
157
+ lineEndings: edit?.lineEndings ?? "preserve",
158
+ ...this.policy.read?.maxBytes !== void 0 ? { hintMaxBytes: this.policy.read.maxBytes } : {}
159
+ };
160
+ }
161
+ /** Run `afterEdit` for a call that wrote `paths`; `undefined` when there is no output. */
162
+ diagnosticsFor(tool, paths) {
163
+ return runAfterEdit(this.policy, {
164
+ paths,
165
+ tool
166
+ });
142
167
  }
143
168
  /** Run `task` after every earlier mutation of `absolutePath` has settled. */
144
169
  async withPathLock(absolutePath, task) {
@@ -183,7 +208,7 @@ var Ops = class {
183
208
  }
184
209
  }
185
210
  async readFile(inputPath, opts) {
186
- const { absolutePath } = await resolveInJail(this.policy, inputPath);
211
+ const { absolutePath } = await resolveInJail(this.policy, inputPath, "read");
187
212
  const raw = await this.backend.readFile(absolutePath);
188
213
  const hash = fs.hash.string(raw);
189
214
  const lines = raw.split("\n");
@@ -192,10 +217,30 @@ var Ops = class {
192
217
  const limit = opts?.limit ?? this.policy.read?.defaultLines ?? DEFAULT_READ_LINES;
193
218
  const maxBytes = this.policy.read?.maxBytes;
194
219
  const startIndex = offset - 1;
195
- const { content, returnedLines, bytesCapped } = numberWindow(lines.slice(startIndex, startIndex + Math.max(0, limit)), offset, maxBytes === void 0 ? void 0 : Math.max(0, Math.floor(maxBytes)));
220
+ const window = lines.slice(startIndex, startIndex + Math.max(0, limit));
221
+ const { content, returnedLines, bytesCapped } = numberWindow(window, offset, maxBytes === void 0 ? void 0 : Math.max(0, Math.floor(maxBytes)));
196
222
  const endLine = offset + returnedLines - 1;
223
+ const spillDir = this.spiller.spillDir;
224
+ let spilled;
225
+ if (bytesCapped && spillDir !== void 0 && !isInside(absolutePath, spillDir)) {
226
+ const full = numberWindow(window, offset, void 0).content;
227
+ const file = await this.spiller.save("read_file", full);
228
+ if (file !== void 0) spilled = {
229
+ content: `${content}\n[showing lines 1-${returnedLines} of ${window.length}; full output saved to ${file}]`,
230
+ truncation: {
231
+ shown: { lines: returnedLines },
232
+ total: {
233
+ lines: window.length,
234
+ bytes: Buffer.byteLength(full, "utf8")
235
+ },
236
+ path: file
237
+ }
238
+ };
239
+ }
197
240
  return {
198
- content,
241
+ content: spilled?.content ?? content,
242
+ ...spilled !== void 0 ? { truncation: spilled.truncation } : {},
243
+ ...spilled?.truncation.path !== void 0 ? { spilledTo: [spilled.truncation.path] } : {},
199
244
  startLine: offset,
200
245
  endLine,
201
246
  totalLines,
@@ -206,8 +251,8 @@ var Ops = class {
206
251
  };
207
252
  }
208
253
  async writeFile(inputPath, content) {
209
- const { absolutePath } = await resolveInJail(this.policy, inputPath);
210
- return this.withPathLock(absolutePath, async () => {
254
+ const { absolutePath, relativePath } = await resolveInJail(this.policy, inputPath);
255
+ const written = await this.withPathLock(absolutePath, async () => {
211
256
  const parent = path.dirname(absolutePath);
212
257
  await this.backend.mkdir(parent);
213
258
  await this.backend.writeFile(absolutePath, content);
@@ -216,10 +261,20 @@ var Ops = class {
216
261
  bytesWritten: Buffer.byteLength(content, "utf8")
217
262
  };
218
263
  });
264
+ const diagnostics = await this.diagnosticsFor("write_file", [relativePath || inputPath]);
265
+ return {
266
+ ...written,
267
+ ...diagnostics !== void 0 ? { diagnostics } : {}
268
+ };
219
269
  }
220
270
  async editFile(input) {
221
271
  const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);
222
- return this.withPathLock(absolutePath, () => this.applyEdit(input, absolutePath, relativePath));
272
+ const result = await this.withPathLock(absolutePath, () => this.applyEdit(input, absolutePath, relativePath));
273
+ const diagnostics = await this.diagnosticsFor("edit_file", [result.path]);
274
+ return diagnostics === void 0 ? result : {
275
+ ...result,
276
+ diagnostics
277
+ };
223
278
  }
224
279
  /** The read-check-replace-write of {@link editFile}, run under its path lock. */
225
280
  async applyEdit(input, absolutePath, relativePath) {
@@ -231,19 +286,20 @@ var Ops = class {
231
286
  expectedHash: input.expectHash,
232
287
  actualHash: currentHash
233
288
  });
234
- const { content: updated, replacements } = applyReplacement(current, input, input.path, relativePath || input.path);
289
+ const { content: updated, replacements, matchTier } = applyReplacement(current, input, input.path, relativePath || input.path, this.editOptions());
235
290
  await this.backend.writeFile(absolutePath, updated);
236
291
  return {
237
292
  path: relativePath || input.path,
238
293
  replacements,
239
- hash: fs.hash.string(updated)
294
+ hash: fs.hash.string(updated),
295
+ ...matchTier !== void 0 ? { matchTier } : {}
240
296
  };
241
297
  }
242
298
  async multiEdit(input) {
243
299
  const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);
244
300
  const shownPath = relativePath || input.path;
245
301
  if (input.edits.length === 0 || input.edits.length > 100) throw new RangeError(`A multi-edit needs between 1 and ${100} edits (got ${input.edits.length}).`);
246
- return this.withPathLock(absolutePath, async () => {
302
+ const result = await this.withPathLock(absolutePath, async () => {
247
303
  const current = await this.backend.readFile(absolutePath);
248
304
  const currentHash = fs.hash.string(current);
249
305
  if (input.expectHash !== void 0 && input.expectHash !== currentHash) throw new WorkspaceEditError(`File "${input.path}" changed since it was read; none of the edits were applied.`, {
@@ -254,21 +310,40 @@ var Ops = class {
254
310
  });
255
311
  let content = current;
256
312
  let replacements = 0;
313
+ let matchTier;
314
+ const options = this.editOptions();
257
315
  input.edits.forEach((edit, editIndex) => {
258
- const applied = applyReplacement(content, edit, input.path, shownPath, editIndex);
316
+ const applied = applyReplacement(content, edit, input.path, shownPath, options, editIndex);
259
317
  content = applied.content;
260
318
  replacements += applied.replacements;
319
+ if (applied.matchTier === "whitespace" || matchTier === void 0) matchTier = applied.matchTier ?? matchTier;
261
320
  });
262
321
  await this.backend.writeFile(absolutePath, content);
263
322
  return {
264
323
  path: shownPath,
265
324
  edits: input.edits.length,
266
325
  replacements,
267
- hash: fs.hash.string(content)
326
+ hash: fs.hash.string(content),
327
+ ...matchTier !== void 0 ? { matchTier } : {}
268
328
  };
269
329
  });
330
+ const diagnostics = await this.diagnosticsFor("multi_edit", [shownPath]);
331
+ return diagnostics === void 0 ? result : {
332
+ ...result,
333
+ diagnostics
334
+ };
270
335
  }
271
336
  async applyPatch(input) {
337
+ const result = await this.runPatch(input);
338
+ if (result.dryRun) return result;
339
+ const paths = result.files.filter((file) => file.action !== "deleted").map((file) => file.path);
340
+ const diagnostics = await this.diagnosticsFor("apply_patch", paths);
341
+ return diagnostics === void 0 ? result : {
342
+ ...result,
343
+ diagnostics
344
+ };
345
+ }
346
+ runPatch(input) {
272
347
  return executePatch(input, {
273
348
  resolve: (inputPath) => resolveInJail(this.policy, inputPath),
274
349
  exists: (absolutePath) => this.backend.exists(absolutePath),
@@ -288,9 +363,14 @@ var Ops = class {
288
363
  * (argv or shell mode), then — shell mode — every redirection target and
289
364
  * `cd` directory against the jail with symlinks resolved.
290
365
  */
291
- async assertCommandAllowed(command) {
292
- const verdict = checkShellCommand(this.policy, command);
293
- if (!verdict.allowed) throw new WorkspacePolicyError(verdict.message ?? `Command is not permitted by the workspace shell policy: ${command}`, {
366
+ async assertCommandAllowed(command, signal) {
367
+ const verdict = checkShellCommand(this.policy, command, {
368
+ remembered: this.approvals,
369
+ resolved: this.resolvedShell
370
+ });
371
+ const onAsk = this.policy.shell?.onAsk;
372
+ const needsApproval = !verdict.allowed && verdict.reason === "approval-required" && verdict.asks.length > 0;
373
+ if (!verdict.allowed && !(needsApproval && onAsk !== void 0)) throw new WorkspacePolicyError(verdict.message ?? `Command is not permitted by the workspace shell policy: ${command}`, {
294
374
  type: "denied-command",
295
375
  command,
296
376
  ...verdict.reason !== void 0 ? { reason: verdict.reason } : {},
@@ -313,9 +393,35 @@ var Ops = class {
313
393
  reason: "not-allowed",
314
394
  command
315
395
  });
396
+ if (needsApproval && onAsk !== void 0) await this.askForApproval(command, verdict.asks, onAsk, signal);
397
+ }
398
+ /** Put the commands that need approval to `shell.onAsk`; refuse the line unless it says yes. */
399
+ async askForApproval(command, asks, onAsk, signal) {
400
+ let decision;
401
+ try {
402
+ decision = await onAsk({
403
+ command,
404
+ asks,
405
+ ...signal ? { signal } : {}
406
+ });
407
+ } catch {
408
+ decision = void 0;
409
+ }
410
+ if (decision?.allow !== true) {
411
+ const reason = refusalReason(decision?.reason);
412
+ throw new WorkspacePolicyError(`Running ${asks.map((ask) => `\`${ask.argv.join(" ")}\``).join(", ")} was not approved${reason}`, {
413
+ type: "denied-command",
414
+ reason: "approval-denied",
415
+ command
416
+ });
417
+ }
418
+ if (decision.remember !== void 0) {
419
+ const patterns = Array.isArray(decision.remember) ? decision.remember : [decision.remember];
420
+ for (const pattern of patterns) if (!this.approvals.some((entry) => samePattern(entry, pattern))) this.approvals.push(pattern);
421
+ }
316
422
  }
317
423
  async exec(command, opts) {
318
- await this.assertCommandAllowed(command);
424
+ await this.assertCommandAllowed(command, opts?.signal);
319
425
  const signal = opts?.signal;
320
426
  if (signal?.aborted) return {
321
427
  exitCode: CANCELLED_BEFORE_START_EXIT_CODE,
@@ -328,24 +434,28 @@ var Ops = class {
328
434
  const shell = this.policy.shell;
329
435
  const timeoutMs = opts?.timeoutMs ?? shell?.timeoutMs;
330
436
  const maxOutputBytes = shell?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
331
- const interpreter = shell?.interpreter ?? "none";
437
+ const interpreter = this.resolvedShell.interpreter;
332
438
  const result = await this.backend.exec(command, {
333
439
  cwd: this.policy.cwd,
334
440
  timeoutMs,
335
441
  env: buildCommandEnv(this.policy),
336
442
  ...signal ? { signal } : {},
337
- ...interpreter !== "none" ? { interpreter } : {}
443
+ ...interpreter !== "none" ? { interpreter } : {},
444
+ ...interpreter !== "none" && this.resolvedShell.file !== void 0 ? { interpreterPath: this.resolvedShell.file } : {}
338
445
  });
339
- const stdout = capOutput(result.stdout, maxOutputBytes);
340
- const stderr = capOutput(result.stderr, maxOutputBytes);
446
+ const stdout = await this.fitStream("run_shell-stdout", result.stdout, maxOutputBytes);
447
+ const stderr = await this.fitStream("run_shell-stderr", result.stderr, maxOutputBytes);
341
448
  const cancelled = result.cancelled === true;
449
+ const truncation = streamTruncation(stdout.truncation, stderr.truncation);
342
450
  return {
343
451
  exitCode: result.exitCode,
344
452
  stdout: stdout.value,
345
453
  stderr: stderr.value,
346
454
  truncated: stdout.truncated || stderr.truncated,
347
455
  timedOut: result.timedOut && !cancelled,
348
- cancelled
456
+ cancelled,
457
+ ...truncation !== void 0 ? { truncation } : {},
458
+ ...spilledField(truncation)
349
459
  };
350
460
  }
351
461
  async startShell(command, opts) {
@@ -363,7 +473,8 @@ var Ops = class {
363
473
  command
364
474
  });
365
475
  const shell = this.policy.shell;
366
- const interpreter = shell?.interpreter ?? "none";
476
+ const interpreter = this.resolvedShell.interpreter;
477
+ const interpreterPath = interpreter !== "none" ? this.resolvedShell.file : void 0;
367
478
  const timeoutMs = lowerOf(background.timeoutMs, opts?.timeoutMs);
368
479
  const env = buildCommandEnv(this.policy);
369
480
  return this.runtime.jobs.start({
@@ -375,12 +486,52 @@ var Ops = class {
375
486
  cwd: this.policy.cwd,
376
487
  env,
377
488
  ...interpreter !== "none" ? { interpreter } : {},
489
+ ...interpreterPath !== void 0 ? { interpreterPath } : {},
378
490
  onOutput
379
491
  })
380
492
  });
381
493
  }
382
- shellOutput(id, opts) {
383
- return this.runtime.jobs.output(id, opts);
494
+ async shellOutput(id, opts) {
495
+ const result = await this.runtime.jobs.output(id, opts);
496
+ if (!this.spiller.enabled) return result;
497
+ const stdout = await this.spiller.text(`shell_output-${id}-stdout`, result.stdout, "head+tail");
498
+ const stderr = await this.spiller.text(`shell_output-${id}-stderr`, result.stderr, "head+tail");
499
+ const truncation = streamTruncation(stdout.truncation, stderr.truncation);
500
+ return truncation === void 0 ? result : {
501
+ ...result,
502
+ stdout: stdout.value,
503
+ stderr: stderr.value,
504
+ truncated: true,
505
+ truncation,
506
+ ...spilledField(truncation)
507
+ };
508
+ }
509
+ /**
510
+ * Fit one captured stream to the limits: spilled to a file when
511
+ * `output.spillDir` is set, otherwise clipped at `maxBytes` as always.
512
+ */
513
+ async fitStream(label, value, maxBytes) {
514
+ if (this.spiller.enabled) {
515
+ const cap = this.policy.shell?.maxOutputBytes;
516
+ const spilled = await this.spiller.text(label, value, "head+tail", cap);
517
+ if (spilled.truncation !== void 0) return {
518
+ value: spilled.value,
519
+ truncated: true,
520
+ truncation: spilled.truncation
521
+ };
522
+ }
523
+ const capped = capOutput(value, maxBytes);
524
+ if (!capped.truncated) return capped;
525
+ return {
526
+ ...capped,
527
+ truncation: {
528
+ shown: { lines: splitLines(capped.value).length },
529
+ total: {
530
+ lines: splitLines(value).length,
531
+ bytes: Buffer.byteLength(value, "utf8")
532
+ }
533
+ }
534
+ };
384
535
  }
385
536
  shellKill(id) {
386
537
  return this.runtime.jobs.kill(id);
@@ -388,8 +539,9 @@ var Ops = class {
388
539
  listShells() {
389
540
  return this.runtime.jobs.list();
390
541
  }
391
- dispose() {
392
- return this.runtime.jobs.dispose();
542
+ async dispose() {
543
+ await this.runtime.jobs.dispose();
544
+ await this.spiller.dispose();
393
545
  }
394
546
  async listDir(inputPath = ".", opts) {
395
547
  const { absolutePath: root, relativePath: rootRelative } = await resolveInJail(this.policy, inputPath);
@@ -493,16 +645,34 @@ var Ops = class {
493
645
  line: index + 1,
494
646
  text: line
495
647
  });
496
- if (matches.length >= DEFAULT_MAX_GREP_MATCHES) return {
497
- matches,
498
- total: matches.length
499
- };
648
+ if (matches.length >= DEFAULT_MAX_GREP_MATCHES) return this.fitMatches(matches);
500
649
  }
501
650
  }
502
651
  }
503
- return {
652
+ return this.fitMatches(matches);
653
+ }
654
+ /** Apply the `output` limits to a finished match list. */
655
+ async fitMatches(matches) {
656
+ const fitted = await this.spiller.list("grep", matches, (match) => `${match.path}:${match.line}:${match.text}`, "head");
657
+ return fitted.truncation === void 0 ? {
504
658
  matches,
505
659
  total: matches.length
660
+ } : {
661
+ matches: fitted.items,
662
+ total: matches.length,
663
+ truncated: true,
664
+ truncation: fitted.truncation,
665
+ ...spilledField({ stdout: fitted.truncation })
666
+ };
667
+ }
668
+ async globResult(pattern) {
669
+ const all = await this.glob(pattern);
670
+ const fitted = await this.spiller.list("glob", all, (entry) => entry, "head");
671
+ return fitted.truncation === void 0 ? { paths: all } : {
672
+ paths: fitted.items,
673
+ truncated: true,
674
+ truncation: fitted.truncation,
675
+ ...spilledField({ stdout: fitted.truncation })
506
676
  };
507
677
  }
508
678
  async glob(pattern) {
@@ -535,55 +705,23 @@ var Ops = class {
535
705
  return denyPaths !== void 0 && denyPaths.length > 0 && matchesAnyGlob(relativePath, denyPaths);
536
706
  }
537
707
  };
538
- /** The lower of two optional limits (either may be absent). */
539
- function lowerOf(policyLimit, requested) {
540
- if (requested === void 0 || requested <= 0) return policyLimit;
541
- return policyLimit === void 0 ? requested : Math.min(policyLimit, requested);
708
+ /** `{ spilledTo }` for every spill file named in the truncation metadata, or nothing. */
709
+ function spilledField(truncation) {
710
+ const paths = [truncation?.stdout?.path, truncation?.stderr?.path].filter((file) => file !== void 0);
711
+ return paths.length > 0 ? { spilledTo: paths } : {};
542
712
  }
543
- /**
544
- * Apply one exact-string replacement to `content` under the edit rules:
545
- * `oldString` must occur, and exactly once unless `replaceAll`.
546
- */
547
- function applyReplacement(content, edit, inputPath, shownPath, editIndex) {
548
- const occurrences = countOccurrences(content, edit.oldString);
549
- const which = editIndex === void 0 ? "" : `Edit ${editIndex + 1}: `;
550
- const nothingApplied = editIndex === void 0 ? "" : " None of the edits were applied.";
551
- const indexOption = editIndex === void 0 ? {} : { editIndex };
552
- if (occurrences === 0) throw new WorkspaceEditError(`${which}The text to replace was not found in "${inputPath}".${nothingApplied}`, {
553
- type: "not-found",
554
- path: shownPath,
555
- matches: 0,
556
- ...indexOption
557
- });
558
- if (occurrences > 1 && !edit.replaceAll) throw new WorkspaceEditError(`${which}The text to replace is not unique in "${inputPath}" (${occurrences} matches); pass replaceAll or include more surrounding context.${nothingApplied}`, {
559
- type: "not-unique",
560
- path: shownPath,
561
- matches: occurrences,
562
- ...indexOption
563
- });
713
+ /** Combine per-stream truncation metadata; `undefined` when neither stream was clipped. */
714
+ function streamTruncation(stdout, stderr) {
715
+ if (stdout === void 0 && stderr === void 0) return;
564
716
  return {
565
- content: edit.replaceAll ? content.split(edit.oldString).join(edit.newString) : replaceFirst(content, edit.oldString, edit.newString),
566
- replacements: edit.replaceAll ? occurrences : 1
717
+ ...stdout !== void 0 ? { stdout } : {},
718
+ ...stderr !== void 0 ? { stderr } : {}
567
719
  };
568
720
  }
569
- /** Count non-overlapping occurrences of `needle` in `haystack`. */
570
- function countOccurrences(haystack, needle) {
571
- if (needle === "") return 0;
572
- let count = 0;
573
- let from = 0;
574
- while (true) {
575
- const index = haystack.indexOf(needle, from);
576
- if (index === -1) break;
577
- count++;
578
- from = index + needle.length;
579
- }
580
- return count;
581
- }
582
- /** Replace the first occurrence of `needle` with `replacement`. */
583
- function replaceFirst(haystack, needle, replacement) {
584
- const index = haystack.indexOf(needle);
585
- if (index === -1) return haystack;
586
- return haystack.slice(0, index) + replacement + haystack.slice(index + needle.length);
721
+ /** The lower of two optional limits (either may be absent). */
722
+ function lowerOf(policyLimit, requested) {
723
+ if (requested === void 0 || requested <= 0) return policyLimit;
724
+ return policyLimit === void 0 ? requested : Math.min(policyLimit, requested);
587
725
  }
588
726
  /**
589
727
  * Create the policy-enforced operation layer over a backend.