@mikode13/harness 4.0.1 → 5.0.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 (167) hide show
  1. package/README.md +278 -16
  2. package/dist/agent/domain/agent.d.ts +12 -0
  3. package/dist/agent/domain/agent.js.map +1 -1
  4. package/dist/agent/domain/approval.d.ts +9 -0
  5. package/dist/agent/domain/approval.js.map +1 -1
  6. package/dist/agent/domain/runContext.d.ts +31 -0
  7. package/dist/agent/domain/runContext.js +47 -0
  8. package/dist/agent/domain/runContext.js.map +1 -0
  9. package/dist/diff/domain/unifiedDiff.d.ts +19 -0
  10. package/dist/diff/domain/unifiedDiff.js +166 -0
  11. package/dist/diff/domain/unifiedDiff.js.map +1 -0
  12. package/dist/engines/domain/model/historyFollower.d.ts +41 -0
  13. package/dist/engines/domain/model/historyFollower.js +158 -0
  14. package/dist/engines/domain/model/historyFollower.js.map +1 -0
  15. package/dist/engines/domain/model/llmAgent.d.ts +42 -4
  16. package/dist/engines/domain/model/llmAgent.js +223 -23
  17. package/dist/engines/domain/model/llmAgent.js.map +1 -1
  18. package/dist/factory/infrastructure/agentFactory.d.ts +14 -0
  19. package/dist/factory/infrastructure/agentFactory.js +20 -2
  20. package/dist/factory/infrastructure/agentFactory.js.map +1 -1
  21. package/dist/factory/infrastructure/agentLLMFactory.d.ts +119 -14
  22. package/dist/factory/infrastructure/agentLLMFactory.js +284 -57
  23. package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
  24. package/dist/index.d.ts +11 -3
  25. package/dist/index.js +5 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/llm/domain/conversation.d.ts +44 -4
  28. package/dist/llm/domain/conversation.js +56 -7
  29. package/dist/llm/domain/conversation.js.map +1 -1
  30. package/dist/llm/domain/llm.d.ts +3 -1
  31. package/dist/llm/domain/llm.js.map +1 -1
  32. package/dist/llm/domain/tool.d.ts +13 -0
  33. package/dist/llm/domain/tool.js +12 -1
  34. package/dist/llm/domain/tool.js.map +1 -1
  35. package/dist/llm/infrastructure/claudeLLMClient.d.ts +3 -2
  36. package/dist/llm/infrastructure/claudeLLMClient.js +38 -10
  37. package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
  38. package/dist/llm/infrastructure/openAILLMClient.d.ts +2 -1
  39. package/dist/llm/infrastructure/openAILLMClient.js +66 -16
  40. package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
  41. package/dist/orchestration/domain/model/orchestratorAgent.d.ts +11 -2
  42. package/dist/orchestration/domain/model/orchestratorAgent.js +43 -4
  43. package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
  44. package/dist/recovery/domain/fileChanges.d.ts +31 -0
  45. package/dist/recovery/domain/fileChanges.js +48 -0
  46. package/dist/recovery/domain/fileChanges.js.map +1 -0
  47. package/dist/recovery/domain/history.d.ts +114 -0
  48. package/dist/recovery/domain/history.js +62 -0
  49. package/dist/recovery/domain/history.js.map +1 -0
  50. package/dist/recovery/domain/recoveryStore.d.ts +233 -0
  51. package/dist/recovery/domain/recoveryStore.js +32 -0
  52. package/dist/recovery/domain/recoveryStore.js.map +1 -0
  53. package/dist/recovery/domain/retention.d.ts +33 -0
  54. package/dist/recovery/domain/retention.js +90 -0
  55. package/dist/recovery/domain/retention.js.map +1 -0
  56. package/dist/recovery/domain/runChanges.d.ts +37 -0
  57. package/dist/recovery/domain/runChanges.js +109 -0
  58. package/dist/recovery/domain/runChanges.js.map +1 -0
  59. package/dist/recovery/domain/runTree.d.ts +32 -0
  60. package/dist/recovery/domain/runTree.js +90 -0
  61. package/dist/recovery/domain/runTree.js.map +1 -0
  62. package/dist/recovery/infrastructure/createHistory.d.ts +11 -0
  63. package/dist/recovery/infrastructure/createHistory.js +17 -0
  64. package/dist/recovery/infrastructure/createHistory.js.map +1 -0
  65. package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +133 -0
  66. package/dist/recovery/infrastructure/fileRecoveryStore.js +1043 -0
  67. package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -0
  68. package/dist/recovery/infrastructure/workspaceFiles.d.ts +30 -0
  69. package/dist/recovery/infrastructure/workspaceFiles.js +125 -0
  70. package/dist/recovery/infrastructure/workspaceFiles.js.map +1 -0
  71. package/dist/tools/domain/accessPolicy.d.ts +30 -0
  72. package/dist/tools/domain/accessPolicy.js +11 -0
  73. package/dist/tools/domain/accessPolicy.js.map +1 -0
  74. package/dist/tools/domain/commands.d.ts +93 -0
  75. package/dist/tools/domain/commands.js +191 -0
  76. package/dist/tools/domain/commands.js.map +1 -0
  77. package/dist/tools/domain/editResponse.d.ts +14 -0
  78. package/dist/tools/domain/editResponse.js +79 -0
  79. package/dist/tools/domain/editResponse.js.map +1 -0
  80. package/dist/tools/domain/fileEdits.d.ts +64 -0
  81. package/dist/tools/domain/fileEdits.js +21 -0
  82. package/dist/tools/domain/fileEdits.js.map +1 -0
  83. package/dist/tools/domain/lineLength.d.ts +7 -0
  84. package/dist/tools/domain/lineLength.js +26 -0
  85. package/dist/tools/domain/lineLength.js.map +1 -0
  86. package/dist/tools/domain/preparedCall.d.ts +31 -0
  87. package/dist/tools/domain/preparedCall.js +14 -0
  88. package/dist/tools/domain/preparedCall.js.map +1 -0
  89. package/dist/tools/domain/readRegistry.d.ts +46 -0
  90. package/dist/tools/domain/readRegistry.js +62 -0
  91. package/dist/tools/domain/readRegistry.js.map +1 -0
  92. package/dist/tools/domain/textLines.d.ts +26 -0
  93. package/dist/tools/domain/textLines.js +44 -0
  94. package/dist/tools/domain/textLines.js.map +1 -0
  95. package/dist/tools/domain/v4aPatch.d.ts +17 -0
  96. package/dist/tools/domain/v4aPatch.js +236 -0
  97. package/dist/tools/domain/v4aPatch.js.map +1 -0
  98. package/dist/tools/infrastructure/boundedWorkspace.d.ts +11 -2
  99. package/dist/tools/infrastructure/boundedWorkspace.js +9 -1
  100. package/dist/tools/infrastructure/boundedWorkspace.js.map +1 -1
  101. package/dist/tools/infrastructure/commandCheckpoint.d.ts +55 -0
  102. package/dist/tools/infrastructure/commandCheckpoint.js +178 -0
  103. package/dist/tools/infrastructure/commandCheckpoint.js.map +1 -0
  104. package/dist/tools/infrastructure/commandProcess.d.ts +47 -0
  105. package/dist/tools/infrastructure/commandProcess.js +217 -0
  106. package/dist/tools/infrastructure/commandProcess.js.map +1 -0
  107. package/dist/tools/infrastructure/commandTool.d.ts +36 -0
  108. package/dist/tools/infrastructure/commandTool.js +254 -0
  109. package/dist/tools/infrastructure/commandTool.js.map +1 -0
  110. package/dist/tools/infrastructure/createWorkspace.d.ts +10 -0
  111. package/dist/tools/infrastructure/createWorkspace.js +11 -3
  112. package/dist/tools/infrastructure/createWorkspace.js.map +1 -1
  113. package/dist/tools/infrastructure/defineTool.d.ts +14 -0
  114. package/dist/tools/infrastructure/defineTool.js +28 -5
  115. package/dist/tools/infrastructure/defineTool.js.map +1 -1
  116. package/dist/tools/infrastructure/editFormats.d.ts +22 -0
  117. package/dist/tools/infrastructure/editFormats.js +211 -0
  118. package/dist/tools/infrastructure/editFormats.js.map +1 -0
  119. package/dist/tools/infrastructure/fileEditor.d.ts +64 -0
  120. package/dist/tools/infrastructure/fileEditor.js +306 -0
  121. package/dist/tools/infrastructure/fileEditor.js.map +1 -0
  122. package/dist/tools/infrastructure/fileTools.d.ts +81 -0
  123. package/dist/tools/infrastructure/fileTools.js +152 -0
  124. package/dist/tools/infrastructure/fileTools.js.map +1 -0
  125. package/dist/tools/infrastructure/gitIgnoreRules.d.ts +15 -0
  126. package/dist/tools/infrastructure/gitIgnoreRules.js +74 -0
  127. package/dist/tools/infrastructure/gitIgnoreRules.js.map +1 -0
  128. package/dist/tools/infrastructure/gitWorkspace.d.ts +1 -1
  129. package/dist/tools/infrastructure/gitWorkspace.js +1 -1
  130. package/dist/tools/infrastructure/gitWorkspace.js.map +1 -1
  131. package/dist/tools/infrastructure/ignoreFileChanges.d.ts +14 -0
  132. package/dist/tools/infrastructure/ignoreFileChanges.js +62 -0
  133. package/dist/tools/infrastructure/ignoreFileChanges.js.map +1 -0
  134. package/dist/tools/infrastructure/policyWorkspace.d.ts +52 -0
  135. package/dist/tools/infrastructure/policyWorkspace.js +32 -0
  136. package/dist/tools/infrastructure/policyWorkspace.js.map +1 -0
  137. package/dist/tools/infrastructure/ripgrepWorkspace.d.ts +2 -1
  138. package/dist/tools/infrastructure/ripgrepWorkspace.js +1 -1
  139. package/dist/tools/infrastructure/ripgrepWorkspace.js.map +1 -1
  140. package/dist/tools/infrastructure/rootsAccessPolicy.d.ts +100 -0
  141. package/dist/tools/infrastructure/rootsAccessPolicy.js +301 -0
  142. package/dist/tools/infrastructure/rootsAccessPolicy.js.map +1 -0
  143. package/dist/tools/infrastructure/runProcess.d.ts +4 -1
  144. package/dist/tools/infrastructure/runProcess.js +4 -2
  145. package/dist/tools/infrastructure/runProcess.js.map +1 -1
  146. package/dist/tools/infrastructure/secretPaths.d.ts +8 -0
  147. package/dist/tools/infrastructure/secretPaths.js +76 -0
  148. package/dist/tools/infrastructure/secretPaths.js.map +1 -0
  149. package/dist/tools/infrastructure/showChangesTool.d.ts +12 -0
  150. package/dist/tools/infrastructure/showChangesTool.js +43 -0
  151. package/dist/tools/infrastructure/showChangesTool.js.map +1 -0
  152. package/dist/tools/infrastructure/trackedEdits.d.ts +33 -0
  153. package/dist/tools/infrastructure/trackedEdits.js +70 -0
  154. package/dist/tools/infrastructure/trackedEdits.js.map +1 -0
  155. package/dist/tools/infrastructure/trackedReadFile.d.ts +21 -0
  156. package/dist/tools/infrastructure/trackedReadFile.js +78 -0
  157. package/dist/tools/infrastructure/trackedReadFile.js.map +1 -0
  158. package/dist/tools/infrastructure/workspaceTools.d.ts +28 -0
  159. package/dist/tools/infrastructure/workspaceTools.js +53 -40
  160. package/dist/tools/infrastructure/workspaceTools.js.map +1 -1
  161. package/dist/tools/infrastructure/workspaceWrites.d.ts +23 -0
  162. package/dist/tools/infrastructure/workspaceWrites.js +42 -0
  163. package/dist/tools/infrastructure/workspaceWrites.js.map +1 -0
  164. package/dist/tools/infrastructure/writeSession.d.ts +35 -0
  165. package/dist/tools/infrastructure/writeSession.js +81 -0
  166. package/dist/tools/infrastructure/writeSession.js.map +1 -0
  167. package/package.json +1 -1
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The read `Workspace` of the file tools, under their access policy.
3
+ *
4
+ * `listFiles` and `searchText` rely on the workspace beneath, which is built with the policy's
5
+ * `hidesFromReading` as its `hidden` filter. That filter leaves out a protected file, or a secret
6
+ * the host did not open, before anything is counted or stored, so no page, total, limit or error
7
+ * can tell that one matched. Its candidates are already free of what `.gitignore` excludes, are
8
+ * inside the root, and are never reached through a link: the program that lists them does not
9
+ * follow links, and links are not regular files. Running the whole policy on every path a
10
+ * listing returns would add nothing for these paths, and would cost one `git check-ignore` each.
11
+ *
12
+ * `readFile` is given a path by the model, which can be anything, so the whole policy decides it.
13
+ */
14
+ export class PolicyWorkspace {
15
+ inner;
16
+ policy;
17
+ constructor({ inner, policy }) {
18
+ this.inner = inner;
19
+ this.policy = policy;
20
+ }
21
+ listFiles(query, signal) {
22
+ return this.inner.listFiles(query, signal);
23
+ }
24
+ searchText(query, signal) {
25
+ return this.inner.searchText(query, signal);
26
+ }
27
+ async readFile(query, signal) {
28
+ await this.policy.check(query.path, 'read', signal);
29
+ return this.inner.readFile(query, signal);
30
+ }
31
+ }
32
+ //# sourceMappingURL=policyWorkspace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policyWorkspace.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/policyWorkspace.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,eAAe;IACV,KAAK,CAAY;IACjB,MAAM,CAAe;IAEtC,YAAY,EAAE,KAAK,EAAE,MAAM,EAA8C;QACxE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACtB,CAAC;IAED,SAAS,CACR,KAAsD,EACtD,MAAmB;QAEnB,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC5C,CAAC;IAED,UAAU,CACT,KAA4F,EAC5F,MAAmB;QAEnB,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,QAAQ,CACb,KAA4D,EAC5D,MAAmB;QAEnB,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QACpD,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC3C,CAAC;CACD","sourcesContent":["import type { AccessPolicy } from '../domain/accessPolicy.ts';\nimport type { TextMatch, Workspace } from '../domain/workspace.ts';\n\n/**\n * The read `Workspace` of the file tools, under their access policy.\n *\n * `listFiles` and `searchText` rely on the workspace beneath, which is built with the policy's\n * `hidesFromReading` as its `hidden` filter. That filter leaves out a protected file, or a secret\n * the host did not open, before anything is counted or stored, so no page, total, limit or error\n * can tell that one matched. Its candidates are already free of what `.gitignore` excludes, are\n * inside the root, and are never reached through a link: the program that lists them does not\n * follow links, and links are not regular files. Running the whole policy on every path a\n * listing returns would add nothing for these paths, and would cost one `git check-ignore` each.\n *\n * `readFile` is given a path by the model, which can be anything, so the whole policy decides it.\n */\nexport class PolicyWorkspace implements Workspace {\n\tprivate readonly inner: Workspace;\n\tprivate readonly policy: AccessPolicy;\n\n\tconstructor({ inner, policy }: { inner: Workspace; policy: AccessPolicy }) {\n\t\tthis.inner = inner;\n\t\tthis.policy = policy;\n\t}\n\n\tlistFiles(\n\t\tquery: { path?: string; glob?: string; limit: number },\n\t\tsignal: AbortSignal,\n\t): Promise<{ files: string[]; total: number; truncated: boolean }> {\n\t\treturn this.inner.listFiles(query, signal);\n\t}\n\n\tsearchText(\n\t\tquery: { pattern: string; ignoreCase: boolean; path?: string; glob?: string; limit: number },\n\t\tsignal: AbortSignal,\n\t): Promise<{ matches: TextMatch[]; total: number; truncated: boolean }> {\n\t\treturn this.inner.searchText(query, signal);\n\t}\n\n\tasync readFile(\n\t\tquery: { path: string; fromLine: number; lineCount: number },\n\t\tsignal: AbortSignal,\n\t): Promise<{ lines: string[]; totalLines: number; truncated: boolean }> {\n\t\tawait this.policy.check(query.path, 'read', signal);\n\t\treturn this.inner.readFile(query, signal);\n\t}\n}\n"]}
@@ -13,8 +13,9 @@ export declare class RipgrepWorkspace extends BoundedWorkspace {
13
13
  root: string;
14
14
  ripgrepPath: string;
15
15
  timeoutMs?: number;
16
+ hidden?: (path: string) => boolean;
16
17
  });
