read.test.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  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. const ctx = {
  7. sessionID: "test",
  8. messageID: "",
  9. callID: "",
  10. agent: "build",
  11. abort: AbortSignal.any([]),
  12. metadata: () => {},
  13. }
  14. describe("tool.read external_directory permission", () => {
  15. test("allows reading absolute path inside project directory", async () => {
  16. await using tmp = await tmpdir({
  17. init: async (dir) => {
  18. await Bun.write(path.join(dir, "test.txt"), "hello world")
  19. await Bun.write(
  20. path.join(dir, "opencode.json"),
  21. JSON.stringify({
  22. permission: {
  23. external_directory: "deny",
  24. },
  25. }),
  26. )
  27. },
  28. })
  29. await Instance.provide({
  30. directory: tmp.path,
  31. fn: async () => {
  32. const read = await ReadTool.init()
  33. const result = await read.execute({ filePath: path.join(tmp.path, "test.txt") }, ctx)
  34. expect(result.output).toContain("hello world")
  35. },
  36. })
  37. })
  38. test("allows reading file in subdirectory inside project directory", async () => {
  39. await using tmp = await tmpdir({
  40. init: async (dir) => {
  41. await Bun.write(path.join(dir, "subdir", "test.txt"), "nested content")
  42. await Bun.write(
  43. path.join(dir, "opencode.json"),
  44. JSON.stringify({
  45. permission: {
  46. external_directory: "deny",
  47. },
  48. }),
  49. )
  50. },
  51. })
  52. await Instance.provide({
  53. directory: tmp.path,
  54. fn: async () => {
  55. const read = await ReadTool.init()
  56. const result = await read.execute({ filePath: path.join(tmp.path, "subdir", "test.txt") }, ctx)
  57. expect(result.output).toContain("nested content")
  58. },
  59. })
  60. })
  61. test("denies reading absolute path outside project directory", async () => {
  62. await using outerTmp = await tmpdir({
  63. init: async (dir) => {
  64. await Bun.write(path.join(dir, "secret.txt"), "secret data")
  65. },
  66. })
  67. await using tmp = await tmpdir({
  68. init: async (dir) => {
  69. await Bun.write(
  70. path.join(dir, "opencode.json"),
  71. JSON.stringify({
  72. permission: {
  73. external_directory: "deny",
  74. },
  75. }),
  76. )
  77. },
  78. })
  79. await Instance.provide({
  80. directory: tmp.path,
  81. fn: async () => {
  82. const read = await ReadTool.init()
  83. await expect(read.execute({ filePath: path.join(outerTmp.path, "secret.txt") }, ctx)).rejects.toThrow(
  84. "not in the current working directory",
  85. )
  86. },
  87. })
  88. })
  89. test("denies reading relative path that traverses outside project directory", async () => {
  90. await using tmp = await tmpdir({
  91. init: async (dir) => {
  92. await Bun.write(
  93. path.join(dir, "opencode.json"),
  94. JSON.stringify({
  95. permission: {
  96. external_directory: "deny",
  97. },
  98. }),
  99. )
  100. },
  101. })
  102. await Instance.provide({
  103. directory: tmp.path,
  104. fn: async () => {
  105. const read = await ReadTool.init()
  106. await expect(read.execute({ filePath: "../../../etc/passwd" }, ctx)).rejects.toThrow(
  107. "not in the current working directory",
  108. )
  109. },
  110. })
  111. })
  112. test("allows reading outside project directory when external_directory is allow", async () => {
  113. await using outerTmp = await tmpdir({
  114. init: async (dir) => {
  115. await Bun.write(path.join(dir, "external.txt"), "external content")
  116. },
  117. })
  118. await using tmp = await tmpdir({
  119. init: async (dir) => {
  120. await Bun.write(
  121. path.join(dir, "opencode.json"),
  122. JSON.stringify({
  123. permission: {
  124. external_directory: "allow",
  125. },
  126. }),
  127. )
  128. },
  129. })
  130. await Instance.provide({
  131. directory: tmp.path,
  132. fn: async () => {
  133. const read = await ReadTool.init()
  134. const result = await read.execute({ filePath: path.join(outerTmp.path, "external.txt") }, ctx)
  135. expect(result.output).toContain("external content")
  136. },
  137. })
  138. })
  139. })
  140. describe("tool.read env file blocking", () => {
  141. test.each([
  142. [".env", true],
  143. [".env.local", true],
  144. [".env.production", true],
  145. [".env.sample", false],
  146. [".env.example", false],
  147. [".envrc", false],
  148. ["environment.ts", false],
  149. ])("%s blocked=%s", async (filename, blocked) => {
  150. await using tmp = await tmpdir({
  151. init: (dir) => Bun.write(path.join(dir, filename), "content"),
  152. })
  153. await Instance.provide({
  154. directory: tmp.path,
  155. fn: async () => {
  156. const read = await ReadTool.init()
  157. const promise = read.execute({ filePath: path.join(tmp.path, filename) }, ctx)
  158. if (blocked) {
  159. await expect(promise).rejects.toThrow("blocked")
  160. } else {
  161. expect((await promise).output).toContain("content")
  162. }
  163. },
  164. })
  165. })
  166. })