@stonerzju/opencode 1.2.17 → 1.2.18

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 (261) hide show
  1. package/bin/opencode +29 -157
  2. package/package.json +29 -29
  3. package/src/acp/agent.ts +4 -4
  4. package/src/acp/session.ts +1 -1
  5. package/src/agent/agent.ts +3 -3
  6. package/src/bun/index.ts +2 -2
  7. package/src/cli/cmd/acp.ts +3 -3
  8. package/src/cli/cmd/debug/file.ts +1 -1
  9. package/src/cli/cmd/github.ts +2 -2
  10. package/src/cli/cmd/pr.ts +1 -1
  11. package/src/cli/cmd/tui/app.tsx +24 -24
  12. package/src/cli/cmd/tui/attach.ts +3 -3
  13. package/src/cli/cmd/tui/component/dialog-agent.tsx +3 -3
  14. package/src/cli/cmd/tui/component/dialog-command.tsx +3 -3
  15. package/src/cli/cmd/tui/component/dialog-mcp.tsx +5 -5
  16. package/src/cli/cmd/tui/component/dialog-model.tsx +4 -4
  17. package/src/cli/cmd/tui/component/dialog-provider.tsx +4 -4
  18. package/src/cli/cmd/tui/component/dialog-session-list.tsx +5 -5
  19. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +3 -3
  20. package/src/cli/cmd/tui/component/dialog-skill.tsx +3 -3
  21. package/src/cli/cmd/tui/component/dialog-stash.tsx +3 -3
  22. package/src/cli/cmd/tui/component/dialog-status.tsx +2 -2
  23. package/src/cli/cmd/tui/component/dialog-tag.tsx +3 -3
  24. package/src/cli/cmd/tui/component/logo.tsx +2 -2
  25. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +6 -6
  26. package/src/cli/cmd/tui/component/prompt/frecency.tsx +2 -2
  27. package/src/cli/cmd/tui/component/prompt/history.tsx +2 -2
  28. package/src/cli/cmd/tui/component/prompt/index.tsx +14 -14
  29. package/src/cli/cmd/tui/component/prompt/stash.tsx +2 -2
  30. package/src/cli/cmd/tui/component/textarea-keybindings.ts +1 -1
  31. package/src/cli/cmd/tui/component/tips.tsx +1 -1
  32. package/src/cli/cmd/tui/context/directory.ts +1 -1
  33. package/src/cli/cmd/tui/context/exit.tsx +1 -1
  34. package/src/cli/cmd/tui/context/keybind.tsx +2 -2
  35. package/src/cli/cmd/tui/context/kv.tsx +2 -2
  36. package/src/cli/cmd/tui/context/local.tsx +6 -6
  37. package/src/cli/cmd/tui/context/sync.tsx +4 -4
  38. package/src/cli/cmd/tui/context/theme/opencode.json +245 -0
  39. package/src/cli/cmd/tui/context/theme.tsx +2 -2
  40. package/src/cli/cmd/tui/context/tui-config.tsx +1 -1
  41. package/src/cli/cmd/tui/event.ts +2 -2
  42. package/src/cli/cmd/tui/routes/home.tsx +6 -6
  43. package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +6 -6
  44. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +6 -6
  45. package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +2 -2
  46. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +3 -3
  47. package/src/cli/cmd/tui/routes/session/header.tsx +5 -5
  48. package/src/cli/cmd/tui/routes/session/index.tsx +32 -32
  49. package/src/cli/cmd/tui/routes/session/permission.tsx +4 -4
  50. package/src/cli/cmd/tui/routes/session/sidebar.tsx +4 -4
  51. package/src/cli/cmd/tui/thread.ts +9 -9
  52. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +1 -1
  53. package/src/cli/cmd/tui/ui/dialog-help.tsx +2 -2
  54. package/src/cli/cmd/tui/ui/dialog-select.tsx +5 -5
  55. package/src/cli/cmd/tui/ui/dialog.tsx +3 -3
  56. package/src/cli/cmd/tui/ui/toast.tsx +1 -1
  57. package/src/cli/cmd/tui/util/editor.ts +3 -3
  58. package/src/cli/cmd/tui/util/transcript.ts +1 -1
  59. package/src/cli/cmd/tui/worker.ts +10 -10
  60. package/src/cli/error.ts +1 -1
  61. package/src/cli/ui.ts +1 -1
  62. package/src/cli/upgrade.ts +4 -4
  63. package/src/command/index.ts +1 -1
  64. package/src/config/config.ts +10 -10
  65. package/src/config/markdown.ts +1 -1
  66. package/src/config/migrate-tui-config.ts +5 -5
  67. package/src/config/paths.ts +4 -4
  68. package/src/config/tui.ts +4 -4
  69. package/src/control/control.sql.ts +1 -1
  70. package/src/control/index.ts +1 -1
  71. package/src/control-plane/adaptors/worktree.ts +1 -1
  72. package/src/control-plane/session-proxy-middleware.ts +1 -1
  73. package/src/control-plane/workspace.sql.ts +1 -1
  74. package/src/control-plane/workspace.ts +7 -7
  75. package/src/file/index.ts +1 -1
  76. package/src/file/ripgrep.ts +2 -2
  77. package/src/file/watcher.ts +5 -5
  78. package/src/format/formatter.ts +1 -1
  79. package/src/ide/index.ts +3 -3
  80. package/src/index.ts +1 -1
  81. package/src/installation/index.ts +3 -3
  82. package/src/lsp/client.ts +3 -3
  83. package/src/lsp/index.ts +3 -3
  84. package/src/mcp/index.ts +4 -4
  85. package/src/permission/index.ts +2 -2
  86. package/src/permission/next.ts +10 -10
  87. package/src/plugin/codex.ts +1 -1
  88. package/src/plugin/copilot.ts +2 -2
  89. package/src/plugin/index.ts +1 -1
  90. package/src/project/bootstrap.ts +2 -2
  91. package/src/project/instance.ts +4 -4
  92. package/src/project/project.sql.ts +1 -1
  93. package/src/project/project.ts +5 -5
  94. package/src/project/state.ts +1 -1
  95. package/src/project/vcs.ts +4 -4
  96. package/src/provider/auth.ts +4 -4
  97. package/src/provider/error.ts +1 -1
  98. package/src/provider/models-snapshot.ts +2 -0
  99. package/src/provider/models.ts +1 -1
  100. package/src/provider/provider.ts +2 -2
  101. package/src/provider/transform.ts +2 -2
  102. package/src/pty/index.ts +5 -5
  103. package/src/question/index.ts +5 -5
  104. package/src/server/event.ts +1 -1
  105. package/src/server/mdns.ts +1 -1
  106. package/src/server/routes/global.ts +3 -3
  107. package/src/server/routes/permission.ts +1 -1
  108. package/src/server/routes/pty.ts +1 -1
  109. package/src/server/routes/session.ts +4 -4
  110. package/src/server/routes/tui.ts +1 -1
  111. package/src/server/server.ts +3 -3
  112. package/src/session/compaction.ts +7 -7
  113. package/src/session/index.ts +10 -10
  114. package/src/session/instruction.ts +1 -1
  115. package/src/session/llm.ts +11 -11
  116. package/src/session/message-v2.ts +10 -10
  117. package/src/session/message.ts +1 -1
  118. package/src/session/processor.ts +10 -10
  119. package/src/session/prompt.ts +8 -8
  120. package/src/session/retry.ts +2 -2
  121. package/src/session/revert.ts +1 -1
  122. package/src/session/session.sql.ts +3 -3
  123. package/src/session/status.ts +3 -3
  124. package/src/session/summary.ts +5 -5
  125. package/src/session/system.ts +1 -1
  126. package/src/session/todo.ts +2 -2
  127. package/src/share/share-next.ts +7 -7
  128. package/src/share/share.sql.ts +1 -1
  129. package/src/shell/shell.ts +3 -3
  130. package/src/skill/skill.ts +6 -6
  131. package/src/storage/db.ts +1 -1
  132. package/src/storage/storage.ts +1 -1
  133. package/src/tool/bash.ts +6 -6
  134. package/src/tool/edit.ts +1 -1
  135. package/src/tool/registry.ts +2 -2
  136. package/src/tool/skill.ts +1 -1
  137. package/src/tool/task.ts +3 -3
  138. package/src/util/array.ts +10 -0
  139. package/src/util/binary.ts +41 -0
  140. package/src/util/encode.ts +51 -0
  141. package/src/util/error.ts +54 -0
  142. package/src/util/identifier.ts +48 -0
  143. package/src/util/lazy.ts +4 -16
  144. package/src/util/path.ts +37 -0
  145. package/src/util/retry.ts +41 -0
  146. package/src/util/slug.ts +74 -0
  147. package/src/worktree/index.ts +3 -3
  148. package/AGENTS.md +0 -10
  149. package/BUN_SHELL_MIGRATION_PLAN.md +0 -136
  150. package/Dockerfile +0 -18
  151. package/README.md +0 -15
  152. package/bunfig.toml +0 -7
  153. package/drizzle.config.ts +0 -10
  154. package/script/build.ts +0 -224
  155. package/script/check-migrations.ts +0 -16
  156. package/script/postinstall.mjs +0 -131
  157. package/script/publish.ts +0 -181
  158. package/script/schema.ts +0 -63
  159. package/script/seed-e2e.ts +0 -50
  160. package/sst-env.d.ts +0 -10
  161. package/test/AGENTS.md +0 -81
  162. package/test/acp/agent-interface.test.ts +0 -51
  163. package/test/acp/event-subscription.test.ts +0 -683
  164. package/test/agent/agent.test.ts +0 -689
  165. package/test/bun.test.ts +0 -53
  166. package/test/cli/github-action.test.ts +0 -197
  167. package/test/cli/github-remote.test.ts +0 -80
  168. package/test/cli/import.test.ts +0 -38
  169. package/test/cli/plugin-auth-picker.test.ts +0 -120
  170. package/test/cli/tui/transcript.test.ts +0 -322
  171. package/test/config/agent-color.test.ts +0 -71
  172. package/test/config/config.test.ts +0 -1886
  173. package/test/config/fixtures/empty-frontmatter.md +0 -4
  174. package/test/config/fixtures/frontmatter.md +0 -28
  175. package/test/config/fixtures/markdown-header.md +0 -11
  176. package/test/config/fixtures/no-frontmatter.md +0 -1
  177. package/test/config/fixtures/weird-model-id.md +0 -13
  178. package/test/config/markdown.test.ts +0 -228
  179. package/test/config/tui.test.ts +0 -510
  180. package/test/control-plane/session-proxy-middleware.test.ts +0 -147
  181. package/test/control-plane/sse.test.ts +0 -56
  182. package/test/control-plane/workspace-server-sse.test.ts +0 -65
  183. package/test/control-plane/workspace-sync.test.ts +0 -97
  184. package/test/file/ignore.test.ts +0 -10
  185. package/test/file/index.test.ts +0 -394
  186. package/test/file/path-traversal.test.ts +0 -198
  187. package/test/file/ripgrep.test.ts +0 -39
  188. package/test/file/time.test.ts +0 -361
  189. package/test/fixture/db.ts +0 -11
  190. package/test/fixture/fixture.ts +0 -45
  191. package/test/fixture/lsp/fake-lsp-server.js +0 -77
  192. package/test/fixture/skills/agents-sdk/SKILL.md +0 -152
  193. package/test/fixture/skills/agents-sdk/references/callable.md +0 -92
  194. package/test/fixture/skills/cloudflare/SKILL.md +0 -211
  195. package/test/fixture/skills/index.json +0 -6
  196. package/test/ide/ide.test.ts +0 -82
  197. package/test/keybind.test.ts +0 -421
  198. package/test/lsp/client.test.ts +0 -95
  199. package/test/mcp/headers.test.ts +0 -153
  200. package/test/mcp/oauth-browser.test.ts +0 -249
  201. package/test/memory/abort-leak.test.ts +0 -136
  202. package/test/patch/patch.test.ts +0 -348
  203. package/test/permission/arity.test.ts +0 -33
  204. package/test/permission/next.test.ts +0 -689
  205. package/test/permission-task.test.ts +0 -319
  206. package/test/plugin/auth-override.test.ts +0 -44
  207. package/test/plugin/codex.test.ts +0 -123
  208. package/test/preload.ts +0 -80
  209. package/test/project/project.test.ts +0 -348
  210. package/test/project/worktree-remove.test.ts +0 -65
  211. package/test/provider/amazon-bedrock.test.ts +0 -446
  212. package/test/provider/copilot/convert-to-copilot-messages.test.ts +0 -523
  213. package/test/provider/copilot/copilot-chat-model.test.ts +0 -592
  214. package/test/provider/gitlab-duo.test.ts +0 -262
  215. package/test/provider/provider.test.ts +0 -2220
  216. package/test/provider/transform.test.ts +0 -2353
  217. package/test/pty/pty-output-isolation.test.ts +0 -140
  218. package/test/question/question.test.ts +0 -300
  219. package/test/scheduler.test.ts +0 -73
  220. package/test/server/global-session-list.test.ts +0 -89
  221. package/test/server/session-list.test.ts +0 -90
  222. package/test/server/session-select.test.ts +0 -78
  223. package/test/session/compaction.test.ts +0 -423
  224. package/test/session/instruction.test.ts +0 -170
  225. package/test/session/llm.test.ts +0 -667
  226. package/test/session/message-v2.test.ts +0 -924
  227. package/test/session/prompt.test.ts +0 -211
  228. package/test/session/retry.test.ts +0 -188
  229. package/test/session/revert-compact.test.ts +0 -285
  230. package/test/session/session.test.ts +0 -71
  231. package/test/session/structured-output-integration.test.ts +0 -233
  232. package/test/session/structured-output.test.ts +0 -385
  233. package/test/skill/discovery.test.ts +0 -110
  234. package/test/skill/skill.test.ts +0 -388
  235. package/test/snapshot/snapshot.test.ts +0 -1180
  236. package/test/storage/json-migration.test.ts +0 -846
  237. package/test/tool/__snapshots__/tool.test.ts.snap +0 -9
  238. package/test/tool/apply_patch.test.ts +0 -566
  239. package/test/tool/bash.test.ts +0 -402
  240. package/test/tool/edit.test.ts +0 -496
  241. package/test/tool/external-directory.test.ts +0 -127
  242. package/test/tool/fixtures/large-image.png +0 -0
  243. package/test/tool/fixtures/models-api.json +0 -38413
  244. package/test/tool/grep.test.ts +0 -110
  245. package/test/tool/question.test.ts +0 -107
  246. package/test/tool/read.test.ts +0 -504
  247. package/test/tool/registry.test.ts +0 -122
  248. package/test/tool/skill.test.ts +0 -112
  249. package/test/tool/truncation.test.ts +0 -160
  250. package/test/tool/webfetch.test.ts +0 -100
  251. package/test/tool/write.test.ts +0 -348
  252. package/test/util/filesystem.test.ts +0 -443
  253. package/test/util/format.test.ts +0 -59
  254. package/test/util/glob.test.ts +0 -164
  255. package/test/util/iife.test.ts +0 -36
  256. package/test/util/lazy.test.ts +0 -50
  257. package/test/util/lock.test.ts +0 -72
  258. package/test/util/process.test.ts +0 -59
  259. package/test/util/timeout.test.ts +0 -21
  260. package/test/util/wildcard.test.ts +0 -90
  261. package/tsconfig.json +0 -16