17
- protected listCandidates(signal: AbortSignal): Promise<string[]>;
18
+ protected listUnignored(signal: AbortSignal): Promise<string[]>;
18
19
  protected findMatches(pattern: string, ignoreCase: boolean, signal: AbortSignal, keep: (match: TextMatch) => void): Promise<void>;
19
20
  private ripgrep;
20
21
  }
@@ -35,7 +35,7 @@ export class RipgrepWorkspace extends BoundedWorkspace {
35
35
  super(options);
36
36
  this.ripgrepPath = ripgrepPath;
37
37
  }
38
- async listCandidates(signal) {
38
+ async listUnignored(signal) {
39
39
  const { stdout, stderr, exitCode } = await this.ripgrep(['--files', '--null'], signal);
40
40
  // 1 means nothing to list, which an empty repository is. 2 with a listing means some
41
41
  // folder could not be read, which git skips with a warning too; without one, it failed.
@@ -1 +1 @@
1
- {"version":3,"file":"ripgrepWorkspace.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/ripgrepWorkspace.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TextMatch } from '../domain/workspace.ts';\nimport { BoundedWorkspace } from './boundedWorkspace.ts';\nimport { firstLine, runProcess } from './runProcess.ts';\n\n// Every call sees what git would. Each flag is ours; nothing a model writes ever reaches the\n// command line except the pattern, and that only after `-e`.\nconst sameViewAsGit = [\n\t// A user's ripgrep configuration could switch the ignore rules off.\n\t'--no-config',\n\t// ripgrep's own `.ignore` and `.rgignore` outrank `.gitignore`, so a `!.env` line in either\n\t// would show a secret git hides. Only git's ignore sources count.\n\t'--no-ignore-dot',\n\t// Hidden files such as `.github/` are part of the repository; ripgrep skips them by default.\n\t'--hidden',\n\t// `.gitignore` applies even outside a git repository.\n\t'--no-require-git',\n\t// `--hidden` would otherwise search git's own folder.\n\t'--glob',\n\t'!.git',\n\t// Windows would print `\\`; every path in a `Workspace` is written with `/`.\n\t'--path-separator',\n\t'/',\n];\n\ninterface RipgrepText {\n\ttext?: string;\n\t// ripgrep sends what is not valid UTF-8 as base64 instead.\n\tbytes?: string;\n}\n\ninterface RipgrepEvent {\n\ttype: string;\n\tdata: { path: RipgrepText; line_number: number; lines: RipgrepText };\n}\n\nfunction decode({ text, bytes }: RipgrepText): string {\n\treturn text ?? Buffer.from(bytes ?? '', 'base64').toString('utf8');\n}\n\n/**\n * The preferred `Workspace`, on the ripgrep binary that ships with the package. It always runs\n * from the root with no path: ripgrep ignores nothing that it is given explicitly.\n *\n * It takes the binary's path instead of importing `@vscode/ripgrep`, which throws on import when\n * the platform has no binary; `createWorkspace` loads it where that failure can be handled.\n */\nexport class RipgrepWorkspace extends BoundedWorkspace {\n\tprivate readonly ripgrepPath: string;\n\n\tconstructor({\n\t\tripgrepPath,\n\t\t...options\n\t}: {\n\t\troot: string;\n\t\tripgrepPath: string;\n\t\ttimeoutMs?: number;\n\t}) {\n\t\tsuper(options);\n\t\tthis.ripgrepPath = ripgrepPath;\n\t}\n\n\tprotected async listCandidates(signal: AbortSignal): Promise<string[]> {\n\t\tconst { stdout, stderr, exitCode } = await this.ripgrep(['--files', '--null'], signal);\n\t\t// 1 means nothing to list, which an empty repository is. 2 with a listing means some\n\t\t// folder could not be read, which git skips with a warning too; without one, it failed.\n\t\tif (exitCode > 1 && stdout === '') {\n\t\t\tthrow new Error(`ripgrep could not list the files: ${firstLine(stderr)}`);\n\t\t}\n\n\t\treturn stdout\n\t\t\t.split('\\0')\n\t\t\t.filter(path => path !== '')\n\t\t\t.map(path => path.replace(/^\\.\\//, ''));\n\t}\n\n\tprotected async findMatches(\n\t\tpattern: string,\n\t\tignoreCase: boolean,\n\t\tsignal: AbortSignal,\n\t\tkeep: (match: TextMatch) => void,\n\t): Promise<void> {\n\t\t// ripgrep ends every search it ran with a summary. A failure before searching, such as a\n\t\t// pattern that does not compile, prints none. A count, not a flag: TypeScript cannot see\n\t\t// the callback change it.\n\t\tlet summaries = 0;\n\t\tconst { stderr, exitCode } = await this.ripgrep(\n\t\t\t['--json', ...(ignoreCase ? ['--ignore-case'] : []), '-e', pattern],\n\t\t\tsignal,\n\t\t\tline => {\n\t\t\t\tif (line === '') {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst event = JSON.parse(line) as RipgrepEvent;\n\t\t\t\tif (event.type === 'summary') {\n\t\t\t\t\tsummaries++;\n\t\t\t\t}\n\t\t\t\tif (event.type === 'match') {\n\t\t\t\t\tkeep({\n\t\t\t\t\t\tpath: decode(event.data.path).replace(/^\\.\\//, ''),\n\t\t\t\t\t\tline: event.data.line_number,\n\t\t\t\t\t\ttext: decode(event.data.lines).replace(/\\r?\\n$/, ''),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\n\t\t// 1 means no match. 2 after a search means some file could not be read, and the rest\n\t\t// stands, as it does on git; 2 with no search is the failure.\n\t\tif (exitCode > 1 && summaries === 0) {\n\t\t\tthrow new Error(`ripgrep could not search for the pattern: ${firstLine(stderr)}`);\n\t\t}\n\t}\n\n\tprivate ripgrep(args: string[], signal: AbortSignal, onLine?: (line: string) => void) {\n\t\treturn runProcess(this.ripgrepPath, [...sameViewAsGit, ...args], {\n\t\t\tcwd: this.root,\n\t\t\tsignal,\n\t\t\tonLine,\n\t\t});\n\t}\n}\n"]}
1
+ {"version":3,"file":"ripgrepWorkspace.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/ripgrepWorkspace.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TextMatch } from '../domain/workspace.ts';\nimport { BoundedWorkspace } from './boundedWorkspace.ts';\nimport { firstLine, runProcess } from './runProcess.ts';\n\n// Every call sees what git would. Each flag is ours; nothing a model writes ever reaches the\n// command line except the pattern, and that only after `-e`.\nconst sameViewAsGit = [\n\t// A user's ripgrep configuration could switch the ignore rules off.\n\t'--no-config',\n\t// ripgrep's own `.ignore` and `.rgignore` outrank `.gitignore`, so a `!.env` line in either\n\t// would show a secret git hides. Only git's ignore sources count.\n\t'--no-ignore-dot',\n\t// Hidden files such as `.github/` are part of the repository; ripgrep skips them by default.\n\t'--hidden',\n\t// `.gitignore` applies even outside a git repository.\n\t'--no-require-git',\n\t// `--hidden` would otherwise search git's own folder.\n\t'--glob',\n\t'!.git',\n\t// Windows would print `\\`; every path in a `Workspace` is written with `/`.\n\t'--path-separator',\n\t'/',\n];\n\ninterface RipgrepText {\n\ttext?: string;\n\t// ripgrep sends what is not valid UTF-8 as base64 instead.\n\tbytes?: string;\n}\n\ninterface RipgrepEvent {\n\ttype: string;\n\tdata: { path: RipgrepText; line_number: number; lines: RipgrepText };\n}\n\nfunction decode({ text, bytes }: RipgrepText): string {\n\treturn text ?? Buffer.from(bytes ?? '', 'base64').toString('utf8');\n}\n\n/**\n * The preferred `Workspace`, on the ripgrep binary that ships with the package. It always runs\n * from the root with no path: ripgrep ignores nothing that it is given explicitly.\n *\n * It takes the binary's path instead of importing `@vscode/ripgrep`, which throws on import when\n * the platform has no binary; `createWorkspace` loads it where that failure can be handled.\n */\nexport class RipgrepWorkspace extends BoundedWorkspace {\n\tprivate readonly ripgrepPath: string;\n\n\tconstructor({\n\t\tripgrepPath,\n\t\t...options\n\t}: {\n\t\troot: string;\n\t\tripgrepPath: string;\n\t\ttimeoutMs?: number;\n\t\thidden?: (path: string) => boolean;\n\t}) {\n\t\tsuper(options);\n\t\tthis.ripgrepPath = ripgrepPath;\n\t}\n\n\tprotected async listUnignored(signal: AbortSignal): Promise<string[]> {\n\t\tconst { stdout, stderr, exitCode } = await this.ripgrep(['--files', '--null'], signal);\n\t\t// 1 means nothing to list, which an empty repository is. 2 with a listing means some\n\t\t// folder could not be read, which git skips with a warning too; without one, it failed.\n\t\tif (exitCode > 1 && stdout === '') {\n\t\t\tthrow new Error(`ripgrep could not list the files: ${firstLine(stderr)}`);\n\t\t}\n\n\t\treturn stdout\n\t\t\t.split('\\0')\n\t\t\t.filter(path => path !== '')\n\t\t\t.map(path => path.replace(/^\\.\\//, ''));\n\t}\n\n\tprotected async findMatches(\n\t\tpattern: string,\n\t\tignoreCase: boolean,\n\t\tsignal: AbortSignal,\n\t\tkeep: (match: TextMatch) => void,\n\t): Promise<void> {\n\t\t// ripgrep ends every search it ran with a summary. A failure before searching, such as a\n\t\t// pattern that does not compile, prints none. A count, not a flag: TypeScript cannot see\n\t\t// the callback change it.\n\t\tlet summaries = 0;\n\t\tconst { stderr, exitCode } = await this.ripgrep(\n\t\t\t['--json', ...(ignoreCase ? ['--ignore-case'] : []), '-e', pattern],\n\t\t\tsignal,\n\t\t\tline => {\n\t\t\t\tif (line === '') {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst event = JSON.parse(line) as RipgrepEvent;\n\t\t\t\tif (event.type === 'summary') {\n\t\t\t\t\tsummaries++;\n\t\t\t\t}\n\t\t\t\tif (event.type === 'match') {\n\t\t\t\t\tkeep({\n\t\t\t\t\t\tpath: decode(event.data.path).replace(/^\\.\\//, ''),\n\t\t\t\t\t\tline: event.data.line_number,\n\t\t\t\t\t\ttext: decode(event.data.lines).replace(/\\r?\\n$/, ''),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\n\t\t// 1 means no match. 2 after a search means some file could not be read, and the rest\n\t\t// stands, as it does on git; 2 with no search is the failure.\n\t\tif (exitCode > 1 && summaries === 0) {\n\t\t\tthrow new Error(`ripgrep could not search for the pattern: ${firstLine(stderr)}`);\n\t\t}\n\t}\n\n\tprivate ripgrep(args: string[], signal: AbortSignal, onLine?: (line: string) => void) {\n\t\treturn runProcess(this.ripgrepPath, [...sameViewAsGit, ...args], {\n\t\t\tcwd: this.root,\n\t\t\tsignal,\n\t\t\tonLine,\n\t\t});\n\t}\n}\n"]}
@@ -0,0 +1,100 @@
1
+ import { type Access, type AccessPolicy, type AllowedPath, type WorkspaceRoot } from '../domain/accessPolicy.ts';
2
+ import type { IgnoreRules } from './gitIgnoreRules.ts';
3
+ /** The host's changes to the default secrets list. */
4
+ export interface SecretRules {
5
+ /** More files or folders to treat as secrets: absolute, `~/…`, or relative to the first root. */
6
+ protect?: string[];
7
+ /** Exact files the defaults would refuse that the host opens, for reading or also writing. */
8
+ allow?: {
9
+ path: string;
10
+ access: Access;
11
+ }[];
12
+ }
13
+ /**
14
+ * The access policy over the roots a host declared. A path is refused unless every step
15
+ * allows it, in this order:
16
+ *
17
+ * 1. Its real location, every symlink resolved, is inside a root. A symlink is followed, never
18
+ * refused for being one: what counts is where it leads.
19
+ * 2. That root allows the access: a `read` root refuses a write.
20
+ * 3. It is not protected: git's own files, and the host's protected paths such as the
21
+ * recovery store, whatever the model calls them.
22
+ * 4. It is not a secret, by the default list and the host's `protect`.
23
+ * 5. `.gitignore` does not exclude it, whether or not it exists yet, nor any symlink it goes
24
+ * through: git sees a link as an entry of its own, so an ignored link stays closed even when
25
+ * it leads somewhere that is not ignored.
26
+ *
27
+ * A file the host's `allow` names exactly skips steps 4 and 5: a `.env` is usually both a
28
+ * secret and ignored, and an opening that `.gitignore` still blocked would open nothing.
29
+ * Nothing opens what step 3 protects.
30
+ *
31
+ * The real location is what the caller reads or writes, so a symlink cannot be swapped for
32
+ * another between the check and the access without a second check. That window is the
33
+ * caller's to close; this policy holds for a host that does not race its own agent.
34
+ */
35
+ export declare class RootsAccessPolicy implements AccessPolicy {
36
+ private readonly roots;
37
+ private readonly protectedPaths;
38
+ private readonly secretFolders;
39
+ private readonly allowed;
40
+ private readonly ignoreRules;
41
+ private readonly home;
42
+ private constructor();
43
+ /**
44
+ * @throws {InvalidAgentConfigError} when there is no root, or one is not an existing folder
45
+ * or is inside git's metadata, which nothing opens.
46
+ */
47
+ static create({ roots, protectedPaths, secrets, ignoreRules, home, }: {
48
+ roots: WorkspaceRoot[];
49
+ /** Paths no tool may reach, such as the recovery store and the host's configuration. */
50
+ protectedPaths?: string[];
51
+ secrets?: SecretRules;
52
+ ignoreRules: IgnoreRules;
53
+ home?: string;
54
+ }): Promise<RootsAccessPolicy>;
55
+ check(path: string, access: Access, signal: AbortSignal): Promise<AllowedPath>;
56
+ /**
57
+ * Whether the policy closes a file to reading by its path alone: protected, or a secret the
58
+ * host did not open. Synchronous, so a search can leave such a file out before it counts a
59
+ * match; `check` still decides each path a tool is given. `absolute` is a real path.
60
+ */
61
+ hidesFromReading(absolute: string): boolean;
62
+ /**
63
+ * Whether a command's checkpoint leaves a file out, by its path alone: it is outside every
64
+ * `write` root, protected, or a secret the host did not open for writing. Such a file is
65
+ * neither copied before a command nor recorded after it. `absolute` is a real path.
66
+ */
67
+ closedToWriting(absolute: string): boolean;
68
+ private get base();
69
+ /**
70
+ * Where `named` really is. For a path that does not exist yet, that is its nearest existing
71
+ * folder, resolved, plus the missing names. A link that leads nowhere is refused: writing to
72
+ * it would create whatever it points at, wherever that is.
73
+ */
74
+ private realLocation;
75
+ /**
76
+ * Where a path that runs through a file would be: the deepest part of it that exists, which is
77
+ * that file, resolved, plus the names after it.
78
+ */
79
+ private throughFile;
80
+ /**
81
+ * Every symlink `named` goes through on the way to its real location, each at its own real
82
+ * place. git answers for a link but not for a path beyond one, so each is checked alone.
83
+ */
84
+ private linksAlong;
85
+ /** Whether `.gitignore` excludes a real location, by the root that holds it. */
86
+ private isIgnored;
87
+ /** The most specific root holding `absolute`, so a folder nested in another has its own say. */
88
+ private rootOf;
89
+ private isProtected;
90
+ /** Whether the host's `allow` names this exact file for this access; `write` covers reading. */
91
+ private isOpened;
92
+ private isSecret;
93
+ /**
94
+ * Whether `path` lies below a secret name inside its root, such as `.env/x`. A file there is
95
+ * as closed as the name: otherwise `.env/x` would be refused for running through `.env` when
96
+ * that file exists, and read as missing when it does not, telling the two apart. Folders above
97
+ * the root do not count, or a workspace inside `.env.d` would be closed whole.
98
+ */
99
+ private underSecretName;
100
+ }
@@ -0,0 +1,301 @@
1
+ import { lstat, readlink, realpath, stat } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { InvalidAgentConfigError } from '#src/shared/domain/errors';
5
+ import { AccessDeniedError, } from "../domain/accessPolicy.js";
6
+ import { isSecretName, isSecretPath } from "./secretPaths.js";
7
+ /** `path` relative to `folder`, or undefined when it is not inside it. */
8
+ function within(folder, path) {
9
+ const below = relative(folder, path);
10
+ if (below === '..' || below.startsWith(`..${sep}`) || isAbsolute(below))
11
+ return undefined;
12
+ return below;
13
+ }
14
+ /** The same test without case, for names a case-insensitive disk would treat as one. */
15
+ function withinIgnoringCase(folder, path) {
16
+ return within(folder.toLowerCase(), path.toLowerCase()) !== undefined;
17
+ }
18
+ /** Whether `path` goes through git's metadata: `.git` as a folder or a worktree's pointer file. */
19
+ function hasGitPart(path) {
20
+ return path.split(sep).some(part => part.toLowerCase() === '.git');
21
+ }
22
+ function secretRefusal(path) {
23
+ return new AccessDeniedError(`"${path}" may hold secrets, so the harness keeps it closed; ask the user to allow it in the configuration if it is needed`);
24
+ }
25
+ /** A path the filesystem would not resolve, named only as the model wrote it. */
26
+ function unreachable(shown, error) {
27
+ return new AccessDeniedError(`Could not reach "${shown}" (${code(error) ?? 'unknown error'})`);
28
+ }
29
+ function code(error) {
30
+ return error.code;
31
+ }
32
+ /**
33
+ * The access policy over the roots a host declared. A path is refused unless every step
34
+ * allows it, in this order:
35
+ *
36
+ * 1. Its real location, every symlink resolved, is inside a root. A symlink is followed, never
37
+ * refused for being one: what counts is where it leads.
38
+ * 2. That root allows the access: a `read` root refuses a write.
39
+ * 3. It is not protected: git's own files, and the host's protected paths such as the
40
+ * recovery store, whatever the model calls them.
41
+ * 4. It is not a secret, by the default list and the host's `protect`.
42
+ * 5. `.gitignore` does not exclude it, whether or not it exists yet, nor any symlink it goes
43
+ * through: git sees a link as an entry of its own, so an ignored link stays closed even when
44
+ * it leads somewhere that is not ignored.
45
+ *
46
+ * A file the host's `allow` names exactly skips steps 4 and 5: a `.env` is usually both a
47
+ * secret and ignored, and an opening that `.gitignore` still blocked would open nothing.
48
+ * Nothing opens what step 3 protects.
49
+ *
50
+ * The real location is what the caller reads or writes, so a symlink cannot be swapped for
51
+ * another between the check and the access without a second check. That window is the
52
+ * caller's to close; this policy holds for a host that does not race its own agent.
53
+ */
54
+ export class RootsAccessPolicy {
55
+ roots;
56
+ protectedPaths;
57
+ secretFolders;
58
+ allowed;
59
+ ignoreRules;
60
+ home;
61
+ constructor(options) {
62
+ this.roots = options.roots;
63
+ this.protectedPaths = options.protectedPaths;
64
+ this.secretFolders = options.secretFolders;
65
+ this.allowed = options.allowed;
66
+ this.ignoreRules = options.ignoreRules;
67
+ this.home = options.home;
68
+ }
69
+ /**
70
+ * @throws {InvalidAgentConfigError} when there is no root, or one is not an existing folder
71
+ * or is inside git's metadata, which nothing opens.
72
+ */
73
+ static async create({ roots, protectedPaths = [], secrets = {}, ignoreRules, home = homedir(), }) {
74
+ const [first] = roots;
75
+ if (!first)
76
+ throw new InvalidAgentConfigError('A workspace needs at least one root');
77
+ const realRoots = await Promise.all(roots.map(async (root) => {
78
+ const real = await realpath(root.path).catch(() => undefined);
79
+ const isFolder = real !== undefined && (await stat(real)).isDirectory();
80
+ if (!isFolder) {
81
+ throw new InvalidAgentConfigError(`Workspace root ${root.path} is not an existing folder`);
82
+ }
83
+ // Protection looks for `.git` below a root, so a root inside it would open it.
84
+ if (hasGitPart(resolve(root.path)) || hasGitPart(real)) {
85
+ throw new InvalidAgentConfigError(`Workspace root ${root.path} is inside git's metadata, which no tool may reach`);
86
+ }
87
+ return { path: real, access: root.access };
88
+ }));
89
+ const base = realRoots[0]?.path ?? first.path;
90
+ // A host path may start with `~/`, or be relative to the first root.
91
+ const hostPath = async (path) => {
92
+ const absolute = path.startsWith('~/') ? join(home, path.slice(2)) : resolve(base, path);
93
+ // Resolved when it exists, so a protected folder behind a symlink is still found.
94
+ return realpath(absolute).catch(() => absolute);
95
+ };
96
+ return new RootsAccessPolicy({
97
+ roots: realRoots,
98
+ protectedPaths: await Promise.all(protectedPaths.map(hostPath)),
99
+ secretFolders: await Promise.all((secrets.protect ?? []).map(hostPath)),
100
+ allowed: await Promise.all((secrets.allow ?? []).map(async ({ path, access }) => ({
101
+ path: await hostPath(path),
102
+ access,
103
+ }))),
104
+ ignoreRules,
105
+ home,
106
+ });
107
+ }
108
+ async check(path, access, signal) {
109
+ if (path === '' || path.includes('\0')) {
110
+ throw new AccessDeniedError(`"${path}" is not a valid path`);
111
+ }
112
+ const named = resolve(this.base, path);
113
+ const { absolute, throughFile } = await this.realLocation(named, path);
114
+ // A path that runs through a file is judged like any other first, from where that file
115
+ // really is, and refused for it only if nothing else refuses it. Otherwise the answer
116
+ // would tell whether a file the policy hides exists, such as `.env` in `.env/x`.
117
+ const allowed = (found) => {
118
+ if (throughFile)
119
+ throw new AccessDeniedError(`"${path}" treats a file as a folder`);
120
+ return found;
121
+ };
122
+ const root = this.rootOf(absolute);
123
+ if (!root)
124
+ throw new AccessDeniedError(`"${path}" is outside the workspace`);
125
+ if (access === 'write' && root.access === 'read') {
126
+ throw new AccessDeniedError(`"${path}" is in a read-only folder of the workspace`);
127
+ }
128
+ const relativePath = (within(root.path, absolute) ?? '').split(sep).join('/');
129
+ if (this.isProtected(named, absolute)) {
130
+ throw new AccessDeniedError(`"${path}" is protected by the harness`);
131
+ }
132
+ if (this.isOpened(absolute, access)) {
133
+ return allowed({ absolute, root: root.path, relative: relativePath });
134
+ }
135
+ if (this.isSecret(named, absolute)) {
136
+ throw secretRefusal(path);
137
+ }
138
+ for (const location of [...(await this.linksAlong(named)), absolute]) {
139
+ if (await this.isIgnored(location, signal)) {
140
+ throw new AccessDeniedError(`"${path}" is excluded by .gitignore`);
141
+ }
142
+ }
143
+ return allowed({ absolute, root: root.path, relative: relativePath });
144
+ }
145
+ /**
146
+ * Whether the policy closes a file to reading by its path alone: protected, or a secret the
147
+ * host did not open. Synchronous, so a search can leave such a file out before it counts a
148
+ * match; `check` still decides each path a tool is given. `absolute` is a real path.
149
+ */
150
+ hidesFromReading(absolute) {
151
+ if (this.isProtected(absolute, absolute))
152
+ return true;
153
+ return !this.isOpened(absolute, 'read') && this.isSecret(absolute, absolute);
154
+ }
155
+ /**
156
+ * Whether a command's checkpoint leaves a file out, by its path alone: it is outside every
157
+ * `write` root, protected, or a secret the host did not open for writing. Such a file is
158
+ * neither copied before a command nor recorded after it. `absolute` is a real path.
159
+ */
160
+ closedToWriting(absolute) {
161
+ const root = this.rootOf(absolute);
162
+ if (root?.access !== 'write')
163
+ return true;
164
+ if (this.isProtected(absolute, absolute))
165
+ return true;
166
+ return !this.isOpened(absolute, 'write') && this.isSecret(absolute, absolute);
167
+ }
168
+ get base() {
169
+ return this.roots[0]?.path ?? '';
170
+ }
171
+ /**
172
+ * Where `named` really is. For a path that does not exist yet, that is its nearest existing
173
+ * folder, resolved, plus the missing names. A link that leads nowhere is refused: writing to
174
+ * it would create whatever it points at, wherever that is.
175
+ */
176
+ async realLocation(named, shown) {
177
+ const missing = [];
178
+ let existing = named;
179
+ for (;;) {
180
+ try {
181
+ return { absolute: join(await realpath(existing), ...missing), throughFile: false };
182
+ }
183
+ catch (error) {
184
+ if (code(error) === 'ENOTDIR')
185
+ return this.throughFile(existing, missing, shown);
186
+ // Such as a loop of links, or a folder it may not enter. Its own message would
187
+ // name the host path.
188
+ if (code(error) !== 'ENOENT')
189
+ throw unreachable(shown, error);
190
+ }
191
+ const isLink = await lstat(existing).then(stats => stats.isSymbolicLink(), () => false);
192
+ if (isLink)
193
+ throw new AccessDeniedError(`"${shown}" is a link to nothing`);
194
+ const parent = dirname(existing);
195
+ // The filesystem root always exists, so this never runs out.
196
+ missing.unshift(basename(existing));
197
+ existing = parent;
198
+ }
199
+ }
200
+ /**
201
+ * Where a path that runs through a file would be: the deepest part of it that exists, which is
202
+ * that file, resolved, plus the names after it.
203
+ */
204
+ async throughFile(named, missing, shown) {
205
+ const after = [...missing];
206
+ let existing = named;
207
+ while (!(await lstat(existing).then(() => true, () => false))) {
208
+ after.unshift(basename(existing));
209
+ existing = dirname(existing);
210
+ }
211
+ try {
212
+ return { absolute: join(await realpath(existing), ...after), throughFile: true };
213
+ }
214
+ catch (error) {
215
+ throw unreachable(shown, error);
216
+ }
217
+ }
218
+ /**
219
+ * Every symlink `named` goes through on the way to its real location, each at its own real
220
+ * place. git answers for a link but not for a path beyond one, so each is checked alone.
221
+ */
222
+ async linksAlong(named) {
223
+ const links = [];
224
+ let pending = named.split(sep).filter(part => part !== '');
225
+ let current = sep;
226
+ // `realLocation` already refused a loop; the bound only keeps this walk finite.
227
+ for (let hops = 0; pending.length > 0 && hops < 40;) {
228
+ const [name = '', ...rest] = pending;
229
+ const next = join(current, name);
230
+ const stats = await lstat(next).catch(() => undefined);
231
+ // Missing from here on: nothing below can be a link.
232
+ if (!stats)
233
+ break;
234
+ if (stats.isSymbolicLink()) {
235
+ hops++;
236
+ links.push(join(await realpath(current), name));
237
+ const target = resolve(current, await readlink(next));
238
+ pending = [...target.split(sep).filter(part => part !== ''), ...rest];
239
+ current = sep;
240
+ }
241
+ else {
242
+ current = next;
243
+ pending = rest;
244
+ }
245
+ }
246
+ return links;
247
+ }
248
+ /** Whether `.gitignore` excludes a real location, by the root that holds it. */
249
+ async isIgnored(location, signal) {
250
+ const root = this.rootOf(location);
251
+ const below = root && within(root.path, location);
252
+ // A root itself, or a link outside every root, has no rule to answer to.
253
+ if (!root || !below)
254
+ return false;
255
+ return this.ignoreRules.isIgnored(root.path, below.split(sep).join('/'), signal);
256
+ }
257
+ /** The most specific root holding `absolute`, so a folder nested in another has its own say. */
258
+ rootOf(absolute) {
259
+ return this.roots
260
+ .filter(root => within(root.path, absolute) !== undefined)
261
+ .sort((a, b) => b.path.length - a.path.length)[0];
262
+ }
263
+ isProtected(named, absolute) {
264
+ // `.git` as a folder or as a worktree's pointer file, under any name the model used.
265
+ const isGitMetadata = (path) => {
266
+ const root = this.rootOf(path);
267
+ if (!root)
268
+ return false;
269
+ return hasGitPart(within(root.path, path) ?? '');
270
+ };
271
+ return (isGitMetadata(named) ||
272
+ isGitMetadata(absolute) ||
273
+ this.protectedPaths.some(folder => withinIgnoringCase(folder, named) || withinIgnoringCase(folder, absolute)));
274
+ }
275
+ /** Whether the host's `allow` names this exact file for this access; `write` covers reading. */
276
+ isOpened(absolute, access) {
277
+ return this.allowed.some(allowed =>
278
+ // Exact: both are real paths in the disk's own case, and on a case-sensitive disk
279
+ // `.ENV` is a different file from the `.env` the host opened.
280
+ allowed.path === absolute && (allowed.access === 'write' || access === 'read'));
281
+ }
282
+ isSecret(named, absolute) {
283
+ return [named, absolute].some(path => isSecretPath(path, this.home) ||
284
+ this.underSecretName(path) ||
285
+ this.secretFolders.some(folder => withinIgnoringCase(folder, path)));
286
+ }
287
+ /**
288
+ * Whether `path` lies below a secret name inside its root, such as `.env/x`. A file there is
289
+ * as closed as the name: otherwise `.env/x` would be refused for running through `.env` when
290
+ * that file exists, and read as missing when it does not, telling the two apart. Folders above
291
+ * the root do not count, or a workspace inside `.env.d` would be closed whole.
292
+ */
293
+ underSecretName(path) {
294
+ const root = this.rootOf(path);
295
+ const below = root && within(root.path, path);
296
+ if (!below)
297
+ return false;
298
+ return below.split(sep).slice(0, -1).some(isSecretName);
299
+ }
300
+ }
301
+ //# sourceMappingURL=rootsAccessPolicy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rootsAccessPolicy.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/rootsAccessPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACxF,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAEN,iBAAiB,GAIjB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAU9D,0EAA0E;AAC1E,SAAS,MAAM,CAAC,MAAc,EAAE,IAAY;IAC3C,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACrC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC1F,OAAO,KAAK,CAAC;AACd,CAAC;AAED,wFAAwF;AACxF,SAAS,kBAAkB,CAAC,MAAc,EAAE,IAAY;IACvD,OAAO,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,SAAS,CAAC;AACvE,CAAC;AAED,mGAAmG;AACnG,SAAS,UAAU,CAAC,IAAY;IAC/B,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,CAAC;AACpE,CAAC;AAED,SAAS,aAAa,CAAC,IAAY;IAClC,OAAO,IAAI,iBAAiB,CAC3B,IAAI,IAAI,mHAAmH,CAC3H,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,SAAS,WAAW,CAAC,KAAa,EAAE,KAAc;IACjD,OAAO,IAAI,iBAAiB,CAAC,oBAAoB,KAAK,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,eAAe,GAAG,CAAC,CAAC;AAChG,CAAC;AAED,SAAS,IAAI,CAAC,KAAc;IAC3B,OAAQ,KAA+B,CAAC,IAAI,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,OAAO,iBAAiB;IACZ,KAAK,CAAkB;IACvB,cAAc,CAAW;IACzB,aAAa,CAAW;IACxB,OAAO,CAAqC;IAC5C,WAAW,CAAc;IACzB,IAAI,CAAS;IAE9B,YAAoB,OAOnB;QACA,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;QAC7C,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;QAC3C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;QACvC,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1B,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EACnB,KAAK,EACL,cAAc,GAAG,EAAE,EACnB,OAAO,GAAG,EAAE,EACZ,WAAW,EACX,IAAI,GAAG,OAAO,EAAE,GAQhB;QACA,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,uBAAuB,CAAC,qCAAqC,CAAC,CAAC;QAErF,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,GAAG,CAClC,KAAK,CAAC,GAAG,CAAC,KAAK,EAAC,IAAI,EAAC,EAAE;YACtB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YAC9D,MAAM,QAAQ,GAAG,IAAI,KAAK,SAAS,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;YACxE,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACf,MAAM,IAAI,uBAAuB,CAChC,kBAAkB,IAAI,CAAC,IAAI,4BAA4B,CACvD,CAAC;YACH,CAAC;YACD,+EAA+E;YAC/E,IAAI,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxD,MAAM,IAAI,uBAAuB,CAChC,kBAAkB,IAAI,CAAC,IAAI,oDAAoD,CAC/E,CAAC;YACH,CAAC;YACD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;QAC5C,CAAC,CAAC,CACF,CAAC;QACF,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC;QAC9C,qEAAqE;QACrE,MAAM,QAAQ,GAAG,KAAK,EAAE,IAAY,EAAE,EAAE;YACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YACzF,kFAAkF;YAClF,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;QACjD,CAAC,CAAC;QAEF,OAAO,IAAI,iBAAiB,CAAC;YAC5B,KAAK,EAAE,SAAS;YAChB,cAAc,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC/D,aAAa,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACvE,OAAO,EAAE,MAAM,OAAO,CAAC,GAAG,CACzB,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;gBACtD,IAAI,EAAE,MAAM,QAAQ,CAAC,IAAI,CAAC;gBAC1B,MAAM;aACN,CAAC,CAAC,CACH;YACD,WAAW;YACX,IAAI;SACJ,CAAC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAY,EAAE,MAAc,EAAE,MAAmB;QAC5D,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACxC,MAAM,IAAI,iBAAiB,CAAC,IAAI,IAAI,uBAAuB,CAAC,CAAC;QAC9D,CAAC;QACD,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACvC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACvE,uFAAuF;QACvF,sFAAsF;QACtF,iFAAiF;QACjF,MAAM,OAAO,GAAG,CAAC,KAAkB,EAAe,EAAE;YACnD,IAAI,WAAW;gBAAE,MAAM,IAAI,iBAAiB,CAAC,IAAI,IAAI,6BAA6B,CAAC,CAAC;YACpF,OAAO,KAAK,CAAC;QACd,CAAC,CAAC;QAEF,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,iBAAiB,CAAC,IAAI,IAAI,4BAA4B,CAAC,CAAC;QAC7E,IAAI,MAAM,KAAK,OAAO,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAClD,MAAM,IAAI,iBAAiB,CAAC,IAAI,IAAI,6CAA6C,CAAC,CAAC;QACpF,CAAC;QAED,MAAM,YAAY,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC9E,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,CAAC;YACvC,MAAM,IAAI,iBAAiB,CAAC,IAAI,IAAI,+BAA+B,CAAC,CAAC;QACtE,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;YACrC,OAAO,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC;QACvE,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,CAAC;YACpC,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;QACD,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC;YACtE,IAAI,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;gBAC5C,MAAM,IAAI,iBAAiB,CAAC,IAAI,IAAI,6BAA6B,CAAC,CAAC;YACpE,CAAC;QACF,CAAC;QAED,OAAO,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC;IACvE,CAAC;IAED;;;;OAIG;IACH,gBAAgB,CAAC,QAAgB;QAChC,IAAI,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QACtD,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC9E,CAAC;IAED;;;;OAIG;IACH,eAAe,CAAC,QAAgB;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,IAAI,EAAE,MAAM,KAAK,OAAO;YAAE,OAAO,IAAI,CAAC;QAC1C,IAAI,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QACtD,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/E,CAAC;IAED,IAAY,IAAI;QACf,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC;IAClC,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,YAAY,CACzB,KAAa,EACb,KAAa;QAEb,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,SAAS,CAAC;YACT,IAAI,CAAC;gBACJ,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,QAAQ,CAAC,QAAQ,CAAC,EAAE,GAAG,OAAO,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;YACrF,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,SAAS;oBAAE,OAAO,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;gBACjF,+EAA+E;gBAC/E,sBAAsB;gBACtB,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,QAAQ;oBAAE,MAAM,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAC/D,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC,IAAI,CACxC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,cAAc,EAAE,EAC/B,GAAG,EAAE,CAAC,KAAK,CACX,CAAC;YACF,IAAI,MAAM;gBAAE,MAAM,IAAI,iBAAiB,CAAC,IAAI,KAAK,wBAAwB,CAAC,CAAC;YAE3E,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;YACjC,6DAA6D;YAC7D,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;YACpC,QAAQ,GAAG,MAAM,CAAC;QACnB,CAAC;IACF,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,WAAW,CACxB,KAAa,EACb,OAAiB,EACjB,KAAa;QAEb,MAAM,KAAK,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC;QAC3B,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,OACC,CAAC,CAAC,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC,IAAI,CAC3B,GAAG,EAAE,CAAC,IAAI,EACV,GAAG,EAAE,CAAC,KAAK,CACX,CAAC,EACD,CAAC;YACF,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;YAClC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC9B,CAAC;QACD,IAAI,CAAC;YACJ,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,QAAQ,CAAC,QAAQ,CAAC,EAAE,GAAG,KAAK,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QAClF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACjC,CAAC;IACF,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,UAAU,CAAC,KAAa;QACrC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC;QAC3D,IAAI,OAAO,GAAW,GAAG,CAAC;QAC1B,gFAAgF;QAChF,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,GAAG,EAAE,GAAG,CAAC;YACrD,MAAM,CAAC,IAAI,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC;YACrC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YACjC,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YACvD,qDAAqD;YACrD,IAAI,CAAC,KAAK;gBAAE,MAAM;YAClB,IAAI,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;gBAC5B,IAAI,EAAE,CAAC;gBACP,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;gBAChD,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;gBACtD,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;gBACtE,OAAO,GAAG,GAAG,CAAC;YACf,CAAC;iBAAM,CAAC;gBACP,OAAO,GAAG,IAAI,CAAC;gBACf,OAAO,GAAG,IAAI,CAAC;YAChB,CAAC;QACF,CAAC;QACD,OAAO,KAAK,CAAC;IACd,CAAC;IAED,gFAAgF;IACxE,KAAK,CAAC,SAAS,CAAC,QAAgB,EAAE,MAAmB;QAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACnC,MAAM,KAAK,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAClD,yEAAyE;QACzE,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAC;QAClC,OAAO,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IAClF,CAAC;IAED,gGAAgG;IACxF,MAAM,CAAC,QAAgB;QAC9B,OAAO,IAAI,CAAC,KAAK;aACf,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,SAAS,CAAC;aACzD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,CAAC;IAEO,WAAW,CAAC,KAAa,EAAE,QAAgB;QAClD,qFAAqF;QACrF,MAAM,aAAa,GAAG,CAAC,IAAY,EAAE,EAAE;YACtC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC/B,IAAI,CAAC,IAAI;gBAAE,OAAO,KAAK,CAAC;YACxB,OAAO,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAClD,CAAC,CAAC;QAEF,OAAO,CACN,aAAa,CAAC,KAAK,CAAC;YACpB,aAAa,CAAC,QAAQ,CAAC;YACvB,IAAI,CAAC,cAAc,CAAC,IAAI,CACvB,MAAM,CAAC,EAAE,CAAC,kBAAkB,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,kBAAkB,CAAC,MAAM,EAAE,QAAQ,CAAC,CACnF,CACD,CAAC;IACH,CAAC;IAED,gGAAgG;IACxF,QAAQ,CAAC,QAAgB,EAAE,MAAc;QAChD,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CACvB,OAAO,CAAC,EAAE;QACT,kFAAkF;QAClF,8DAA8D;QAC9D,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,OAAO,IAAI,MAAM,KAAK,MAAM,CAAC,CAC/E,CAAC;IACH,CAAC;IAEO,QAAQ,CAAC,KAAa,EAAE,QAAgB;QAC/C,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,IAAI,CAC5B,IAAI,CAAC,EAAE,CACN,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC;YAC7B,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC;YAC1B,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CACpE,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACK,eAAe,CAAC,IAAY;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,KAAK,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC9C,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAC;QACzB,OAAO,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACzD,CAAC;CACD","sourcesContent":["import { lstat, readlink, realpath, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\nimport { InvalidAgentConfigError } from '#src/shared/domain/errors';\nimport {\n\ttype Access,\n\tAccessDeniedError,\n\ttype AccessPolicy,\n\ttype AllowedPath,\n\ttype WorkspaceRoot,\n} from '../domain/accessPolicy.ts';\nimport type { IgnoreRules } from './gitIgnoreRules.ts';\nimport { isSecretName, isSecretPath } from './secretPaths.ts';\n\n/** The host's changes to the default secrets list. */\nexport interface SecretRules {\n\t/** More files or folders to treat as secrets: absolute, `~/…`, or relative to the first root. */\n\tprotect?: string[];\n\t/** Exact files the defaults would refuse that the host opens, for reading or also writing. */\n\tallow?: { path: string; access: Access }[];\n}\n\n/** `path` relative to `folder`, or undefined when it is not inside it. */\nfunction within(folder: string, path: string): string | undefined {\n\tconst below = relative(folder, path);\n\tif (below === '..' || below.startsWith(`..${sep}`) || isAbsolute(below)) return undefined;\n\treturn below;\n}\n\n/** The same test without case, for names a case-insensitive disk would treat as one. */\nfunction withinIgnoringCase(folder: string, path: string): boolean {\n\treturn within(folder.toLowerCase(), path.toLowerCase()) !== undefined;\n}\n\n/** Whether `path` goes through git's metadata: `.git` as a folder or a worktree's pointer file. */\nfunction hasGitPart(path: string): boolean {\n\treturn path.split(sep).some(part => part.toLowerCase() === '.git');\n}\n\nfunction secretRefusal(path: string): AccessDeniedError {\n\treturn new AccessDeniedError(\n\t\t`\"${path}\" may hold secrets, so the harness keeps it closed; ask the user to allow it in the configuration if it is needed`,\n\t);\n}\n\n/** A path the filesystem would not resolve, named only as the model wrote it. */\nfunction unreachable(shown: string, error: unknown): AccessDeniedError {\n\treturn new AccessDeniedError(`Could not reach \"${shown}\" (${code(error) ?? 'unknown error'})`);\n}\n\nfunction code(error: unknown): string | undefined {\n\treturn (error as NodeJS.ErrnoException).code;\n}\n\n/**\n * The access policy over the roots a host declared. A path is refused unless every step\n * allows it, in this order:\n *\n * 1. Its real location, every symlink resolved, is inside a root. A symlink is followed, never\n * refused for being one: what counts is where it leads.\n * 2. That root allows the access: a `read` root refuses a write.\n * 3. It is not protected: git's own files, and the host's protected paths such as the\n * recovery store, whatever the model calls them.\n * 4. It is not a secret, by the default list and the host's `protect`.\n * 5. `.gitignore` does not exclude it, whether or not it exists yet, nor any symlink it goes\n * through: git sees a link as an entry of its own, so an ignored link stays closed even when\n * it leads somewhere that is not ignored.\n *\n * A file the host's `allow` names exactly skips steps 4 and 5: a `.env` is usually both a\n * secret and ignored, and an opening that `.gitignore` still blocked would open nothing.\n * Nothing opens what step 3 protects.\n *\n * The real location is what the caller reads or writes, so a symlink cannot be swapped for\n * another between the check and the access without a second check. That window is the\n * caller's to close; this policy holds for a host that does not race its own agent.\n */\nexport class RootsAccessPolicy implements AccessPolicy {\n\tprivate readonly roots: WorkspaceRoot[];\n\tprivate readonly protectedPaths: string[];\n\tprivate readonly secretFolders: string[];\n\tprivate readonly allowed: { path: string; access: Access }[];\n\tprivate readonly ignoreRules: IgnoreRules;\n\tprivate readonly home: string;\n\n\tprivate constructor(options: {\n\t\troots: WorkspaceRoot[];\n\t\tprotectedPaths: string[];\n\t\tsecretFolders: string[];\n\t\tallowed: { path: string; access: Access }[];\n\t\tignoreRules: IgnoreRules;\n\t\thome: string;\n\t}) {\n\t\tthis.roots = options.roots;\n\t\tthis.protectedPaths = options.protectedPaths;\n\t\tthis.secretFolders = options.secretFolders;\n\t\tthis.allowed = options.allowed;\n\t\tthis.ignoreRules = options.ignoreRules;\n\t\tthis.home = options.home;\n\t}\n\n\t/**\n\t * @throws {InvalidAgentConfigError} when there is no root, or one is not an existing folder\n\t * or is inside git's metadata, which nothing opens.\n\t */\n\tstatic async create({\n\t\troots,\n\t\tprotectedPaths = [],\n\t\tsecrets = {},\n\t\tignoreRules,\n\t\thome = homedir(),\n\t}: {\n\t\troots: WorkspaceRoot[];\n\t\t/** Paths no tool may reach, such as the recovery store and the host's configuration. */\n\t\tprotectedPaths?: string[];\n\t\tsecrets?: SecretRules;\n\t\tignoreRules: IgnoreRules;\n\t\thome?: string;\n\t}): Promise<RootsAccessPolicy> {\n\t\tconst [first] = roots;\n\t\tif (!first) throw new InvalidAgentConfigError('A workspace needs at least one root');\n\n\t\tconst realRoots = await Promise.all(\n\t\t\troots.map(async root => {\n\t\t\t\tconst real = await realpath(root.path).catch(() => undefined);\n\t\t\t\tconst isFolder = real !== undefined && (await stat(real)).isDirectory();\n\t\t\t\tif (!isFolder) {\n\t\t\t\t\tthrow new InvalidAgentConfigError(\n\t\t\t\t\t\t`Workspace root ${root.path} is not an existing folder`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\t// Protection looks for `.git` below a root, so a root inside it would open it.\n\t\t\t\tif (hasGitPart(resolve(root.path)) || hasGitPart(real)) {\n\t\t\t\t\tthrow new InvalidAgentConfigError(\n\t\t\t\t\t\t`Workspace root ${root.path} is inside git's metadata, which no tool may reach`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn { path: real, access: root.access };\n\t\t\t}),\n\t\t);\n\t\tconst base = realRoots[0]?.path ?? first.path;\n\t\t// A host path may start with `~/`, or be relative to the first root.\n\t\tconst hostPath = async (path: string) => {\n\t\t\tconst absolute = path.startsWith('~/') ? join(home, path.slice(2)) : resolve(base, path);\n\t\t\t// Resolved when it exists, so a protected folder behind a symlink is still found.\n\t\t\treturn realpath(absolute).catch(() => absolute);\n\t\t};\n\n\t\treturn new RootsAccessPolicy({\n\t\t\troots: realRoots,\n\t\t\tprotectedPaths: await Promise.all(protectedPaths.map(hostPath)),\n\t\t\tsecretFolders: await Promise.all((secrets.protect ?? []).map(hostPath)),\n\t\t\tallowed: await Promise.all(\n\t\t\t\t(secrets.allow ?? []).map(async ({ path, access }) => ({\n\t\t\t\t\tpath: await hostPath(path),\n\t\t\t\t\taccess,\n\t\t\t\t})),\n\t\t\t),\n\t\t\tignoreRules,\n\t\t\thome,\n\t\t});\n\t}\n\n\tasync check(path: string, access: Access, signal: AbortSignal): Promise<AllowedPath> {\n\t\tif (path === '' || path.includes('\\0')) {\n\t\t\tthrow new AccessDeniedError(`\"${path}\" is not a valid path`);\n\t\t}\n\t\tconst named = resolve(this.base, path);\n\t\tconst { absolute, throughFile } = await this.realLocation(named, path);\n\t\t// A path that runs through a file is judged like any other first, from where that file\n\t\t// really is, and refused for it only if nothing else refuses it. Otherwise the answer\n\t\t// would tell whether a file the policy hides exists, such as `.env` in `.env/x`.\n\t\tconst allowed = (found: AllowedPath): AllowedPath => {\n\t\t\tif (throughFile) throw new AccessDeniedError(`\"${path}\" treats a file as a folder`);\n\t\t\treturn found;\n\t\t};\n\n\t\tconst root = this.rootOf(absolute);\n\t\tif (!root) throw new AccessDeniedError(`\"${path}\" is outside the workspace`);\n\t\tif (access === 'write' && root.access === 'read') {\n\t\t\tthrow new AccessDeniedError(`\"${path}\" is in a read-only folder of the workspace`);\n\t\t}\n\n\t\tconst relativePath = (within(root.path, absolute) ?? '').split(sep).join('/');\n\t\tif (this.isProtected(named, absolute)) {\n\t\t\tthrow new AccessDeniedError(`\"${path}\" is protected by the harness`);\n\t\t}\n\t\tif (this.isOpened(absolute, access)) {\n\t\t\treturn allowed({ absolute, root: root.path, relative: relativePath });\n\t\t}\n\t\tif (this.isSecret(named, absolute)) {\n\t\t\tthrow secretRefusal(path);\n\t\t}\n\t\tfor (const location of [...(await this.linksAlong(named)), absolute]) {\n\t\t\tif (await this.isIgnored(location, signal)) {\n\t\t\t\tthrow new AccessDeniedError(`\"${path}\" is excluded by .gitignore`);\n\t\t\t}\n\t\t}\n\n\t\treturn allowed({ absolute, root: root.path, relative: relativePath });\n\t}\n\n\t/**\n\t * Whether the policy closes a file to reading by its path alone: protected, or a secret the\n\t * host did not open. Synchronous, so a search can leave such a file out before it counts a\n\t * match; `check` still decides each path a tool is given. `absolute` is a real path.\n\t */\n\thidesFromReading(absolute: string): boolean {\n\t\tif (this.isProtected(absolute, absolute)) return true;\n\t\treturn !this.isOpened(absolute, 'read') && this.isSecret(absolute, absolute);\n\t}\n\n\t/**\n\t * Whether a command's checkpoint leaves a file out, by its path alone: it is outside every\n\t * `write` root, protected, or a secret the host did not open for writing. Such a file is\n\t * neither copied before a command nor recorded after it. `absolute` is a real path.\n\t */\n\tclosedToWriting(absolute: string): boolean {\n\t\tconst root = this.rootOf(absolute);\n\t\tif (root?.access !== 'write') return true;\n\t\tif (this.isProtected(absolute, absolute)) return true;\n\t\treturn !this.isOpened(absolute, 'write') && this.isSecret(absolute, absolute);\n\t}\n\n\tprivate get base(): string {\n\t\treturn this.roots[0]?.path ?? '';\n\t}\n\n\t/**\n\t * Where `named` really is. For a path that does not exist yet, that is its nearest existing\n\t * folder, resolved, plus the missing names. A link that leads nowhere is refused: writing to\n\t * it would create whatever it points at, wherever that is.\n\t */\n\tprivate async realLocation(\n\t\tnamed: string,\n\t\tshown: string,\n\t): Promise<{ absolute: string; throughFile: boolean }> {\n\t\tconst missing: string[] = [];\n\t\tlet existing = named;\n\t\tfor (;;) {\n\t\t\ttry {\n\t\t\t\treturn { absolute: join(await realpath(existing), ...missing), throughFile: false };\n\t\t\t} catch (error) {\n\t\t\t\tif (code(error) === 'ENOTDIR') return this.throughFile(existing, missing, shown);\n\t\t\t\t// Such as a loop of links, or a folder it may not enter. Its own message would\n\t\t\t\t// name the host path.\n\t\t\t\tif (code(error) !== 'ENOENT') throw unreachable(shown, error);\n\t\t\t}\n\t\t\tconst isLink = await lstat(existing).then(\n\t\t\t\tstats => stats.isSymbolicLink(),\n\t\t\t\t() => false,\n\t\t\t);\n\t\t\tif (isLink) throw new AccessDeniedError(`\"${shown}\" is a link to nothing`);\n\n\t\t\tconst parent = dirname(existing);\n\t\t\t// The filesystem root always exists, so this never runs out.\n\t\t\tmissing.unshift(basename(existing));\n\t\t\texisting = parent;\n\t\t}\n\t}\n\n\t/**\n\t * Where a path that runs through a file would be: the deepest part of it that exists, which is\n\t * that file, resolved, plus the names after it.\n\t */\n\tprivate async throughFile(\n\t\tnamed: string,\n\t\tmissing: string[],\n\t\tshown: string,\n\t): Promise<{ absolute: string; throughFile: boolean }> {\n\t\tconst after = [...missing];\n\t\tlet existing = named;\n\t\twhile (\n\t\t\t!(await lstat(existing).then(\n\t\t\t\t() => true,\n\t\t\t\t() => false,\n\t\t\t))\n\t\t) {\n\t\t\tafter.unshift(basename(existing));\n\t\t\texisting = dirname(existing);\n\t\t}\n\t\ttry {\n\t\t\treturn { absolute: join(await realpath(existing), ...after), throughFile: true };\n\t\t} catch (error) {\n\t\t\tthrow unreachable(shown, error);\n\t\t}\n\t}\n\n\t/**\n\t * Every symlink `named` goes through on the way to its real location, each at its own real\n\t * place. git answers for a link but not for a path beyond one, so each is checked alone.\n\t */\n\tprivate async linksAlong(named: string): Promise<string[]> {\n\t\tconst links: string[] = [];\n\t\tlet pending = named.split(sep).filter(part => part !== '');\n\t\tlet current: string = sep;\n\t\t// `realLocation` already refused a loop; the bound only keeps this walk finite.\n\t\tfor (let hops = 0; pending.length > 0 && hops < 40;) {\n\t\t\tconst [name = '', ...rest] = pending;\n\t\t\tconst next = join(current, name);\n\t\t\tconst stats = await lstat(next).catch(() => undefined);\n\t\t\t// Missing from here on: nothing below can be a link.\n\t\t\tif (!stats) break;\n\t\t\tif (stats.isSymbolicLink()) {\n\t\t\t\thops++;\n\t\t\t\tlinks.push(join(await realpath(current), name));\n\t\t\t\tconst target = resolve(current, await readlink(next));\n\t\t\t\tpending = [...target.split(sep).filter(part => part !== ''), ...rest];\n\t\t\t\tcurrent = sep;\n\t\t\t} else {\n\t\t\t\tcurrent = next;\n\t\t\t\tpending = rest;\n\t\t\t}\n\t\t}\n\t\treturn links;\n\t}\n\n\t/** Whether `.gitignore` excludes a real location, by the root that holds it. */\n\tprivate async isIgnored(location: string, signal: AbortSignal): Promise<boolean> {\n\t\tconst root = this.rootOf(location);\n\t\tconst below = root && within(root.path, location);\n\t\t// A root itself, or a link outside every root, has no rule to answer to.\n\t\tif (!root || !below) return false;\n\t\treturn this.ignoreRules.isIgnored(root.path, below.split(sep).join('/'), signal);\n\t}\n\n\t/** The most specific root holding `absolute`, so a folder nested in another has its own say. */\n\tprivate rootOf(absolute: string): WorkspaceRoot | undefined {\n\t\treturn this.roots\n\t\t\t.filter(root => within(root.path, absolute) !== undefined)\n\t\t\t.sort((a, b) => b.path.length - a.path.length)[0];\n\t}\n\n\tprivate isProtected(named: string, absolute: string): boolean {\n\t\t// `.git` as a folder or as a worktree's pointer file, under any name the model used.\n\t\tconst isGitMetadata = (path: string) => {\n\t\t\tconst root = this.rootOf(path);\n\t\t\tif (!root) return false;\n\t\t\treturn hasGitPart(within(root.path, path) ?? '');\n\t\t};\n\n\t\treturn (\n\t\t\tisGitMetadata(named) ||\n\t\t\tisGitMetadata(absolute) ||\n\t\t\tthis.protectedPaths.some(\n\t\t\t\tfolder => withinIgnoringCase(folder, named) || withinIgnoringCase(folder, absolute),\n\t\t\t)\n\t\t);\n\t}\n\n\t/** Whether the host's `allow` names this exact file for this access; `write` covers reading. */\n\tprivate isOpened(absolute: string, access: Access): boolean {\n\t\treturn this.allowed.some(\n\t\t\tallowed =>\n\t\t\t\t// Exact: both are real paths in the disk's own case, and on a case-sensitive disk\n\t\t\t\t// `.ENV` is a different file from the `.env` the host opened.\n\t\t\t\tallowed.path === absolute && (allowed.access === 'write' || access === 'read'),\n\t\t);\n\t}\n\n\tprivate isSecret(named: string, absolute: string): boolean {\n\t\treturn [named, absolute].some(\n\t\t\tpath =>\n\t\t\t\tisSecretPath(path, this.home) ||\n\t\t\t\tthis.underSecretName(path) ||\n\t\t\t\tthis.secretFolders.some(folder => withinIgnoringCase(folder, path)),\n\t\t);\n\t}\n\n\t/**\n\t * Whether `path` lies below a secret name inside its root, such as `.env/x`. A file there is\n\t * as closed as the name: otherwise `.env/x` would be refused for running through `.env` when\n\t * that file exists, and read as missing when it does not, telling the two apart. Folders above\n\t * the root do not count, or a workspace inside `.env.d` would be closed whole.\n\t */\n\tprivate underSecretName(path: string): boolean {\n\t\tconst root = this.rootOf(path);\n\t\tconst below = root && within(root.path, path);\n\t\tif (!below) return false;\n\t\treturn below.split(sep).slice(0, -1).some(isSecretName);\n\t}\n}\n"]}
@@ -13,11 +13,14 @@ export interface ProcessResult {
13
13
  * With `onLine`, each line of the output is handed over as it arrives and nothing is kept, so
14
14
  * the caller decides what to hold; only a single line is then bound by the size limit. An
15
15
  * error thrown by `onLine` stops the program and rejects with that error.
16
+ *
17
+ * `env` replaces the environment the program inherits; without it, it gets this process's.
16
18
  */
17
- export declare function runProcess(command: string, args: string[], { cwd, signal, onLine }: {
19
+ export declare function runProcess(command: string, args: string[], { cwd, signal, onLine, env, }: {
18
20
  cwd: string;
19
21
  signal: AbortSignal;
20
22
  onLine?: (line: string) => void;
23
+ env?: NodeJS.ProcessEnv;
21
24
  }): Promise<ProcessResult>;
22
25
  /** The first line of a program's error output, which is the part a model can act on. */
23
26
  export declare function firstLine(text: string): string;
@@ -12,10 +12,12 @@ const maxOutputBytes = 64 * 1024 * 1024;
12
12
  * With `onLine`, each line of the output is handed over as it arrives and nothing is kept, so
13
13
  * the caller decides what to hold; only a single line is then bound by the size limit. An
14
14
  * error thrown by `onLine` stops the program and rejects with that error.
15
+ *
16
+ * `env` replaces the environment the program inherits; without it, it gets this process's.
15
17
  */
16
- export function runProcess(command, args, { cwd, signal, onLine }) {
18
+ export function runProcess(command, args, { cwd, signal, onLine, env, }) {
17
19
  return new Promise((resolve, reject) => {
18
- const child = spawn(command, args, { cwd, signal, stdio: ['ignore', 'pipe', 'pipe'] });
20
+ const child = spawn(command, args, { cwd, signal, env, stdio: ['ignore', 'pipe', 'pipe'] });
19
21
  const decoder = new StringDecoder('utf8');
20
22
  const stdout = [];
21
23
  const stderr = [];
@@ -1 +1 @@
1
- {"version":3,"file":"runProcess.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/runProcess.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { StringDecoder } from 'node:string_decoder';\n\n// What a program may print, or one line of it may hold, before it is stopped. Beyond this it\n// has printed far more than anyone can read.\nconst maxOutputBytes = 64 * 1024 * 1024;\n\nexport interface ProcessResult {\n\t/** Empty when the output went to `onLine` instead. */\n\tstdout: string;\n\tstderr: string;\n\texitCode: number;\n}\n\n/**\n * Runs a program with its arguments as an array, never through a shell, so nothing a model\n * writes can become a command. Standard input is closed: ripgrep given no path searches its\n * standard input whenever that is a pipe. A cancelled signal kills the process and rejects\n * with the signal's `AbortError`.\n *\n * With `onLine`, each line of the output is handed over as it arrives and nothing is kept, so\n * the caller decides what to hold; only a single line is then bound by the size limit. An\n * error thrown by `onLine` stops the program and rejects with that error.\n */\nexport function runProcess(\n\tcommand: string,\n\targs: string[],\n\t{ cwd, signal, onLine }: { cwd: string; signal: AbortSignal; onLine?: (line: string) => void },\n): Promise<ProcessResult> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(command, args, { cwd, signal, stdio: ['ignore', 'pipe', 'pipe'] });\n\t\tconst decoder = new StringDecoder('utf8');\n\t\tconst stdout: string[] = [];\n\t\tconst stderr: Buffer[] = [];\n\t\tlet pending = '';\n\t\tlet size = 0;\n\t\tlet failure: Error | undefined;\n\n\t\tconst stop = (error: Error) => {\n\t\t\tfailure ??= error;\n\t\t\tchild.kill();\n\t\t};\n\n\t\tconst deliver = (text: string) => {\n\t\t\tif (!onLine) {\n\t\t\t\tstdout.push(text);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst lines = (pending + text).split('\\n');\n\t\t\tpending = lines.pop() ?? '';\n\t\t\tfor (const line of lines) onLine(line);\n\t\t};\n\n\t\tchild.stdout.on('data', (chunk: Buffer) => {\n\t\t\tif (failure) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsize = onLine ? pending.length + chunk.length : size + chunk.length;\n\t\t\tif (size > maxOutputBytes) {\n\t\t\t\tstop(new Error(`The program printed more than ${String(maxOutputBytes / 1024 / 1024)} MB`));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tdeliver(decoder.write(chunk));\n\t\t\t} catch (error) {\n\t\t\t\tstop(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t});\n\t\tchild.stderr.on('data', (chunk: Buffer) => stderr.push(chunk));\n\n\t\t// A missing program, or the cancellation, which arrives as an `AbortError`.\n\t\tchild.on('error', reject);\n\t\tchild.on('close', (code, killedBy) => {\n\t\t\t// Stopped from outside, such as by the out-of-memory killer: what it printed is not the\n\t\t\t// whole answer, and an exit code would pass it off as one. Its own stops set `failure`.\n\t\t\tif (code === null && !failure) {\n\t\t\t\tfailure = new Error(\n\t\t\t\t\t`${command} was stopped by ${killedBy ?? 'a signal'} before it finished`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!failure) {\n\t\t\t\ttry {\n\t\t\t\t\tdeliver(decoder.end());\n\t\t\t\t\tif (onLine && pending !== '') onLine(pending);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailure = error instanceof Error ? error : new Error(String(error));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (failure) {\n\t\t\t\treject(failure);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout: stdout.join(''),\n\t\t\t\tstderr: Buffer.concat(stderr).toString('utf8'),\n\t\t\t\t// Not null here: a process stopped by a signal was rejected above.\n\t\t\t\texitCode: code ?? 1,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/** The first line of a program's error output, which is the part a model can act on. */\nexport function firstLine(text: string): string {\n\treturn text.trim().split('\\n')[0] ?? '';\n}\n"]}
1
+ {"version":3,"file":"runProcess.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/runProcess.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { StringDecoder } from 'node:string_decoder';\n\n// What a program may print, or one line of it may hold, before it is stopped. Beyond this it\n// has printed far more than anyone can read.\nconst maxOutputBytes = 64 * 1024 * 1024;\n\nexport interface ProcessResult {\n\t/** Empty when the output went to `onLine` instead. */\n\tstdout: string;\n\tstderr: string;\n\texitCode: number;\n}\n\n/**\n * Runs a program with its arguments as an array, never through a shell, so nothing a model\n * writes can become a command. Standard input is closed: ripgrep given no path searches its\n * standard input whenever that is a pipe. A cancelled signal kills the process and rejects\n * with the signal's `AbortError`.\n *\n * With `onLine`, each line of the output is handed over as it arrives and nothing is kept, so\n * the caller decides what to hold; only a single line is then bound by the size limit. An\n * error thrown by `onLine` stops the program and rejects with that error.\n *\n * `env` replaces the environment the program inherits; without it, it gets this process's.\n */\nexport function runProcess(\n\tcommand: string,\n\targs: string[],\n\t{\n\t\tcwd,\n\t\tsignal,\n\t\tonLine,\n\t\tenv,\n\t}: {\n\t\tcwd: string;\n\t\tsignal: AbortSignal;\n\t\tonLine?: (line: string) => void;\n\t\tenv?: NodeJS.ProcessEnv;\n\t},\n): Promise<ProcessResult> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(command, args, { cwd, signal, env, stdio: ['ignore', 'pipe', 'pipe'] });\n\t\tconst decoder = new StringDecoder('utf8');\n\t\tconst stdout: string[] = [];\n\t\tconst stderr: Buffer[] = [];\n\t\tlet pending = '';\n\t\tlet size = 0;\n\t\tlet failure: Error | undefined;\n\n\t\tconst stop = (error: Error) => {\n\t\t\tfailure ??= error;\n\t\t\tchild.kill();\n\t\t};\n\n\t\tconst deliver = (text: string) => {\n\t\t\tif (!onLine) {\n\t\t\t\tstdout.push(text);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst lines = (pending + text).split('\\n');\n\t\t\tpending = lines.pop() ?? '';\n\t\t\tfor (const line of lines) onLine(line);\n\t\t};\n\n\t\tchild.stdout.on('data', (chunk: Buffer) => {\n\t\t\tif (failure) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsize = onLine ? pending.length + chunk.length : size + chunk.length;\n\t\t\tif (size > maxOutputBytes) {\n\t\t\t\tstop(new Error(`The program printed more than ${String(maxOutputBytes / 1024 / 1024)} MB`));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tdeliver(decoder.write(chunk));\n\t\t\t} catch (error) {\n\t\t\t\tstop(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t});\n\t\tchild.stderr.on('data', (chunk: Buffer) => stderr.push(chunk));\n\n\t\t// A missing program, or the cancellation, which arrives as an `AbortError`.\n\t\tchild.on('error', reject);\n\t\tchild.on('close', (code, killedBy) => {\n\t\t\t// Stopped from outside, such as by the out-of-memory killer: what it printed is not the\n\t\t\t// whole answer, and an exit code would pass it off as one. Its own stops set `failure`.\n\t\t\tif (code === null && !failure) {\n\t\t\t\tfailure = new Error(\n\t\t\t\t\t`${command} was stopped by ${killedBy ?? 'a signal'} before it finished`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!failure) {\n\t\t\t\ttry {\n\t\t\t\t\tdeliver(decoder.end());\n\t\t\t\t\tif (onLine && pending !== '') onLine(pending);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailure = error instanceof Error ? error : new Error(String(error));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (failure) {\n\t\t\t\treject(failure);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout: stdout.join(''),\n\t\t\t\tstderr: Buffer.concat(stderr).toString('utf8'),\n\t\t\t\t// Not null here: a process stopped by a signal was rejected above.\n\t\t\t\texitCode: code ?? 1,\n\t\t\t});\n\t\t});\n\t});\n}\n\n/** The first line of a program's error output, which is the part a model can act on. */\nexport function firstLine(text: string): string {\n\treturn text.trim().split('\\n')[0] ?? '';\n}\n"]}