@cuongtran001/kanna 1.63.2 → 1.63.3

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 (199) hide show
  1. package/README.md +5 -9
  2. package/dist/client/assets/{BoardsPage-BJalB-bc.js → BoardsPage-AckCuswi.js} +1 -1
  3. package/dist/client/assets/BoardsRoutePage-DzKwKy3f.js +1 -0
  4. package/dist/client/assets/ChatPage-DsaH8CTz.js +93 -0
  5. package/dist/client/assets/{CronJobRow-CPbO_jwU.js → CronJobRow-Bx9gRqKW.js} +1 -1
  6. package/dist/client/assets/{CronJobsPage-Ck3SNgGi.js → CronJobsPage-v0-2X1oX.js} +1 -1
  7. package/dist/client/assets/{FilePreviewSheet-XEuUtDPn.js → FilePreviewSheet-B6SDxSD0.js} +2 -2
  8. package/dist/client/assets/{FlowSurface-BCbeMTJe.js → FlowSurface-CLC-ZFar.js} +1 -1
  9. package/dist/client/assets/{GenUIBlock-BJy_dfs-.js → GenUIBlock-CC79Ujro.js} +2 -2
  10. package/dist/client/assets/{KannaSocketProvider-D7Lzlt35.js → KannaSocketProvider-B2sHsWP5.js} +1 -1
  11. package/dist/client/assets/{LocalProjectsPage-9LJkQjhT.js → LocalProjectsPage-DYBkcEAm.js} +1 -1
  12. package/dist/client/assets/SettingsPage-CZqMkMRW.js +9 -0
  13. package/dist/client/assets/{StackBoardsRoutePage-t_EaFNG4.js → StackBoardsRoutePage-Clviz0hX.js} +1 -1
  14. package/dist/client/assets/{ToolCallMessage-RbF9XT2B.js → ToolCallMessage-BfNbWGmc.js} +1 -1
  15. package/dist/client/assets/{VChartSurface-CKuPelsn.js → VChartSurface-CUbxyBo3.js} +1 -1
  16. package/dist/client/assets/{WorkflowsPage-B6qUmQA3.js → WorkflowsPage-rengzb97.js} +1 -1
  17. package/dist/client/assets/{WorkflowsSection-DxO0S4An.js → WorkflowsSection-DuXdLsMO.js} +1 -1
  18. package/dist/client/assets/{chart-renderer-DjYZvNxh.js → chart-renderer-D7ukB2wg.js} +1 -1
  19. package/dist/client/assets/{chatSounds-FD2DQzF6.js → chatSounds-BLg2xPDZ.js} +1 -1
  20. package/dist/client/assets/chevron-left-RbFvmeyK.js +1 -0
  21. package/dist/client/assets/{clipboard.adapter-GN9nnYak.js → clipboard.adapter-CgdNwn-1.js} +1 -1
  22. package/dist/client/assets/{columnStyle-DjA7hA9k.js → columnStyle-CMCF0ds6.js} +1 -1
  23. package/dist/client/assets/{createScopedStore-BoLB9Rhc.js → createScopedStore-BrD5NHWx.js} +1 -1
  24. package/dist/client/assets/{dist-CimtOoGK.js → dist-DNSTHlSo.js} +1 -1
  25. package/dist/client/assets/editor-icons-DY4uKeEY.js +1 -0
  26. package/dist/client/assets/{ellipsis-Cd7sO0LO.js → ellipsis-OHg0x_x1.js} +1 -1
  27. package/dist/client/assets/{flow-kinds-CW2TU_83.js → flow-kinds-Ki2DP9nP.js} +1 -1
  28. package/dist/client/assets/{folder-C7sXCaHj.js → folder-CnWzWIG5.js} +1 -1
  29. package/dist/client/assets/{formatters-wSVyvDFZ.js → formatters-BGORekd-.js} +1 -1
  30. package/dist/client/assets/{genui-EzBIAYFE.js → genui-C9i6j0XZ.js} +1 -1
  31. package/dist/client/assets/{globe-xmRpmpma.js → globe-BjR1tyE4.js} +1 -1
  32. package/dist/client/assets/{host-DCaA9W-Z.js → host-D9tJwZXl.js} +1 -1
  33. package/dist/client/assets/{index-CDqjh7nt.js → index-CesRxZ0X.js} +7 -7
  34. package/dist/client/assets/index-CxDE9VOy.css +1 -0
  35. package/dist/client/assets/{info-D4MMvZFe.js → info-DNYekoE-.js} +1 -1
  36. package/dist/client/assets/{input-C4fvDiQ4.js → input-DgW9uRZN.js} +1 -1
  37. package/dist/client/assets/{lexicalToReact-sPUyOWk6.js → lexicalToReact-CZvp9Pgz.js} +3 -3
  38. package/dist/client/assets/{log-aTaDVF_e.js → log-DnEvbp3E.js} +1 -1
  39. package/dist/client/assets/{monitor-_CRKcV1h.js → monitor-qbWfUdBG.js} +1 -1
  40. package/dist/client/assets/{pencil-fNBeUUCf.js → pencil-C965rrpd.js} +1 -1
  41. package/dist/client/assets/{pendingActionsStore-COTh4Ztq.js → pendingActionsStore-CxhI19V9.js} +1 -1
  42. package/dist/client/assets/{play-BAmkJfn6.js → play-uofLR7yT.js} +1 -1
  43. package/dist/client/assets/{plus-c75CkNEO.js → plus-D4CI6rj1.js} +1 -1
  44. package/dist/client/assets/{popover-CHM4skzh.js → popover-Bc646hKn.js} +1 -1
  45. package/dist/client/assets/{pushClient-BJH_ITRB.js → pushClient-CxnHu9h4.js} +1 -1
  46. package/dist/client/assets/runDetached-DfQXHAaY.js +1 -0
  47. package/dist/client/assets/{segmented-control-DNOwRupT.js → segmented-control-CKGX29Kl.js} +1 -1
  48. package/dist/client/assets/{select-dLHiAls6.js → select-BjeajpVt.js} +1 -1
  49. package/dist/client/assets/{settings-header-button-CGKCzFyk.js → settings-header-button-BYvHR6Xl.js} +1 -1
  50. package/dist/client/assets/{sidebarPendingActions-Dyp80Lv_.js → sidebarPendingActions-xgMTnfRY.js} +1 -1
  51. package/dist/client/assets/{square-pen-B1IxmScM.js → square-pen-DmppQdTd.js} +1 -1
  52. package/dist/client/assets/{state-mark-BhTZfcYe.js → state-mark-C3kEL1hX.js} +1 -1
  53. package/dist/client/assets/{status-pill-CwhJhaMy.js → status-pill-Bo1Nb6HX.js} +1 -1
  54. package/dist/client/assets/{terminal-Bnj7qFGw.js → terminal-DfCF-J4p.js} +1 -1
  55. package/dist/client/assets/{timer.adapter-DHt2zwsL.js → timer.adapter-C2RhP4d6.js} +1 -1
  56. package/dist/client/assets/{tools-3gJcj3hK.js → tools-t5T_3UBg.js} +1 -1
  57. package/dist/client/assets/{triangle-alert-CWrc0CIO.js → triangle-alert-CYQhobyY.js} +1 -1
  58. package/dist/client/assets/useAppGlobalState-DUa3pkCF.js +1 -0
  59. package/dist/client/assets/useKannaState-FcieR5qb.js +5 -0
  60. package/dist/client/assets/useTheme-D9sKDPEm.js +1 -0
  61. package/dist/client/index.html +35 -35
  62. package/package.json +2 -2
  63. package/src/server/agent-coordinator-types.ts +0 -6
  64. package/src/server/agent-coordinator.ts +6 -45
  65. package/src/server/agent.compaction.test.ts +1 -13
  66. package/src/server/agent.oauth-account-info.test.ts +0 -3
  67. package/src/server/agent.oauth-release.test.ts +0 -2
  68. package/src/server/agent.oauth-rotation.test.ts +0 -9
  69. package/src/server/agent.oauth-token-share-cap.test.ts +0 -1
  70. package/src/server/agent.openrouter-model.test.ts +0 -2
  71. package/src/server/agent.openrouter-watchdog.test.ts +0 -3
  72. package/src/server/agent.sdk-workflow-register.test.ts +2 -64
  73. package/src/server/agent.test.ts +1 -319
  74. package/src/server/app-settings.test.ts +14 -17
  75. package/src/server/app-settings.ts +22 -37
  76. package/src/server/auto-continue/e2e.test.ts +0 -1
  77. package/src/server/background-task-output-registry.test.ts +1 -1
  78. package/src/server/background-task-output-registry.ts +1 -1
  79. package/src/server/claude-cancel-handler.test.ts +0 -58
  80. package/src/server/claude-cancel-handler.ts +1 -17
  81. package/src/server/claude-harness-stream.test.ts +134 -0
  82. package/src/server/{claude-pty/jsonl-path.test.ts → claude-projects-path.adapter.test.ts} +1 -19
  83. package/src/server/{claude-pty/jsonl-path.adapter.ts → claude-projects-path.adapter.ts} +0 -11
  84. package/src/server/claude-send-command.test.ts +0 -1
  85. package/src/server/claude-session-config-helpers.test.ts +1 -90
  86. package/src/server/claude-session-config-helpers.ts +1 -38
  87. package/src/server/claude-session-error-handler.test.ts +0 -1
  88. package/src/server/claude-session-lifecycle.test.ts +2 -36
  89. package/src/server/claude-session-lifecycle.ts +2 -8
  90. package/src/server/claude-session-runner.test.ts +8 -36
  91. package/src/server/claude-session-runner.ts +1 -26
  92. package/src/server/claude-session-spawner.test.ts +5 -64
  93. package/src/server/claude-session-spawner.ts +49 -115
  94. package/src/server/claude-session-start.test.ts +1 -14
  95. package/src/server/claude-session-start.ts +0 -8
  96. package/src/server/claude-session-state-queries.test.ts +1 -2
  97. package/src/server/claude-session-state.test.ts +0 -1
  98. package/src/server/claude-session-state.ts +0 -4
  99. package/src/server/claude-subagent-wiring.test.ts +2 -68
  100. package/src/server/claude-subagent-wiring.ts +3 -50
  101. package/src/server/claude-turn-starter-types.ts +0 -3
  102. package/src/server/claude-turn-starter.test.ts +0 -28
  103. package/src/server/claude-turn-starter.ts +0 -5
  104. package/src/server/event-store.test.ts +0 -2
  105. package/src/server/events.ts +1 -2
  106. package/src/server/harness-types.ts +1 -2
  107. package/src/server/import-subagent-drill.e2e.test.ts +1 -1
  108. package/src/server/imported-subagents-dir.test.ts +1 -1
  109. package/src/server/imported-subagents-dir.ts +1 -1
  110. package/src/server/kanna-mcp.test.ts +0 -75
  111. package/src/server/kanna-mcp.ts +1 -47
  112. package/src/server/server.test.ts +5 -7
  113. package/src/server/server.ts +2 -18
  114. package/src/server/subagent-orchestrator.test.ts +0 -1
  115. package/src/server/subagent-provider-run.test.ts +48 -10
  116. package/src/server/subagent-provider-run.ts +12 -3
  117. package/src/server/tool-callback.test.ts +1 -1
  118. package/src/server/ws-router-agent-ctrl.test.ts +0 -45
  119. package/src/server/ws-router-agent-ctrl.ts +1 -31
  120. package/src/server/ws-router-broadcast.ts +0 -27
  121. package/src/server/ws-router-defaults.test.ts +2 -3
  122. package/src/server/ws-router-defaults.ts +2 -3
  123. package/src/server/ws-router-envelope.ts +0 -15
  124. package/src/server/ws-router-observability.test.ts +1 -1
  125. package/src/server/ws-router-settings.test.ts +2 -2
  126. package/src/server/ws-router.test.ts +2 -2
  127. package/src/server/ws-router.ts +0 -8
  128. package/src/shared/app-settings-types.ts +9 -20
  129. package/src/shared/claude-api-error-classification.ts +1 -1
  130. package/src/shared/protocol.ts +1 -11
  131. package/dist/client/assets/BoardsRoutePage-BTTjJX-Y.js +0 -1
  132. package/dist/client/assets/ChatPage-DH8wtQyD.js +0 -93
  133. package/dist/client/assets/SettingsPage-CKrZ6dGQ.js +0 -9
  134. package/dist/client/assets/chevron-left-CJ0qv9kF.js +0 -1
  135. package/dist/client/assets/editor-icons-CONDQZof.js +0 -1
  136. package/dist/client/assets/index-C-UYaDk6.css +0 -1
  137. package/dist/client/assets/runDetached-DqQ3k2ji.js +0 -1
  138. package/dist/client/assets/useAppGlobalState-DKOGrO5l.js +0 -1
  139. package/dist/client/assets/useKannaState-D7R_-dM6.js +0 -5
  140. package/dist/client/assets/useTheme-B57LIKyx.js +0 -1
  141. package/src/server/agent.pty-rotation.test.ts +0 -322
  142. package/src/server/claude-pty/auth.test.ts +0 -38
  143. package/src/server/claude-pty/auth.ts +0 -17
  144. package/src/server/claude-pty/claude-session-registry.adapter.ts +0 -68
  145. package/src/server/claude-pty/claude-session-registry.test.ts +0 -149
  146. package/src/server/claude-pty/driver.test.ts +0 -1153
  147. package/src/server/claude-pty/driver.ts +0 -829
  148. package/src/server/claude-pty/env.test.ts +0 -79
  149. package/src/server/claude-pty/env.ts +0 -20
  150. package/src/server/claude-pty/jsonl-to-event.test.ts +0 -882
  151. package/src/server/claude-pty/jsonl-to-event.ts +0 -357
  152. package/src/server/claude-pty/parity-matrix.test.ts +0 -437
  153. package/src/server/claude-pty/pid-registry.adapter.ts +0 -164
  154. package/src/server/claude-pty/pid-registry.test.ts +0 -178
  155. package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +0 -20
  156. package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +0 -32
  157. package/src/server/claude-pty/pty-instance-registry.test.ts +0 -208
  158. package/src/server/claude-pty/pty-instance-registry.ts +0 -172
  159. package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +0 -103
  160. package/src/server/claude-pty/pty-memory-sampler.adapter.ts +0 -86
  161. package/src/server/claude-pty/pty-process.adapter.ts +0 -60
  162. package/src/server/claude-pty/pty-process.test.ts +0 -49
  163. package/src/server/claude-pty/resolve-binary.adapter.ts +0 -106
  164. package/src/server/claude-pty/resolve-binary.test.ts +0 -118
  165. package/src/server/claude-pty/runtime-dir.adapter.ts +0 -19
  166. package/src/server/claude-pty/sandbox/detect.adapter.ts +0 -17
  167. package/src/server/claude-pty/sandbox/detect.test.ts +0 -17
  168. package/src/server/claude-pty/sandbox/platform.test.ts +0 -35
  169. package/src/server/claude-pty/sandbox/platform.ts +0 -27
  170. package/src/server/claude-pty/sandbox/preflight-io.adapter.ts +0 -14
  171. package/src/server/claude-pty/sandbox/preflight.test.ts +0 -69
  172. package/src/server/claude-pty/sandbox/preflight.ts +0 -44
  173. package/src/server/claude-pty/sandbox/profile-linux.adapter.ts +0 -63
  174. package/src/server/claude-pty/sandbox/profile-linux.test.ts +0 -90
  175. package/src/server/claude-pty/sandbox/profile-macos.adapter.ts +0 -72
  176. package/src/server/claude-pty/sandbox/profile-macos.test.ts +0 -68
  177. package/src/server/claude-pty/sandbox/wrap.adapter.ts +0 -58
  178. package/src/server/claude-pty/sandbox/wrap.test.ts +0 -82
  179. package/src/server/claude-pty/settings-writer.adapter.ts +0 -23
  180. package/src/server/claude-pty/settings-writer.test.ts +0 -22
  181. package/src/server/claude-pty/smoke-test-io.adapter.ts +0 -28
  182. package/src/server/claude-pty/smoke-test.test.ts +0 -199
  183. package/src/server/claude-pty/smoke-test.ts +0 -182
  184. package/src/server/claude-pty/tui-control.test.ts +0 -300
  185. package/src/server/claude-pty/tui-control.ts +0 -174
  186. package/src/server/claude-pty/tui-source.adapter.test.ts +0 -374
  187. package/src/server/claude-pty/tui-source.adapter.ts +0 -294
  188. package/src/server/http-server.adapter.test.ts +0 -18
  189. package/src/server/http-server.adapter.ts +0 -39
  190. package/src/server/kanna-mcp-http.test.ts +0 -274
  191. package/src/server/kanna-mcp-http.ts +0 -194
  192. package/src/server/kanna-mcp-tools/ask-user-question.test.ts +0 -108
  193. package/src/server/kanna-mcp-tools/ask-user-question.ts +0 -55
  194. package/src/server/kanna-mcp-tools/exit-plan-mode.test.ts +0 -61
  195. package/src/server/kanna-mcp-tools/exit-plan-mode.ts +0 -49
  196. package/src/server/mcp-zod-compat.adapter.ts +0 -20
  197. package/src/shared/pty-instance.ts +0 -43
  198. /package/src/server/{claude-pty/output-ring.test.ts → output-ring.test.ts} +0 -0
  199. /package/src/server/{claude-pty/output-ring.ts → output-ring.ts} +0 -0