@@ -1,689 +0,0 @@
1
- import { test, expect } from "bun:test"
2
- import os from "os"
3
- import { PermissionNext } from "../../src/permission/next"
4
- import { Instance } from "../../src/project/instance"
5
- import { tmpdir } from "../fixture/fixture"
6
-
7
- // fromConfig tests
8
-
9
- test("fromConfig - string value becomes wildcard rule", () => {
10
- const result = PermissionNext.fromConfig({ bash: "allow" })
11
- expect(result).toEqual([{ permission: "bash", pattern: "*", action: "allow" }])
12
- })
13
-
14
- test("fromConfig - object value converts to rules array", () => {
15
- const result = PermissionNext.fromConfig({ bash: { "*": "allow", rm: "deny" } })
16
- expect(result).toEqual([
17
- { permission: "bash", pattern: "*", action: "allow" },
18
- { permission: "bash", pattern: "rm", action: "deny" },
19
- ])
20
- })
21
-
22
- test("fromConfig - mixed string and object values", () => {
23
- const result = PermissionNext.fromConfig({
24
- bash: { "*": "allow", rm: "deny" },
25
- edit: "allow",
26
- webfetch: "ask",
27
- })
28
- expect(result).toEqual([
29
- { permission: "bash", pattern: "*", action: "allow" },
30
- { permission: "bash", pattern: "rm", action: "deny" },
31
- { permission: "edit", pattern: "*", action: "allow" },
32
- { permission: "webfetch", pattern: "*", action: "ask" },
33
- ])
34
- })
35
-
36
- test("fromConfig - empty object", () => {
37
- const result = PermissionNext.fromConfig({})
38
- expect(result).toEqual([])
39
- })
40
-
41
- test("fromConfig - expands tilde to home directory", () => {
42
- const result = PermissionNext.fromConfig({ external_directory: { "~/projects/*": "allow" } })
43
- expect(result).toEqual([{ permission: "external_directory", pattern: `${os.homedir()}/projects/*`, action: "allow" }])
44
- })
45
-
46
- test("fromConfig - expands $HOME to home directory", () => {
47
- const result = PermissionNext.fromConfig({ external_directory: { "$HOME/projects/*": "allow" } })
48
- expect(result).toEqual([{ permission: "external_directory", pattern: `${os.homedir()}/projects/*`, action: "allow" }])
49
- })
50
-
51
- test("fromConfig - expands $HOME without trailing slash", () => {
52
- const result = PermissionNext.fromConfig({ external_directory: { $HOME: "allow" } })
53
- expect(result).toEqual([{ permission: "external_directory", pattern: os.homedir(), action: "allow" }])
54
- })
55
-
56
- test("fromConfig - does not expand tilde in middle of path", () => {
57
- const result = PermissionNext.fromConfig({ external_directory: { "/some/~/path": "allow" } })
58
- expect(result).toEqual([{ permission: "external_directory", pattern: "/some/~/path", action: "allow" }])
59
- })
60
-
61
- test("fromConfig - expands exact tilde to home directory", () => {
62
- const result = PermissionNext.fromConfig({ external_directory: { "~": "allow" } })
63
- expect(result).toEqual([{ permission: "external_directory", pattern: os.homedir(), action: "allow" }])
64
- })
65
-
66
- test("evaluate - matches expanded tilde pattern", () => {
67
- const ruleset = PermissionNext.fromConfig({ external_directory: { "~/projects/*": "allow" } })
68
- const result = PermissionNext.evaluate("external_directory", `${os.homedir()}/projects/file.txt`, ruleset)
69
- expect(result.action).toBe("allow")
70
- })
71
-
72
- test("evaluate - matches expanded $HOME pattern", () => {
73
- const ruleset = PermissionNext.fromConfig({ external_directory: { "$HOME/projects/*": "allow" } })
74
- const result = PermissionNext.evaluate("external_directory", `${os.homedir()}/projects/file.txt`, ruleset)
75
- expect(result.action).toBe("allow")
76
- })
77
-
78
- // merge tests
79
-
80
- test("merge - simple concatenation", () => {
81
- const result = PermissionNext.merge(
82
- [{ permission: "bash", pattern: "*", action: "allow" }],
83
- [{ permission: "bash", pattern: "*", action: "deny" }],
84
- )
85
- expect(result).toEqual([
86
- { permission: "bash", pattern: "*", action: "allow" },
87
- { permission: "bash", pattern: "*", action: "deny" },
88
- ])
89
- })
90
-
91
- test("merge - adds new permission", () => {
92
- const result = PermissionNext.merge(
93
- [{ permission: "bash", pattern: "*", action: "allow" }],
94
- [{ permission: "edit", pattern: "*", action: "deny" }],
95
- )
96
- expect(result).toEqual([
97
- { permission: "bash", pattern: "*", action: "allow" },
98
- { permission: "edit", pattern: "*", action: "deny" },
99
- ])
100
- })
101
-
102
- test("merge - concatenates rules for same permission", () => {
103
- const result = PermissionNext.merge(
104
- [{ permission: "bash", pattern: "foo", action: "ask" }],
105
- [{ permission: "bash", pattern: "*", action: "deny" }],
106
- )
107
- expect(result).toEqual([
108
- { permission: "bash", pattern: "foo", action: "ask" },
109
- { permission: "bash", pattern: "*", action: "deny" },
110
- ])
111
- })
112
-
113
- test("merge - multiple rulesets", () => {
114
- const result = PermissionNext.merge(
115
- [{ permission: "bash", pattern: "*", action: "allow" }],
116
- [{ permission: "bash", pattern: "rm", action: "ask" }],
117
- [{ permission: "edit", pattern: "*", action: "allow" }],
118
- )
119
- expect(result).toEqual([
120
- { permission: "bash", pattern: "*", action: "allow" },
121
- { permission: "bash", pattern: "rm", action: "ask" },
122
- { permission: "edit", pattern: "*", action: "allow" },
123
- ])
124
- })
125
-
126
- test("merge - empty ruleset does nothing", () => {
127
- const result = PermissionNext.merge([{ permission: "bash", pattern: "*", action: "allow" }], [])
128
- expect(result).toEqual([{ permission: "bash", pattern: "*", action: "allow" }])
129
- })
130
-
131
- test("merge - preserves rule order", () => {
132
- const result = PermissionNext.merge(
133
- [
134
- { permission: "edit", pattern: "src/*", action: "allow" },
135
- { permission: "edit", pattern: "src/secret/*", action: "deny" },
136
- ],
137
- [{ permission: "edit", pattern: "src/secret/ok.ts", action: "allow" }],
138
- )
139
- expect(result).toEqual([
140
- { permission: "edit", pattern: "src/*", action: "allow" },
141
- { permission: "edit", pattern: "src/secret/*", action: "deny" },
142
- { permission: "edit", pattern: "src/secret/ok.ts", action: "allow" },
143
- ])
144
- })
145
-
146
- test("merge - config permission overrides default ask", () => {
147
- // Simulates: defaults have "*": "ask", config sets bash: "allow"
148
- const defaults: PermissionNext.Ruleset = [{ permission: "*", pattern: "*", action: "ask" }]
149
- const config: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
150
- const merged = PermissionNext.merge(defaults, config)
151
-
152
- // Config's bash allow should override default ask
153
- expect(PermissionNext.evaluate("bash", "ls", merged).action).toBe("allow")
154
- // Other permissions should still be ask (from defaults)
155
- expect(PermissionNext.evaluate("edit", "foo.ts", merged).action).toBe("ask")
156
- })
157
-
158
- test("merge - config ask overrides default allow", () => {
159
- // Simulates: defaults have bash: "allow", config sets bash: "ask"
160
- const defaults: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
161
- const config: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "ask" }]
162
- const merged = PermissionNext.merge(defaults, config)
163
-
164
- // Config's ask should override default allow
165
- expect(PermissionNext.evaluate("bash", "ls", merged).action).toBe("ask")
166
- })
167
-
168
- // evaluate tests
169
-
170
- test("evaluate - exact pattern match", () => {
171
- const result = PermissionNext.evaluate("bash", "rm", [{ permission: "bash", pattern: "rm", action: "deny" }])
172
- expect(result.action).toBe("deny")
173
- })
174
-
175
- test("evaluate - wildcard pattern match", () => {
176
- const result = PermissionNext.evaluate("bash", "rm", [{ permission: "bash", pattern: "*", action: "allow" }])
177
- expect(result.action).toBe("allow")
178
- })
179
-
180
- test("evaluate - last matching rule wins", () => {
181
- const result = PermissionNext.evaluate("bash", "rm", [
182
- { permission: "bash", pattern: "*", action: "allow" },
183
- { permission: "bash", pattern: "rm", action: "deny" },
184
- ])
185
- expect(result.action).toBe("deny")
186
- })
187
-
188
- test("evaluate - last matching rule wins (wildcard after specific)", () => {
189
- const result = PermissionNext.evaluate("bash", "rm", [
190
- { permission: "bash", pattern: "rm", action: "deny" },
191
- { permission: "bash", pattern: "*", action: "allow" },
192
- ])
193
- expect(result.action).toBe("allow")
194
- })
195
-
196
- test("evaluate - glob pattern match", () => {
197
- const result = PermissionNext.evaluate("edit", "src/foo.ts", [
198
- { permission: "edit", pattern: "src/*", action: "allow" },
199
- ])
200
- expect(result.action).toBe("allow")
201
- })
202
-
203
- test("evaluate - last matching glob wins", () => {
204
- const result = PermissionNext.evaluate("edit", "src/components/Button.tsx", [
205
- { permission: "edit", pattern: "src/*", action: "deny" },
206
- { permission: "edit", pattern: "src/components/*", action: "allow" },
207
- ])
208
- expect(result.action).toBe("allow")
209
- })
210
-
211
- test("evaluate - order matters for specificity", () => {
212
- // If more specific rule comes first, later wildcard overrides it
213
- const result = PermissionNext.evaluate("edit", "src/components/Button.tsx", [
214
- { permission: "edit", pattern: "src/components/*", action: "allow" },
215
- { permission: "edit", pattern: "src/*", action: "deny" },
216
- ])
217
- expect(result.action).toBe("deny")
218
- })
219
-
220
- test("evaluate - unknown permission returns ask", () => {
221
- const result = PermissionNext.evaluate("unknown_tool", "anything", [
222
- { permission: "bash", pattern: "*", action: "allow" },
223
- ])
224
- expect(result.action).toBe("ask")
225
- })
226
-
227
- test("evaluate - empty ruleset returns ask", () => {
228
- const result = PermissionNext.evaluate("bash", "rm", [])
229
- expect(result.action).toBe("ask")
230
- })
231
-
232
- test("evaluate - no matching pattern returns ask", () => {
233
- const result = PermissionNext.evaluate("edit", "etc/passwd", [
234
- { permission: "edit", pattern: "src/*", action: "allow" },
235
- ])
236
- expect(result.action).toBe("ask")
237
- })
238
-
239
- test("evaluate - empty rules array returns ask", () => {
240
- const result = PermissionNext.evaluate("bash", "rm", [])
241
- expect(result.action).toBe("ask")
242
- })
243
-
244
- test("evaluate - multiple matching patterns, last wins", () => {
245
- const result = PermissionNext.evaluate("edit", "src/secret.ts", [
246
- { permission: "edit", pattern: "*", action: "ask" },
247
- { permission: "edit", pattern: "src/*", action: "allow" },
248
- { permission: "edit", pattern: "src/secret.ts", action: "deny" },
249
- ])
250
- expect(result.action).toBe("deny")
251
- })
252
-
253
- test("evaluate - non-matching patterns are skipped", () => {
254
- const result = PermissionNext.evaluate("edit", "src/foo.ts", [
255
- { permission: "edit", pattern: "*", action: "ask" },
256
- { permission: "edit", pattern: "test/*", action: "deny" },
257
- { permission: "edit", pattern: "src/*", action: "allow" },
258
- ])
259
- expect(result.action).toBe("allow")
260
- })
261
-
262
- test("evaluate - exact match at end wins over earlier wildcard", () => {
263
- const result = PermissionNext.evaluate("bash", "/bin/rm", [
264
- { permission: "bash", pattern: "*", action: "allow" },
265
- { permission: "bash", pattern: "/bin/rm", action: "deny" },
266
- ])
267
- expect(result.action).toBe("deny")
268
- })
269
-
270
- test("evaluate - wildcard at end overrides earlier exact match", () => {
271
- const result = PermissionNext.evaluate("bash", "/bin/rm", [
272
- { permission: "bash", pattern: "/bin/rm", action: "deny" },
273
- { permission: "bash", pattern: "*", action: "allow" },
274
- ])
275
- expect(result.action).toBe("allow")
276
- })
277
-
278
- // wildcard permission tests
279
-
280
- test("evaluate - wildcard permission matches any permission", () => {
281
- const result = PermissionNext.evaluate("bash", "rm", [{ permission: "*", pattern: "*", action: "deny" }])
282
- expect(result.action).toBe("deny")
283
- })
284
-
285
- test("evaluate - wildcard permission with specific pattern", () => {
286
- const result = PermissionNext.evaluate("bash", "rm", [{ permission: "*", pattern: "rm", action: "deny" }])
287
- expect(result.action).toBe("deny")
288
- })
289
-
290
- test("evaluate - glob permission pattern", () => {
291
- const result = PermissionNext.evaluate("mcp_server_tool", "anything", [
292
- { permission: "mcp_*", pattern: "*", action: "allow" },
293
- ])
294
- expect(result.action).toBe("allow")
295
- })
296
-
297
- test("evaluate - specific permission and wildcard permission combined", () => {
298
- const result = PermissionNext.evaluate("bash", "rm", [
299
- { permission: "*", pattern: "*", action: "deny" },
300
- { permission: "bash", pattern: "*", action: "allow" },
301
- ])
302
- expect(result.action).toBe("allow")
303
- })
304
-
305
- test("evaluate - wildcard permission does not match when specific exists", () => {
306
- const result = PermissionNext.evaluate("edit", "src/foo.ts", [
307
- { permission: "*", pattern: "*", action: "deny" },
308
- { permission: "edit", pattern: "src/*", action: "allow" },
309
- ])
310
- expect(result.action).toBe("allow")
311
- })
312
-
313
- test("evaluate - multiple matching permission patterns combine rules", () => {
314
- const result = PermissionNext.evaluate("mcp_dangerous", "anything", [
315
- { permission: "*", pattern: "*", action: "ask" },
316
- { permission: "mcp_*", pattern: "*", action: "allow" },
317
- { permission: "mcp_dangerous", pattern: "*", action: "deny" },
318
- ])
319
- expect(result.action).toBe("deny")
320
- })
321
-
322
- test("evaluate - wildcard permission fallback for unknown tool", () => {
323
- const result = PermissionNext.evaluate("unknown_tool", "anything", [
324
- { permission: "*", pattern: "*", action: "ask" },
325
- { permission: "bash", pattern: "*", action: "allow" },
326
- ])
327
- expect(result.action).toBe("ask")
328
- })
329
-
330
- test("evaluate - permission patterns sorted by length regardless of object order", () => {
331
- // specific permission listed before wildcard, but specific should still win
332
- const result = PermissionNext.evaluate("bash", "rm", [
333
- { permission: "bash", pattern: "*", action: "allow" },
334
- { permission: "*", pattern: "*", action: "deny" },
335
- ])
336
- // With flat list, last matching rule wins - so "*" matches bash and wins
337
- expect(result.action).toBe("deny")
338
- })
339
-
340
- test("evaluate - merges multiple rulesets", () => {
341
- const config: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
342
- const approved: PermissionNext.Ruleset = [{ permission: "bash", pattern: "rm", action: "deny" }]
343
- // approved comes after config, so rm should be denied
344
- const result = PermissionNext.evaluate("bash", "rm", config, approved)
345
- expect(result.action).toBe("deny")
346
- })
347
-
348
- // disabled tests
349
-
350
- test("disabled - returns empty set when all tools allowed", () => {
351
- const result = PermissionNext.disabled(["bash", "edit", "read"], [{ permission: "*", pattern: "*", action: "allow" }])
352
- expect(result.size).toBe(0)
353
- })
354
-
355
- test("disabled - disables tool when denied", () => {
356
- const result = PermissionNext.disabled(
357
- ["bash", "edit", "read"],
358
- [
359
- { permission: "*", pattern: "*", action: "allow" },
360
- { permission: "bash", pattern: "*", action: "deny" },
361
- ],
362
- )
363
- expect(result.has("bash")).toBe(true)
364
- expect(result.has("edit")).toBe(false)
365
- expect(result.has("read")).toBe(false)
366
- })
367
-
368
- test("disabled - disables edit/write/patch/multiedit when edit denied", () => {
369
- const result = PermissionNext.disabled(
370
- ["edit", "write", "patch", "multiedit", "bash"],
371
- [
372
- { permission: "*", pattern: "*", action: "allow" },
373
- { permission: "edit", pattern: "*", action: "deny" },
374
- ],
375
- )
376
- expect(result.has("edit")).toBe(true)
377
- expect(result.has("write")).toBe(true)
378
- expect(result.has("patch")).toBe(true)
379
- expect(result.has("multiedit")).toBe(true)
380
- expect(result.has("bash")).toBe(false)
381
- })
382
-
383
- test("disabled - does not disable when partially denied", () => {
384
- const result = PermissionNext.disabled(
385
- ["bash"],
386
- [
387
- { permission: "bash", pattern: "*", action: "allow" },
388
- { permission: "bash", pattern: "rm *", action: "deny" },
389
- ],
390
- )
391
- expect(result.has("bash")).toBe(false)
392
- })
393
-
394
- test("disabled - does not disable when action is ask", () => {
395
- const result = PermissionNext.disabled(["bash", "edit"], [{ permission: "*", pattern: "*", action: "ask" }])
396
- expect(result.size).toBe(0)
397
- })
398
-
399
- test("disabled - does not disable when specific allow after wildcard deny", () => {
400
- // Tool is NOT disabled because a specific allow after wildcard deny means
401
- // there's at least some usage allowed
402
- const result = PermissionNext.disabled(
403
- ["bash"],
404
- [
405
- { permission: "bash", pattern: "*", action: "deny" },
406
- { permission: "bash", pattern: "echo *", action: "allow" },
407
- ],
408
- )
409
- expect(result.has("bash")).toBe(false)
410
- })
411
-
412
- test("disabled - does not disable when wildcard allow after deny", () => {
413
- const result = PermissionNext.disabled(
414
- ["bash"],
415
- [
416
- { permission: "bash", pattern: "rm *", action: "deny" },
417
- { permission: "bash", pattern: "*", action: "allow" },
418
- ],
419
- )
420
- expect(result.has("bash")).toBe(false)
421
- })
422
-
423
- test("disabled - disables multiple tools", () => {
424
- const result = PermissionNext.disabled(
425
- ["bash", "edit", "webfetch"],
426
- [
427
- { permission: "bash", pattern: "*", action: "deny" },
428
- { permission: "edit", pattern: "*", action: "deny" },
429
- { permission: "webfetch", pattern: "*", action: "deny" },
430
- ],
431
- )
432
- expect(result.has("bash")).toBe(true)
433
- expect(result.has("edit")).toBe(true)
434
- expect(result.has("webfetch")).toBe(true)
435
- })
436
-
437
- test("disabled - wildcard permission denies all tools", () => {
438
- const result = PermissionNext.disabled(["bash", "edit", "read"], [{ permission: "*", pattern: "*", action: "deny" }])
439
- expect(result.has("bash")).toBe(true)
440
- expect(result.has("edit")).toBe(true)
441
- expect(result.has("read")).toBe(true)
442
- })
443
-
444
- test("disabled - specific allow overrides wildcard deny", () => {
445
- const result = PermissionNext.disabled(
446
- ["bash", "edit", "read"],
447
- [
448
- { permission: "*", pattern: "*", action: "deny" },
449
- { permission: "bash", pattern: "*", action: "allow" },
450
- ],
451
- )
452
- expect(result.has("bash")).toBe(false)
453
- expect(result.has("edit")).toBe(true)
454
- expect(result.has("read")).toBe(true)
455
- })
456
-
457
- // ask tests
458
-
459
- test("ask - resolves immediately when action is allow", async () => {
460
- await using tmp = await tmpdir({ git: true })
461
- await Instance.provide({
462
- directory: tmp.path,
463
- fn: async () => {
464
- const result = await PermissionNext.ask({
465
- sessionID: "session_test",
466
- permission: "bash",
467
- patterns: ["ls"],
468
- metadata: {},
469
- always: [],
470
- ruleset: [{ permission: "bash", pattern: "*", action: "allow" }],
471
- })
472
- expect(result).toBeUndefined()
473
- },
474
- })
475
- })
476
-
477
- test("ask - throws RejectedError when action is deny", async () => {
478
- await using tmp = await tmpdir({ git: true })
479
- await Instance.provide({
480
- directory: tmp.path,
481
- fn: async () => {
482
- await expect(
483
- PermissionNext.ask({
484
- sessionID: "session_test",
485
- permission: "bash",
486
- patterns: ["rm -rf /"],
487
- metadata: {},
488
- always: [],
489
- ruleset: [{ permission: "bash", pattern: "*", action: "deny" }],
490
- }),
491
- ).rejects.toBeInstanceOf(PermissionNext.DeniedError)
492
- },
493
- })
494
- })
495
-
496
- test("ask - returns pending promise when action is ask", async () => {
497
- await using tmp = await tmpdir({ git: true })
498
- await Instance.provide({
499
- directory: tmp.path,
500
- fn: async () => {
501
- const promise = PermissionNext.ask({
502
- sessionID: "session_test",
503
- permission: "bash",
504
- patterns: ["ls"],
505
- metadata: {},
506
- always: [],
507
- ruleset: [{ permission: "bash", pattern: "*", action: "ask" }],
508
- })
509
- // Promise should be pending, not resolved
510
- expect(promise).toBeInstanceOf(Promise)
511
- // Don't await - just verify it returns a promise
512
- },
513
- })
514
- })
515
-
516
- // reply tests
517
-
518
- test("reply - once resolves the pending ask", async () => {
519
- await using tmp = await tmpdir({ git: true })
520
- await Instance.provide({
521
- directory: tmp.path,
522
- fn: async () => {
523
- const askPromise = PermissionNext.ask({
524
- id: "permission_test1",
525
- sessionID: "session_test",
526
- permission: "bash",
527
- patterns: ["ls"],
528
- metadata: {},
529
- always: [],
530
- ruleset: [],
531
- })
532
-
533
- await PermissionNext.reply({
534
- requestID: "permission_test1",
535
- reply: "once",
536
- })
537
-
538
- await expect(askPromise).resolves.toBeUndefined()
539
- },
540
- })
541
- })
542
-
543
- test("reply - reject throws RejectedError", async () => {
544
- await using tmp = await tmpdir({ git: true })
545
- await Instance.provide({
546
- directory: tmp.path,
547
- fn: async () => {
548
- const askPromise = PermissionNext.ask({
549
- id: "permission_test2",
550
- sessionID: "session_test",
551
- permission: "bash",
552
- patterns: ["ls"],
553
- metadata: {},
554
- always: [],
555
- ruleset: [],
556
- })
557
-
558
- await PermissionNext.reply({
559
- requestID: "permission_test2",
560
- reply: "reject",
561
- })
562
-
563
- await expect(askPromise).rejects.toBeInstanceOf(PermissionNext.RejectedError)
564
- },
565
- })
566
- })
567
-
568
- test("reply - always persists approval and resolves", async () => {
569
- await using tmp = await tmpdir({ git: true })
570
- await Instance.provide({
571
- directory: tmp.path,
572
- fn: async () => {
573
- const askPromise = PermissionNext.ask({
574
- id: "permission_test3",
575
- sessionID: "session_test",
576
- permission: "bash",
577
- patterns: ["ls"],
578
- metadata: {},
579
- always: ["ls"],
580
- ruleset: [],
581
- })
582
-
583
- await PermissionNext.reply({
584
- requestID: "permission_test3",
585
- reply: "always",
586
- })
587
-
588
- await expect(askPromise).resolves.toBeUndefined()
589
- },
590
- })
591
- // Re-provide to reload state with stored permissions
592
- await Instance.provide({
593
- directory: tmp.path,
594
- fn: async () => {
595
- // Stored approval should allow without asking
596
- const result = await PermissionNext.ask({
597
- sessionID: "session_test2",
598
- permission: "bash",
599
- patterns: ["ls"],
600
- metadata: {},
601
- always: [],
602
- ruleset: [],
603
- })
604
- expect(result).toBeUndefined()
605
- },
606
- })
607
- })
608
-
609
- test("reply - reject cancels all pending for same session", async () => {
610
- await using tmp = await tmpdir({ git: true })
611
- await Instance.provide({
612
- directory: tmp.path,
613
- fn: async () => {
614
- const askPromise1 = PermissionNext.ask({
615
- id: "permission_test4a",
616
- sessionID: "session_same",
617
- permission: "bash",
618
- patterns: ["ls"],
619
- metadata: {},
620
- always: [],
621
- ruleset: [],
622
- })
623
-
624
- const askPromise2 = PermissionNext.ask({
625
- id: "permission_test4b",
626
- sessionID: "session_same",
627
- permission: "edit",
628
- patterns: ["foo.ts"],
629
- metadata: {},
630
- always: [],
631
- ruleset: [],
632
- })
633
-
634
- // Catch rejections before they become unhandled
635
- const result1 = askPromise1.catch((e) => e)
636
- const result2 = askPromise2.catch((e) => e)
637
-
638
- // Reject the first one
639
- await PermissionNext.reply({
640
- requestID: "permission_test4a",
641
- reply: "reject",
642
- })
643
-
644
- // Both should be rejected
645
- expect(await result1).toBeInstanceOf(PermissionNext.RejectedError)
646
- expect(await result2).toBeInstanceOf(PermissionNext.RejectedError)
647
- },
648
- })
649
- })
650
-
651
- test("ask - checks all patterns and stops on first deny", async () => {
652
- await using tmp = await tmpdir({ git: true })
653
- await Instance.provide({
654
- directory: tmp.path,
655
- fn: async () => {
656
- await expect(
657
- PermissionNext.ask({
658
- sessionID: "session_test",
659
- permission: "bash",
660
- patterns: ["echo hello", "rm -rf /"],
661
- metadata: {},
662
- always: [],
663
- ruleset: [
664
- { permission: "bash", pattern: "*", action: "allow" },
665
- { permission: "bash", pattern: "rm *", action: "deny" },
666
- ],
667
- }),
668
- ).rejects.toBeInstanceOf(PermissionNext.DeniedError)
669
- },
670
- })
671
- })
672
-
673
- test("ask - allows all patterns when all match allow rules", async () => {
674
- await using tmp = await tmpdir({ git: true })
675
- await Instance.provide({
676
- directory: tmp.path,
677
- fn: async () => {
678
- const result = await PermissionNext.ask({
679
- sessionID: "session_test",
680
- permission: "bash",
681
- patterns: ["echo hello", "ls -la", "pwd"],
682
- metadata: {},
683
- always: [],
684
- ruleset: [{ permission: "bash", pattern: "*", action: "allow" }],
685
- })
686
- expect(result).toBeUndefined()
687
- },
688
- })
689
- })