read.test.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { describe, expect, test } from "bun:test"
  2. import path from "path"
  3. import { ReadTool } from "../../src/tool/read"
  4. import { Instance } from "../../src/project/instance"
  5. import { tmpdir } from "../fixture/fixture"
  6. import { PermissionNext } from "../../src/permission/next"
  7. import { Agent } from "../../src/agent/agent"
  8. const ctx = {
  9. sessionID: "test",
  10. messageID: "",
  11. callID: "",
  12. agent: "build",
  13. abort: AbortSignal.any([]),
  14. metadata: () => {},
  15. ask: async () => {},
  16. }
  17. describe("tool.read external_directory permission", () => {
  18. test("allows reading absolute path inside project directory", async () => {
  19. await using tmp = await tmpdir({
  20. init: async (dir) => {
  21. await Bun.write(path.join(dir, "test.txt"), "hello world")
  22. },
  23. })
  24. await Instance.provide({
  25. directory: tmp.path,
  26. fn: async () => {
  27. const read = await ReadTool.init()
  28. const result = await read.execute({ filePath: path.join(tmp.path, "test.txt") }, ctx)
  29. expect(result.output).toContain("hello world")
  30. },
  31. })
  32. })
  33. test("allows reading file in subdirectory inside project directory", async () => {
  34. await using tmp = await tmpdir({
  35. init: async (dir) => {
  36. await Bun.write(path.join(dir, "subdir", "test.txt"), "nested content")
  37. },
  38. })
  39. await Instance.provide({
  40. directory: tmp.path,
  41. fn: async () => {
  42. const read = await ReadTool.init()
  43. const result = await read.execute({ filePath: path.join(tmp.path, "subdir", "test.txt") }, ctx)
  44. expect(result.output).toContain("nested content")
  45. },
  46. })
  47. })
  48. test("asks for external_directory permission when reading absolute path outside project", async () => {
  49. await using outerTmp = await tmpdir({
  50. init: async (dir) => {
  51. await Bun.write(path.join(dir, "secret.txt"), "secret data")
  52. },
  53. })
  54. await using tmp = await tmpdir({ git: true })
  55. await Instance.provide({
  56. directory: tmp.path,
  57. fn: async () => {
  58. const read = await ReadTool.init()
  59. const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
  60. const testCtx = {
  61. ...ctx,
  62. ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
  63. requests.push(req)
  64. },
  65. }
  66. await read.execute({ filePath: path.join(outerTmp.path, "secret.txt") }, testCtx)
  67. const extDirReq = requests.find((r) => r.permission === "external_directory")
  68. expect(extDirReq).toBeDefined()
  69. expect(extDirReq!.patterns.some((p) => p.includes(outerTmp.path))).toBe(true)
  70. },
  71. })
  72. })
  73. test("asks for external_directory permission when reading relative path outside project", async () => {
  74. await using tmp = await tmpdir({ git: true })
  75. await Instance.provide({
  76. directory: tmp.path,
  77. fn: async () => {
  78. const read = await ReadTool.init()
  79. const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
  80. const testCtx = {
  81. ...ctx,
  82. ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
  83. requests.push(req)
  84. },
  85. }
  86. // This will fail because file doesn't exist, but we can check if permission was asked
  87. await read.execute({ filePath: "../outside.txt" }, testCtx).catch(() => {})
  88. const extDirReq = requests.find((r) => r.permission === "external_directory")
  89. expect(extDirReq).toBeDefined()
  90. },
  91. })
  92. })
  93. test("does not ask for external_directory permission when reading inside project", async () => {
  94. await using tmp = await tmpdir({
  95. git: true,
  96. init: async (dir) => {
  97. await Bun.write(path.join(dir, "internal.txt"), "internal content")
  98. },
  99. })
  100. await Instance.provide({
  101. directory: tmp.path,
  102. fn: async () => {
  103. const read = await ReadTool.init()
  104. const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
  105. const testCtx = {
  106. ...ctx,
  107. ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
  108. requests.push(req)
  109. },
  110. }
  111. await read.execute({ filePath: path.join(tmp.path, "internal.txt") }, testCtx)
  112. const extDirReq = requests.find((r) => r.permission === "external_directory")
  113. expect(extDirReq).toBeUndefined()
  114. },
  115. })
  116. })
  117. })
  118. describe("tool.read env file blocking", () => {
  119. const cases: [string, boolean][] = [
  120. [".env", true],
  121. [".env.local", true],
  122. [".env.production", true],
  123. [".env.development.local", true],
  124. [".env.example", false],
  125. [".envrc", false],
  126. ["environment.ts", false],
  127. ]
  128. describe.each(["build", "plan"])("agent=%s", (agentName) => {
  129. test.each(cases)("%s blocked=%s", async (filename, blocked) => {
  130. await using tmp = await tmpdir({
  131. init: (dir) => Bun.write(path.join(dir, filename), "content"),
  132. })
  133. await Instance.provide({
  134. directory: tmp.path,
  135. fn: async () => {
  136. const agent = await Agent.get(agentName)
  137. const ctxWithPermissions = {
  138. ...ctx,
  139. ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
  140. for (const pattern of req.patterns) {
  141. const rule = PermissionNext.evaluate(req.permission, pattern, agent.permission)
  142. if (rule.action === "deny") {
  143. throw new PermissionNext.DeniedError(agent.permission)
  144. }
  145. }
  146. },
  147. }
  148. const read = await ReadTool.init()
  149. const promise = read.execute({ filePath: path.join(tmp.path, filename) }, ctxWithPermissions)
  150. if (blocked) {
  151. await expect(promise).rejects.toThrow(PermissionNext.DeniedError)
  152. } else {
  153. expect((await promise).output).toContain("content")
  154. }
  155. },
  156. })
  157. })
  158. })
  159. })