@@ -1,69 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises"
3
- import { tmpdir } from "node:os"
4
- import path from "node:path"
5
- import { runSandboxPreflight } from "./preflight"
6
- import { POLICY_DEFAULT } from "../../../shared/permission-policy"
7
-
8
- describe("runSandboxPreflight (cross-platform)", () => {
9
- test("macOS: ok when sentinel denied", async () => {
10
- if (process.platform !== "darwin") return
11
- const home = await mkdtemp(path.join(tmpdir(), "kanna-sb-pf-mac-"))
12
- const runtimeDir = await mkdtemp(path.join(tmpdir(), "kanna-sb-pf-runtime-"))
13
- try {
14
- await mkdir(path.join(home, ".ssh"), { recursive: true })
15
- await writeFile(path.join(home, ".ssh", "id_rsa"), "SECRET", "utf8")
16
- const policy = { ...POLICY_DEFAULT, readPathDeny: [`${home}/.ssh`] }
17
- const result = await runSandboxPreflight({
18
- platform: "darwin",
19
- enabled: true,
20
- policy,
21
- homeDir: home,
22
- runtimeDir,
23
- sentinelPath: `${home}/.ssh/id_rsa`,
24
- })
25
- expect(result.ok).toBe(true)
26
- } finally {
27
- await rm(home, { recursive: true, force: true })
28
- await rm(runtimeDir, { recursive: true, force: true })
29
- }
30
- }, 30_000)
31
-
32
- test("linux: ok when sentinel denied via bwrap tmpfs", async () => {
33
- if (process.platform !== "linux") return
34
- const home = await mkdtemp(path.join(tmpdir(), "kanna-sb-pf-lin-"))
35
- const runtimeDir = await mkdtemp(path.join(tmpdir(), "kanna-sb-pf-runtime-"))
36
- try {
37
- await mkdir(path.join(home, ".ssh"), { recursive: true })
38
- await writeFile(path.join(home, ".ssh", "id_rsa"), "SECRET", "utf8")
39
- const policy = { ...POLICY_DEFAULT, readPathDeny: [`${home}/.ssh`] }
40
- const result = await runSandboxPreflight({
41
- platform: "linux",
42
- enabled: true,
43
- policy,
44
- homeDir: home,
45
- runtimeDir,
46
- sentinelPath: `${home}/.ssh/id_rsa`,
47
- })
48
- expect(result.ok).toBe(true)
49
- } finally {
50
- await rm(home, { recursive: true, force: true })
51
- await rm(runtimeDir, { recursive: true, force: true })
52
- }
53
- }, 30_000)
54
-
55
- test("returns ok on unsupported platform", async () => {
56
- const runtimeDir = await mkdtemp(path.join(tmpdir(), "kanna-sb-pf-win-"))
57
- try {
58
- const result = await runSandboxPreflight({
59
- platform: "win32",
60
- enabled: true,
61
- policy: POLICY_DEFAULT,
62
- homeDir: "/tmp",
63
- runtimeDir,
64
- sentinelPath: "/tmp/x",
65
- })
66
- expect(result.ok).toBe(true)
67
- } finally { await rm(runtimeDir, { recursive: true, force: true }) }
68
- })
69
- })
@@ -1,44 +0,0 @@
1
- import path from "node:path"
2
- import type { ChatPermissionPolicy } from "../../../shared/permission-policy"
3
- import { spawnExitCode, writeTextFile } from "./preflight-io.adapter"
4
- import { generateMacosProfile } from "./profile-macos.adapter"
5
- import { generateBwrapArgs } from "./profile-linux.adapter"
6
-
7
- export interface SandboxPreflightArgs {
8
- platform: NodeJS.Platform
9
- enabled: boolean
10
- policy: ChatPermissionPolicy
11
- homeDir: string
12
- runtimeDir: string
13
- sentinelPath: string
14
- }
15
-
16
- export type SandboxPreflightResult =
17
- | { ok: true }
18
- | { ok: false; reason: string }
19
-
20
- export async function runSandboxPreflight(args: SandboxPreflightArgs): Promise<SandboxPreflightResult> {
21
- if (!args.enabled) return { ok: true }
22
-
23
- if (args.platform === "darwin") {
24
- const profileBody = generateMacosProfile({ policy: args.policy, homeDir: args.homeDir })
25
- const profilePath = path.join(args.runtimeDir, "preflight.sb")
26
- await writeTextFile(profilePath, profileBody)
27
- const code = await spawnExitCode("/usr/bin/sandbox-exec", ["-f", profilePath, "/bin/cat", args.sentinelPath])
28
- if (code === 0) {
29
- return { ok: false, reason: `sentinel readable under sandbox: ${args.sentinelPath}` }
30
- }
31
- return { ok: true }
32
- }
33
-
34
- if (args.platform === "linux") {
35
- const { argv } = generateBwrapArgs({ policy: args.policy, homeDir: args.homeDir })
36
- const code = await spawnExitCode("/usr/bin/bwrap", [...argv, "/bin/cat", args.sentinelPath])
37
- if (code === 0) {
38
- return { ok: false, reason: `sentinel readable under bwrap: ${args.sentinelPath}` }
39
- }
40
- return { ok: true }
41
- }
42
-
43
- return { ok: true }
44
- }
@@ -1,63 +0,0 @@
1
- import path from "node:path"
2
- import { realpathSync } from "node:fs"
3
- import type { ChatPermissionPolicy } from "../../../shared/permission-policy"
4
-
5
- function expandTilde(p: string, homeDir: string): string {
6
- if (!p.startsWith("~")) return p
7
- return path.join(homeDir, p.slice(1).replace(/^\//, ""))
8
- }
9
-
10
- function resolveReal(p: string): string {
11
- try {
12
- return realpathSync(p)
13
- } catch {
14
- const parent = path.dirname(p)
15
- if (parent === p) return p
16
- try {
17
- return path.join(realpathSync(parent), path.basename(p))
18
- } catch {
19
- return p
20
- }
21
- }
22
- }
23
-
24
- export interface BwrapArgsResult {
25
- argv: string[]
26
- unmountableGlobs: string[]
27
- }
28
-
29
- function classify(raw: string): { kind: "path"; value: string } | { kind: "glob" } {
30
- if (raw.endsWith("/**")) return { kind: "path", value: raw.slice(0, -3) }
31
- if (raw.includes("*")) return { kind: "glob" }
32
- return { kind: "path", value: raw }
33
- }
34
-
35
- export function generateBwrapArgs(args: {
36
- policy: ChatPermissionPolicy
37
- homeDir: string
38
- }): BwrapArgsResult {
39
- const deny = new Set<string>()
40
- const unmountableGlobs = new Set<string>()
41
-
42
- const consume = (raw: string) => {
43
- const c = classify(raw)
44
- if (c.kind === "glob") {
45
- unmountableGlobs.add(raw)
46
- return
47
- }
48
- deny.add(resolveReal(expandTilde(c.value, args.homeDir)))
49
- }
50
- for (const raw of args.policy.readPathDeny) consume(raw)
51
- for (const raw of args.policy.writePathDeny) consume(raw)
52
-
53
- const argv: string[] = [
54
- "--bind", "/", "/",
55
- "--dev", "/dev",
56
- "--proc", "/proc",
57
- "--die-with-parent",
58
- ]
59
- for (const p of deny) {
60
- argv.push("--tmpfs", p)
61
- }
62
- return { argv, unmountableGlobs: [...unmountableGlobs] }
63
- }
@@ -1,90 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { mkdtempSync, symlinkSync, mkdirSync, realpathSync } from "node:fs"
3
- import { tmpdir } from "node:os"
4
- import path from "node:path"
5
- import { generateBwrapArgs } from "./profile-linux.adapter"
6
-
7
- function r(p: string): string {
8
- try {
9
- return realpathSync(p)
10
- } catch {
11
- try {
12
- return path.join(realpathSync(path.dirname(p)), path.basename(p))
13
- } catch {
14
- return p
15
- }
16
- }
17
- }
18
-
19
- const POLICY = {
20
- defaultAction: "ask" as const,
21
- bash: { autoAllowVerbs: [] },
22
- readPathDeny: ["~/.ssh", "/etc/shadow"],
23
- writePathDeny: ["/etc/**"],
24
- toolDenyList: [],
25
- toolAllowList: [],
26
- }
27
-
28
- function tmpfsTargets(argv: string[]): string[] {
29
- const out: string[] = []
30
- for (let i = 0; i < argv.length; i++) {
31
- if (argv[i] === "--tmpfs") out.push(argv[i + 1])
32
- }
33
- return out
34
- }
35
-
36
- describe("generateBwrapArgs", () => {
37
- test("emits base --bind / / and --die-with-parent", () => {
38
- const { argv } = generateBwrapArgs({ policy: POLICY, homeDir: "/home/u" })
39
- expect(argv).toContain("--bind")
40
- expect(argv).toContain("--die-with-parent")
41
- })
42
-
43
- test("emits --tmpfs for each non-glob deny entry (tilde expanded, symlink-resolved)", () => {
44
- const { argv } = generateBwrapArgs({ policy: POLICY, homeDir: "/home/u" })
45
- const targets = tmpfsTargets(argv)
46
- expect(targets).toContain(r("/home/u/.ssh"))
47
- expect(targets).toContain(r("/etc/shadow"))
48
- })
49
-
50
- test("strips /** suffix to the directory path", () => {
51
- const { argv } = generateBwrapArgs({ policy: POLICY, homeDir: "/home/u" })
52
- expect(tmpfsTargets(argv)).toContain(r("/etc"))
53
- })
54
-
55
- test("glob patterns surface in unmountableGlobs, never as a --tmpfs arg", () => {
56
- const { argv, unmountableGlobs } = generateBwrapArgs({
57
- policy: { ...POLICY, readPathDeny: ["**/.env", "**/*.pem", "~/.ssh"] },
58
- homeDir: "/home/u",
59
- })
60
- expect(unmountableGlobs).toContain("**/.env")
61
- expect(unmountableGlobs).toContain("**/*.pem")
62
- expect(tmpfsTargets(argv)).toContain(r("/home/u/.ssh"))
63
- expect(tmpfsTargets(argv).some((t) => t.includes("*"))).toBe(false)
64
- })
65
-
66
- test("resolves a symlinked homeDir to its real target (no symlink bypass)", () => {
67
- const root = mkdtempSync(path.join(tmpdir(), "kanna-bwrap-sym-"))
68
- const realHome = path.join(root, "real-home")
69
- mkdirSync(path.join(realHome, ".ssh"), { recursive: true })
70
- const linkHome = path.join(root, "link-home")
71
- symlinkSync(realHome, linkHome)
72
-
73
- const { argv } = generateBwrapArgs({
74
- policy: { ...POLICY, readPathDeny: ["~/.ssh"], writePathDeny: [] },
75
- homeDir: linkHome,
76
- })
77
- const targets = tmpfsTargets(argv)
78
- expect(targets).toContain(path.join(realpathSync(realHome), ".ssh"))
79
- expect(targets).not.toContain(path.join(linkHome, ".ssh"))
80
- })
81
-
82
- test("non-existent deny path falls back to walk-up real prefix (never throws)", () => {
83
- const root = mkdtempSync(path.join(tmpdir(), "kanna-bwrap-ne-"))
84
- const { argv } = generateBwrapArgs({
85
- policy: { ...POLICY, readPathDeny: [path.join(root, "nope", "creds")], writePathDeny: [] },
86
- homeDir: root,
87
- })
88
- expect(tmpfsTargets(argv).length).toBeGreaterThanOrEqual(1)
89
- })
90
- })
@@ -1,72 +0,0 @@
1
- import path from "node:path"
2
- import { realpathSync } from "node:fs"
3
- import type { ChatPermissionPolicy } from "../../../shared/permission-policy"
4
-
5
- function expandTilde(p: string, homeDir: string): string {
6
- if (!p.startsWith("~")) return p
7
- return path.join(homeDir, p.slice(1).replace(/^\//, ""))
8
- }
9
-
10
- function resolveReal(p: string): string {
11
- try {
12
- return realpathSync(p)
13
- } catch {
14
- const parent = path.dirname(p)
15
- if (parent === p) return p
16
- try {
17
- return path.join(realpathSync(parent), path.basename(p))
18
- } catch {
19
- return p
20
- }
21
- }
22
- }
23
-
24
- function escapeForScheme(s: string): string {
25
- return s
26
- .replace(/[\x00-\x1f\x7f]/g, "")
27
- .replace(/\\/g, "\\\\")
28
- .replace(/"/g, '\\"')
29
- }
30
-
31
- function denyEntry(action: string, raw: string, homeDir: string): string {
32
- if (raw.endsWith("/**")) {
33
- const base = resolveReal(expandTilde(raw.slice(0, -3), homeDir))
34
- return `(deny ${action} (subpath "${escapeForScheme(base)}"))`
35
- }
36
-
37
- const expanded = expandTilde(raw, homeDir)
38
-
39
- if (expanded.includes("*")) {
40
- return `(deny ${action} (literal "${escapeForScheme(expanded)}"))`
41
- }
42
-
43
- const resolved = resolveReal(expanded)
44
- const escaped = escapeForScheme(resolved)
45
-
46
- if (raw.startsWith("~")) {
47
- return `(deny ${action} (subpath "${escaped}"))`
48
- }
49
-
50
- const basename = path.basename(resolved)
51
- if (basename.startsWith(".")) {
52
- return `(deny ${action} (subpath "${escaped}"))`
53
- }
54
- return `(deny ${action} (literal "${escaped}"))`
55
- }
56
-
57
- export function generateMacosProfile(args: {
58
- policy: ChatPermissionPolicy
59
- homeDir: string
60
- }): string {
61
- const readDenies = args.policy.readPathDeny.map((p) => denyEntry("file-read*", p, args.homeDir))
62
- const writeDenies = args.policy.writePathDeny.map((p) => denyEntry("file-write*", p, args.homeDir))
63
-
64
- const lines = [
65
- "(version 1)",
66
- "(allow default)",
67
- ";; Kanna-generated profile for claude PTY",
68
- ...readDenies,
69
- ...writeDenies,
70
- ]
71
- return lines.join("\n")
72
- }
@@ -1,68 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { realpathSync } from "node:fs"
3
- import path from "node:path"
4
- import { generateMacosProfile } from "./profile-macos.adapter"
5
-
6
- function r(p: string): string {
7
- try { return realpathSync(p) } catch { }
8
- try { return path.join(realpathSync(path.dirname(p)), path.basename(p)) } catch { return p }
9
- }
10
-
11
- const POLICY = {
12
- defaultAction: "ask" as const,
13
- bash: { autoAllowVerbs: [] },
14
- readPathDeny: ["~/.ssh", "~/.aws", "/etc/shadow"],
15
- writePathDeny: ["/etc/**", "~/.ssh/**"],
16
- toolDenyList: [],
17
- toolAllowList: [],
18
- }
19
-
20
- describe("generateMacosProfile", () => {
21
- test("emits version + default-allow + deny entries for readPathDeny", () => {
22
- const profile = generateMacosProfile({ policy: POLICY, homeDir: "/Users/u" })
23
- expect(profile).toContain("(version 1)")
24
- expect(profile).toContain(`(deny file-read* (subpath "/Users/u/.ssh"))`)
25
- expect(profile).toContain(`(deny file-read* (subpath "/Users/u/.aws"))`)
26
- expect(profile).toContain(`(deny file-read* (literal "${r("/etc/shadow")}"))`)
27
- })
28
-
29
- test("emits writePathDeny entries as file-write* denies", () => {
30
- const profile = generateMacosProfile({ policy: POLICY, homeDir: "/Users/u" })
31
- expect(profile).toContain(`file-write* (subpath "${r("/etc")}")`)
32
- expect(profile).toContain(`file-write* (subpath "/Users/u/.ssh")`)
33
- })
34
-
35
- test("escapes quotes in paths defensively", () => {
36
- const profile = generateMacosProfile({
37
- policy: { ...POLICY, readPathDeny: ['/tmp/with"quote'] },
38
- homeDir: "/Users/u",
39
- })
40
- const match = profile.match(/subpath "[^"]*"/g) ?? profile.match(/literal "[^"]*"/g)
41
- expect(match).not.toBeNull()
42
- })
43
-
44
- test("strips control chars (newline/CR/tab) so a crafted path cannot inject profile clauses", () => {
45
- const profile = generateMacosProfile({
46
- policy: {
47
- ...POLICY,
48
- readPathDeny: ['/tmp/evil\n(allow file-read* (subpath "/"))'],
49
- writePathDeny: [],
50
- },
51
- homeDir: "/Users/u",
52
- })
53
- const denyLines = profile.split("\n").filter((l) => l.startsWith("(deny"))
54
- expect(denyLines).toHaveLength(1)
55
- expect(profile.split("\n")).not.toContain(`(allow file-read* (subpath "/"))`)
56
- expect(denyLines[0]).toContain('\\"/\\"')
57
- expect(/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(profile)).toBe(false)
58
- })
59
-
60
- test("skips empty deny lists", () => {
61
- const empty = generateMacosProfile({
62
- policy: { ...POLICY, readPathDeny: [], writePathDeny: [] },
63
- homeDir: "/Users/u",
64
- })
65
- expect(empty).toContain("(version 1)")
66
- expect(empty).not.toContain("file-read*")
67
- })
68
- })
@@ -1,58 +0,0 @@
1
- import path from "node:path"
2
- import { writeFile } from "node:fs/promises"
3
- import type { ChatPermissionPolicy } from "../../../shared/permission-policy"
4
- import { generateMacosProfile } from "./profile-macos.adapter"
5
- import { generateBwrapArgs } from "./profile-linux.adapter"
6
- import { log } from "../../../shared/log"
7
-
8
- const SANDBOX_EXEC = "/usr/bin/sandbox-exec"
9
- const BWRAP = "/usr/bin/bwrap"
10
-
11
- export interface WrapArgs {
12
- platform: NodeJS.Platform
13
- enabled: boolean
14
- policy: ChatPermissionPolicy
15
- homeDir: string
16
- runtimeDir: string
17
- command: string
18
- args: string[]
19
- }
20
-
21
- export interface WrapResult {
22
- command: string
23
- args: string[]
24
- }
25
-
26
- export async function wrapWithSandbox(opts: WrapArgs): Promise<WrapResult> {
27
- if (!opts.enabled) {
28
- return { command: opts.command, args: opts.args }
29
- }
30
- if (opts.platform === "darwin") {
31
- const profileBody = generateMacosProfile({ policy: opts.policy, homeDir: opts.homeDir })
32
- const profilePath = path.join(opts.runtimeDir, "claude-sandbox.sb")
33
- await writeFile(profilePath, profileBody, "utf8")
34
- return {
35
- command: SANDBOX_EXEC,
36
- args: ["-f", profilePath, opts.command, ...opts.args],
37
- }
38
- }
39
- if (opts.platform === "linux") {
40
- const { argv, unmountableGlobs } = generateBwrapArgs({
41
- policy: opts.policy,
42
- homeDir: opts.homeDir,
43
- })
44
- if (unmountableGlobs.length > 0) {
45
- log.warn(
46
- "[claude-pty/sandbox] bwrap cannot tmpfs glob deny patterns "
47
- + `(${unmountableGlobs.join(", ")}); these are enforced by the `
48
- + "kanna-mcp tool-callback layer (permission-gate minimatch), not "
49
- + "the OS sandbox. Literal credential dirs remain tmpfs-protected.",
50
- )
51
- }
52
- return {
53
- command: BWRAP,
54
- args: [...argv, opts.command, ...opts.args],
55
- }
56
- }
57
- return { command: opts.command, args: opts.args }
58
- }
@@ -1,82 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { mkdtemp, rm, readFile } from "node:fs/promises"
3
- import { tmpdir } from "node:os"
4
- import path from "node:path"
5
- import { wrapWithSandbox } from "./wrap.adapter"
6
- import { POLICY_DEFAULT } from "../../../shared/permission-policy"
7
-
8
- describe("wrapWithSandbox (async dispatch)", () => {
9
- test("darwin enabled → prepends sandbox-exec and writes profile", async () => {
10
- if (process.platform !== "darwin") return
11
- const runtimeDir = await mkdtemp(path.join(tmpdir(), "kanna-wrap-"))
12
- try {
13
- const result = await wrapWithSandbox({
14
- platform: "darwin",
15
- enabled: true,
16
- policy: POLICY_DEFAULT,
17
- homeDir: "/Users/u",
18
- runtimeDir,
19
- command: "/usr/local/bin/claude",
20
- args: ["--model", "x"],
21
- })
22
- expect(result.command).toBe("/usr/bin/sandbox-exec")
23
- expect(result.args[0]).toBe("-f")
24
- const profile = await readFile(result.args[1], "utf8")
25
- expect(profile).toContain("(version 1)")
26
- } finally { await rm(runtimeDir, { recursive: true, force: true }) }
27
- })
28
-
29
- test("linux enabled → prepends bwrap argv", async () => {
30
- if (process.platform !== "linux") return
31
- const runtimeDir = await mkdtemp(path.join(tmpdir(), "kanna-wrap-"))
32
- try {
33
- const result = await wrapWithSandbox({
34
- platform: "linux",
35
- enabled: true,
36
- policy: POLICY_DEFAULT,
37
- homeDir: "/home/u",
38
- runtimeDir,
39
- command: "/usr/local/bin/claude",
40
- args: ["--model", "x"],
41
- })
42
- expect(result.command).toBe("/usr/bin/bwrap")
43
- expect(result.args).toContain("--bind")
44
- expect(result.args).toContain("--die-with-parent")
45
- expect(result.args).toContain("/usr/local/bin/claude")
46
- expect(result.args).toContain("--model")
47
- } finally { await rm(runtimeDir, { recursive: true, force: true }) }
48
- })
49
-
50
- test("disabled → pass through", async () => {
51
- const runtimeDir = await mkdtemp(path.join(tmpdir(), "kanna-wrap-"))
52
- try {
53
- const result = await wrapWithSandbox({
54
- platform: "darwin",
55
- enabled: false,
56
- policy: POLICY_DEFAULT,
57
- homeDir: "/Users/u",
58
- runtimeDir,
59
- command: "/usr/local/bin/claude",
60
- args: ["--model", "x"],
61
- })
62
- expect(result.command).toBe("/usr/local/bin/claude")
63
- expect(result.args).toEqual(["--model", "x"])
64
- } finally { await rm(runtimeDir, { recursive: true, force: true }) }
65
- })
66
-
67
- test("unsupported platform → pass through", async () => {
68
- const runtimeDir = await mkdtemp(path.join(tmpdir(), "kanna-wrap-"))
69
- try {
70
- const result = await wrapWithSandbox({
71
- platform: "win32",
72
- enabled: true,
73
- policy: POLICY_DEFAULT,
74
- homeDir: "/Users/u",
75
- runtimeDir,
76
- command: "claude.exe",
77
- args: ["--model", "x"],
78
- })
79
- expect(result.command).toBe("claude.exe")
80
- } finally { await rm(runtimeDir, { recursive: true, force: true }) }
81
- })
82
- })
@@ -1,23 +0,0 @@
1
- import { mkdir, writeFile } from "node:fs/promises"
2
- import path from "node:path"
3
-
4
- export interface WriteSpawnSettingsResult {
5
- settingsPath: string
6
- }
7
-
8
- export async function writeSpawnSettings(args: {
9
- runtimeDir: string
10
- }): Promise<WriteSpawnSettingsResult> {
11
- await mkdir(args.runtimeDir, { recursive: true, mode: 0o700 })
12
- const settingsPath = path.join(args.runtimeDir, "settings.local.json")
13
- const body = {
14
- spinnerTipsEnabled: false,
15
- showTurnDuration: false,
16
- syntaxHighlightingDisabled: true,
17
- permissions: {
18
- allow: ["mcp__kanna__*"],
19
- },
20
- }
21
- await writeFile(settingsPath, JSON.stringify(body, null, 2), { encoding: "utf8", mode: 0o600 })
22
- return { settingsPath }
23
- }
@@ -1,22 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { mkdtemp, rm, readFile } from "node:fs/promises"
3
- import { tmpdir } from "node:os"
4
- import path from "node:path"
5
- import { writeSpawnSettings } from "./settings-writer.adapter"
6
-
7
- describe("writeSpawnSettings", () => {
8
- test("writes per-spawn settings with claimed keys", async () => {
9
- const dir = await mkdtemp(path.join(tmpdir(), "kanna-settings-"))
10
- try {
11
- const result = await writeSpawnSettings({ runtimeDir: dir })
12
- expect(result.settingsPath.startsWith(dir)).toBe(true)
13
- const raw = await readFile(result.settingsPath, "utf8")
14
- const parsed = JSON.parse(raw)
15
- expect(parsed.spinnerTipsEnabled).toBe(false)
16
- expect(parsed.showTurnDuration).toBe(false)
17
- expect(parsed.permissions?.allow).toContain("mcp__kanna__*")
18
- } finally {
19
- await rm(dir, { recursive: true, force: true })
20
- }
21
- })
22
- })
@@ -1,28 +0,0 @@
1
- import { mkdir, mkdtemp, readFile, writeFile, rm } from "node:fs/promises"
2
- import { existsSync } from "node:fs"
3
- import path from "node:path"
4
- import { tmpdir } from "node:os"
5
-
6
- export function makeTempCwd(prefix: string): Promise<string> {
7
- return mkdtemp(path.join(tmpdir(), prefix))
8
- }
9
-
10
- export function readTextFile(p: string): Promise<string> {
11
- return readFile(p, "utf8")
12
- }
13
-
14
- export async function rmDirRecursive(p: string): Promise<void> {
15
- await rm(p, { recursive: true, force: true })
16
- }
17
-
18
- export function fileExists(p: string): boolean {
19
- return existsSync(p)
20
- }
21
-
22
- export async function mkdirRecursive(p: string): Promise<void> {
23
- await mkdir(p, { recursive: true })
24
- }
25
-
26
- export async function writeFile0600(p: string, contents: string): Promise<void> {
27
- await writeFile(p, contents, { encoding: "utf8", mode: 0o600 })
28
- }