@gajae-code/coding-agent 0.2.4 → 0.3.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 (266) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +145 -2
  4. package/dist/types/commands/harness.d.ts +37 -0
  5. package/dist/types/config/settings-schema.d.ts +13 -3
  6. package/dist/types/config/settings.d.ts +3 -1
  7. package/dist/types/deep-interview/render-middleware.d.ts +5 -0
  8. package/dist/types/discovery/helpers.d.ts +1 -0
  9. package/dist/types/exec/bash-executor.d.ts +8 -1
  10. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  12. package/dist/types/extensibility/shared-events.d.ts +1 -0
  13. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +2 -0
  14. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
  16. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  17. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  18. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  19. package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
  20. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  21. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  22. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  23. package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
  24. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  25. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  26. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  27. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  28. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  29. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  30. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  31. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  32. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  33. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  34. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  35. package/dist/types/harness-control-plane/types.d.ts +162 -0
  36. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  37. package/dist/types/hooks/skill-state.d.ts +2 -29
  38. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  39. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  40. package/dist/types/modes/components/skill-hud/render.d.ts +1 -1
  41. package/dist/types/modes/interactive-mode.d.ts +2 -0
  42. package/dist/types/modes/theme/defaults/index.d.ts +45 -9477
  43. package/dist/types/modes/theme/theme.d.ts +1 -5
  44. package/dist/types/modes/types.d.ts +2 -0
  45. package/dist/types/sdk.d.ts +4 -0
  46. package/dist/types/session/agent-session.d.ts +8 -0
  47. package/dist/types/session/streaming-output.d.ts +11 -0
  48. package/dist/types/skill-state/active-state.d.ts +3 -0
  49. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  50. package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
  51. package/dist/types/task/executor.d.ts +3 -0
  52. package/dist/types/task/types.d.ts +56 -3
  53. package/dist/types/tools/bash-allowed-prefixes.d.ts +5 -0
  54. package/dist/types/tools/bash.d.ts +24 -0
  55. package/dist/types/tools/cron.d.ts +110 -0
  56. package/dist/types/tools/index.d.ts +4 -0
  57. package/dist/types/tools/monitor.d.ts +54 -0
  58. package/dist/types/tools/subagent.d.ts +11 -1
  59. package/dist/types/web/search/index.d.ts +1 -0
  60. package/dist/types/web/search/provider.d.ts +11 -4
  61. package/dist/types/web/search/providers/duckduckgo.d.ts +57 -0
  62. package/dist/types/web/search/types.d.ts +1 -1
  63. package/package.json +7 -7
  64. package/src/async/job-manager.ts +522 -6
  65. package/src/cli/agents-cli.ts +3 -0
  66. package/src/cli/auth-broker-cli.ts +1 -0
  67. package/src/cli/config-cli.ts +10 -2
  68. package/src/cli.ts +2 -0
  69. package/src/commands/harness.ts +592 -0
  70. package/src/commands/team.ts +36 -39
  71. package/src/config/settings-schema.ts +15 -2
  72. package/src/config/settings.ts +49 -7
  73. package/src/deep-interview/render-middleware.ts +366 -0
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +9 -2
  75. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  76. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  77. package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
  78. package/src/discovery/helpers.ts +5 -0
  79. package/src/eval/js/shared/rewrite-imports.ts +1 -2
  80. package/src/exec/bash-executor.ts +20 -9
  81. package/src/extensibility/custom-tools/types.ts +1 -0
  82. package/src/extensibility/extensions/types.ts +6 -0
  83. package/src/extensibility/shared-events.ts +1 -0
  84. package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
  85. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  86. package/src/gjc-runtime/ralplan-runtime.ts +27 -10
  87. package/src/gjc-runtime/restricted-role-agent-bash.ts +5 -0
  88. package/src/gjc-runtime/state-graph.ts +86 -0
  89. package/src/gjc-runtime/state-migrations.ts +132 -0
  90. package/src/gjc-runtime/state-renderer.ts +345 -0
  91. package/src/gjc-runtime/state-runtime.ts +733 -21
  92. package/src/gjc-runtime/state-validation.ts +49 -0
  93. package/src/gjc-runtime/state-writer.ts +718 -0
  94. package/src/gjc-runtime/team-runtime.ts +1083 -89
  95. package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
  96. package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
  97. package/src/gjc-runtime/workflow-manifest.ts +425 -0
  98. package/src/harness-control-plane/classifier.ts +128 -0
  99. package/src/harness-control-plane/control-endpoint.ts +137 -0
  100. package/src/harness-control-plane/finalize.ts +222 -0
  101. package/src/harness-control-plane/frame-mapper.ts +286 -0
  102. package/src/harness-control-plane/operate.ts +225 -0
  103. package/src/harness-control-plane/owner.ts +553 -0
  104. package/src/harness-control-plane/preserve.ts +102 -0
  105. package/src/harness-control-plane/receipts.ts +216 -0
  106. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  107. package/src/harness-control-plane/seams.ts +39 -0
  108. package/src/harness-control-plane/session-lease.ts +388 -0
  109. package/src/harness-control-plane/state-machine.ts +97 -0
  110. package/src/harness-control-plane/storage.ts +257 -0
  111. package/src/harness-control-plane/types.ts +214 -0
  112. package/src/hooks/skill-keywords.ts +4 -2
  113. package/src/hooks/skill-state.ts +25 -42
  114. package/src/internal-urls/docs-index.generated.ts +6 -4
  115. package/src/lsp/render.ts +1 -1
  116. package/src/modes/acp/acp-agent.ts +1 -1
  117. package/src/modes/acp/acp-client-bridge.ts +1 -1
  118. package/src/modes/components/agent-dashboard.ts +1 -1
  119. package/src/modes/components/assistant-message.ts +5 -1
  120. package/src/modes/components/diff.ts +2 -2
  121. package/src/modes/components/hook-selector.ts +72 -2
  122. package/src/modes/components/skill-hud/render.ts +7 -2
  123. package/src/modes/controllers/event-controller.ts +71 -6
  124. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  125. package/src/modes/controllers/input-controller.ts +19 -3
  126. package/src/modes/controllers/selector-controller.ts +3 -2
  127. package/src/modes/interactive-mode.ts +21 -2
  128. package/src/modes/theme/defaults/index.ts +0 -196
  129. package/src/modes/theme/theme.ts +35 -35
  130. package/src/modes/types.ts +2 -0
  131. package/src/prompts/agents/architect.md +5 -1
  132. package/src/prompts/agents/critic.md +5 -1
  133. package/src/prompts/agents/executor.md +13 -0
  134. package/src/prompts/agents/frontmatter.md +1 -0
  135. package/src/prompts/agents/planner.md +5 -1
  136. package/src/prompts/tools/bash.md +9 -0
  137. package/src/prompts/tools/cron.md +25 -0
  138. package/src/prompts/tools/monitor.md +30 -0
  139. package/src/prompts/tools/subagent.md +33 -3
  140. package/src/runtime-mcp/oauth-flow.ts +4 -2
  141. package/src/sdk.ts +7 -0
  142. package/src/session/agent-session.ts +247 -38
  143. package/src/session/session-manager.ts +13 -1
  144. package/src/session/streaming-output.ts +21 -0
  145. package/src/skill-state/active-state.ts +222 -78
  146. package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
  147. package/src/skill-state/initial-phase.ts +2 -0
  148. package/src/skill-state/workflow-state-contract.ts +26 -0
  149. package/src/task/agents.ts +1 -0
  150. package/src/task/executor.ts +51 -8
  151. package/src/task/index.ts +120 -8
  152. package/src/task/render.ts +6 -3
  153. package/src/task/types.ts +57 -3
  154. package/src/tools/ask.ts +28 -7
  155. package/src/tools/bash-allowed-prefixes.ts +169 -0
  156. package/src/tools/bash.ts +190 -29
  157. package/src/tools/browser/tab-worker.ts +1 -1
  158. package/src/tools/cron.ts +665 -0
  159. package/src/tools/index.ts +20 -2
  160. package/src/tools/monitor.ts +136 -0
  161. package/src/tools/subagent.ts +255 -64
  162. package/src/vim/engine.ts +3 -3
  163. package/src/web/search/index.ts +31 -18
  164. package/src/web/search/provider.ts +57 -12
  165. package/src/web/search/providers/duckduckgo.ts +279 -0
  166. package/src/web/search/types.ts +2 -0
  167. package/src/modes/theme/dark.json +0 -95
  168. package/src/modes/theme/defaults/alabaster.json +0 -93
  169. package/src/modes/theme/defaults/amethyst.json +0 -96
  170. package/src/modes/theme/defaults/anthracite.json +0 -93
  171. package/src/modes/theme/defaults/basalt.json +0 -91
  172. package/src/modes/theme/defaults/birch.json +0 -95
  173. package/src/modes/theme/defaults/dark-abyss.json +0 -91
  174. package/src/modes/theme/defaults/dark-arctic.json +0 -104
  175. package/src/modes/theme/defaults/dark-aurora.json +0 -95
  176. package/src/modes/theme/defaults/dark-catppuccin.json +0 -107
  177. package/src/modes/theme/defaults/dark-cavern.json +0 -91
  178. package/src/modes/theme/defaults/dark-copper.json +0 -95
  179. package/src/modes/theme/defaults/dark-cosmos.json +0 -90
  180. package/src/modes/theme/defaults/dark-cyberpunk.json +0 -102
  181. package/src/modes/theme/defaults/dark-dracula.json +0 -98
  182. package/src/modes/theme/defaults/dark-eclipse.json +0 -91
  183. package/src/modes/theme/defaults/dark-ember.json +0 -95
  184. package/src/modes/theme/defaults/dark-equinox.json +0 -90
  185. package/src/modes/theme/defaults/dark-forest.json +0 -96
  186. package/src/modes/theme/defaults/dark-github.json +0 -105
  187. package/src/modes/theme/defaults/dark-gruvbox.json +0 -112
  188. package/src/modes/theme/defaults/dark-lavender.json +0 -95
  189. package/src/modes/theme/defaults/dark-lunar.json +0 -89
  190. package/src/modes/theme/defaults/dark-midnight.json +0 -95
  191. package/src/modes/theme/defaults/dark-monochrome.json +0 -94
  192. package/src/modes/theme/defaults/dark-monokai.json +0 -98
  193. package/src/modes/theme/defaults/dark-nebula.json +0 -90
  194. package/src/modes/theme/defaults/dark-nord.json +0 -97
  195. package/src/modes/theme/defaults/dark-ocean.json +0 -101
  196. package/src/modes/theme/defaults/dark-one.json +0 -100
  197. package/src/modes/theme/defaults/dark-poimandres.json +0 -141
  198. package/src/modes/theme/defaults/dark-rainforest.json +0 -91
  199. package/src/modes/theme/defaults/dark-reef.json +0 -91
  200. package/src/modes/theme/defaults/dark-retro.json +0 -92
  201. package/src/modes/theme/defaults/dark-rose-pine.json +0 -96
  202. package/src/modes/theme/defaults/dark-sakura.json +0 -95
  203. package/src/modes/theme/defaults/dark-slate.json +0 -95
  204. package/src/modes/theme/defaults/dark-solarized.json +0 -97
  205. package/src/modes/theme/defaults/dark-solstice.json +0 -90
  206. package/src/modes/theme/defaults/dark-starfall.json +0 -91
  207. package/src/modes/theme/defaults/dark-sunset.json +0 -99
  208. package/src/modes/theme/defaults/dark-swamp.json +0 -90
  209. package/src/modes/theme/defaults/dark-synthwave.json +0 -103
  210. package/src/modes/theme/defaults/dark-taiga.json +0 -91
  211. package/src/modes/theme/defaults/dark-terminal.json +0 -95
  212. package/src/modes/theme/defaults/dark-tokyo-night.json +0 -101
  213. package/src/modes/theme/defaults/dark-tundra.json +0 -91
  214. package/src/modes/theme/defaults/dark-twilight.json +0 -91
  215. package/src/modes/theme/defaults/dark-volcanic.json +0 -91
  216. package/src/modes/theme/defaults/graphite.json +0 -92
  217. package/src/modes/theme/defaults/light-arctic.json +0 -107
  218. package/src/modes/theme/defaults/light-aurora-day.json +0 -91
  219. package/src/modes/theme/defaults/light-canyon.json +0 -91
  220. package/src/modes/theme/defaults/light-catppuccin.json +0 -106
  221. package/src/modes/theme/defaults/light-cirrus.json +0 -90
  222. package/src/modes/theme/defaults/light-coral.json +0 -95
  223. package/src/modes/theme/defaults/light-cyberpunk.json +0 -96
  224. package/src/modes/theme/defaults/light-dawn.json +0 -90
  225. package/src/modes/theme/defaults/light-dunes.json +0 -91
  226. package/src/modes/theme/defaults/light-eucalyptus.json +0 -95
  227. package/src/modes/theme/defaults/light-forest.json +0 -100
  228. package/src/modes/theme/defaults/light-frost.json +0 -95
  229. package/src/modes/theme/defaults/light-github.json +0 -115
  230. package/src/modes/theme/defaults/light-glacier.json +0 -91
  231. package/src/modes/theme/defaults/light-gruvbox.json +0 -108
  232. package/src/modes/theme/defaults/light-haze.json +0 -90
  233. package/src/modes/theme/defaults/light-honeycomb.json +0 -95
  234. package/src/modes/theme/defaults/light-lagoon.json +0 -91
  235. package/src/modes/theme/defaults/light-lavender.json +0 -95
  236. package/src/modes/theme/defaults/light-meadow.json +0 -91
  237. package/src/modes/theme/defaults/light-mint.json +0 -95
  238. package/src/modes/theme/defaults/light-monochrome.json +0 -101
  239. package/src/modes/theme/defaults/light-ocean.json +0 -99
  240. package/src/modes/theme/defaults/light-one.json +0 -99
  241. package/src/modes/theme/defaults/light-opal.json +0 -91
  242. package/src/modes/theme/defaults/light-orchard.json +0 -91
  243. package/src/modes/theme/defaults/light-paper.json +0 -95
  244. package/src/modes/theme/defaults/light-poimandres.json +0 -141
  245. package/src/modes/theme/defaults/light-prism.json +0 -90
  246. package/src/modes/theme/defaults/light-retro.json +0 -98
  247. package/src/modes/theme/defaults/light-sand.json +0 -95
  248. package/src/modes/theme/defaults/light-savanna.json +0 -91
  249. package/src/modes/theme/defaults/light-solarized.json +0 -102
  250. package/src/modes/theme/defaults/light-soleil.json +0 -90
  251. package/src/modes/theme/defaults/light-sunset.json +0 -99
  252. package/src/modes/theme/defaults/light-synthwave.json +0 -98
  253. package/src/modes/theme/defaults/light-tokyo-night.json +0 -111
  254. package/src/modes/theme/defaults/light-wetland.json +0 -91
  255. package/src/modes/theme/defaults/light-zenith.json +0 -89
  256. package/src/modes/theme/defaults/limestone.json +0 -94
  257. package/src/modes/theme/defaults/mahogany.json +0 -97
  258. package/src/modes/theme/defaults/marble.json +0 -93
  259. package/src/modes/theme/defaults/obsidian.json +0 -91
  260. package/src/modes/theme/defaults/onyx.json +0 -91
  261. package/src/modes/theme/defaults/pearl.json +0 -93
  262. package/src/modes/theme/defaults/porcelain.json +0 -91
  263. package/src/modes/theme/defaults/quartz.json +0 -96
  264. package/src/modes/theme/defaults/sandstone.json +0 -95
  265. package/src/modes/theme/defaults/titanium.json +0 -90
  266. package/src/modes/theme/light.json +0 -93
@@ -0,0 +1,169 @@
1
+ export interface BashAllowedPrefixesCheck {
2
+ allowed: boolean;
3
+ reason?: string;
4
+ }
5
+
6
+ const SHELL_CONTROL_CHARS = new Set([";", "|", "&", "<", ">", "(", ")"]);
7
+ const UNSAFE_UNQUOTED_EXPANSION_CHARS = new Set(["$", "*", "?", "[", "]", "{", "}", "~"]);
8
+ const STATE_FLAGS_WITH_VALUES = new Set(["--input", "--mode", "--session-id", "--thread-id", "--turn-id", "--to"]);
9
+ const STATE_ACTIONS = new Set(["read", "write", "clear", "contract", "handoff"]);
10
+ const ALLOWED_STATE_ACTIONS = new Set(["read", "write", "contract"]);
11
+
12
+ function parseShellWords(command: string): { words: string[]; reason?: string } {
13
+ const words: string[] = [];
14
+ let current = "";
15
+ let quote: "single" | "double" | null = null;
16
+
17
+ for (let index = 0; index < command.length; index += 1) {
18
+ const char = command[index]!;
19
+ const next = command[index + 1];
20
+
21
+ if (quote === "single") {
22
+ if (char === "'") {
23
+ quote = null;
24
+ } else {
25
+ current += char;
26
+ }
27
+ continue;
28
+ }
29
+
30
+ if (quote === "double") {
31
+ if (char === '"') {
32
+ quote = null;
33
+ continue;
34
+ }
35
+ if (char === "`" || (char === "$" && next === "(")) {
36
+ return { words, reason: "command substitution is not allowed in restricted bash commands" };
37
+ }
38
+ if (char === "$") {
39
+ return { words, reason: "shell expansion character '$' is not allowed in restricted bash commands" };
40
+ }
41
+ if (char === "\\") {
42
+ return { words, reason: "backslash escapes are not allowed in restricted bash commands" };
43
+ }
44
+ current += char;
45
+ continue;
46
+ }
47
+
48
+ if (char === "'") {
49
+ quote = "single";
50
+ continue;
51
+ }
52
+ if (char === '"') {
53
+ quote = "double";
54
+ continue;
55
+ }
56
+ if (char === "`" || (char === "$" && next === "(")) {
57
+ return { words, reason: "command substitution is not allowed in restricted bash commands" };
58
+ }
59
+ if (char === "\n" || char === "\r") {
60
+ return { words, reason: "multiple shell commands are not allowed in restricted bash mode" };
61
+ }
62
+ if (SHELL_CONTROL_CHARS.has(char)) {
63
+ return { words, reason: `shell control operator '${char}' is not allowed in restricted bash commands` };
64
+ }
65
+ if (UNSAFE_UNQUOTED_EXPANSION_CHARS.has(char)) {
66
+ return { words, reason: `shell expansion character '${char}' is not allowed in restricted bash commands` };
67
+ }
68
+ if (/\s/u.test(char)) {
69
+ if (current.length > 0) {
70
+ words.push(current);
71
+ current = "";
72
+ }
73
+ continue;
74
+ }
75
+ if (char === "\\") {
76
+ return { words, reason: "backslash escapes are not allowed in restricted bash commands" };
77
+ }
78
+ current += char;
79
+ }
80
+
81
+ if (quote !== null) {
82
+ return { words, reason: "unterminated quote in restricted bash command" };
83
+ }
84
+ if (current.length > 0) words.push(current);
85
+ return { words };
86
+ }
87
+
88
+ function prefixWords(prefix: string): string[] {
89
+ return prefix.trim().split(/\s+/u).filter(Boolean);
90
+ }
91
+
92
+ function wordsStartWith(words: readonly string[], prefix: readonly string[]): boolean {
93
+ if (prefix.length === 0 || words.length < prefix.length) return false;
94
+ return prefix.every((word, index) => words[index] === word);
95
+ }
96
+
97
+ function parseStateAction(words: readonly string[]): string | undefined {
98
+ const args = words.slice(2);
99
+ const positional: string[] = [];
100
+ let skipNext = false;
101
+ for (const arg of args) {
102
+ if (skipNext) {
103
+ skipNext = false;
104
+ continue;
105
+ }
106
+ if (STATE_FLAGS_WITH_VALUES.has(arg)) {
107
+ skipNext = true;
108
+ continue;
109
+ }
110
+ if (!arg.startsWith("-")) positional.push(arg);
111
+ }
112
+
113
+ const [first, second, third] = positional;
114
+ if (!first) return "read";
115
+ if (STATE_ACTIONS.has(first)) return second ? undefined : first;
116
+ if (!second) return "read";
117
+ if (!STATE_ACTIONS.has(second)) return undefined;
118
+ return third ? undefined : second;
119
+ }
120
+
121
+ function validateMatchedGjcCommand(words: readonly string[]): BashAllowedPrefixesCheck {
122
+ if (words[0] !== "gjc") return { allowed: true };
123
+
124
+ if (words[1] === "ralplan") {
125
+ if (!words.includes("--write")) {
126
+ return { allowed: false, reason: "restricted role-agent bash only allows `gjc ralplan --write ...`" };
127
+ }
128
+ return { allowed: true };
129
+ }
130
+
131
+ if (words[1] === "state") {
132
+ const action = parseStateAction(words);
133
+ if (!action) {
134
+ return {
135
+ allowed: false,
136
+ reason: "restricted role-agent bash only allows documented `gjc state` action shapes",
137
+ };
138
+ }
139
+ if (!ALLOWED_STATE_ACTIONS.has(action)) {
140
+ return { allowed: false, reason: `restricted role-agent bash does not allow \`gjc state ${action}\`` };
141
+ }
142
+ return { allowed: true };
143
+ }
144
+
145
+ return { allowed: true };
146
+ }
147
+
148
+ export function checkBashAllowedPrefixes(
149
+ command: string,
150
+ allowedPrefixes: readonly string[] | undefined,
151
+ ): BashAllowedPrefixesCheck {
152
+ const normalizedPrefixes = allowedPrefixes?.map(prefix => prefix.trim()).filter(Boolean) ?? [];
153
+ if (normalizedPrefixes.length === 0) return { allowed: true };
154
+
155
+ const parsed = parseShellWords(command.trim());
156
+ if (parsed.reason) return { allowed: false, reason: parsed.reason };
157
+ if (parsed.words.length === 0)
158
+ return { allowed: false, reason: "empty command is not allowed in restricted bash mode" };
159
+
160
+ const matched = normalizedPrefixes.some(prefix => wordsStartWith(parsed.words, prefixWords(prefix)));
161
+ if (!matched) {
162
+ return {
163
+ allowed: false,
164
+ reason: `restricted role-agent bash only allows commands starting with: ${normalizedPrefixes.join(", ")}`,
165
+ };
166
+ }
167
+
168
+ return validateMatchedGjcCommand(parsed.words);
169
+ }
package/src/tools/bash.ts CHANGED
@@ -8,6 +8,7 @@ import { AsyncJobManager } from "../async";
8
8
  import { type BashResult, executeBash } from "../exec/bash-executor";
9
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
10
  import { buildGjcRuntimeSessionEnv } from "../gjc-runtime/goal-mode-request";
11
+ import { GJC_RESTRICTED_ROLE_AGENT_BASH_ENV } from "../gjc-runtime/restricted-role-agent-bash";
11
12
  import { InternalUrlRouter } from "../internal-urls";
12
13
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
13
14
  import { highlightCode, type Theme } from "../modes/theme/theme";
@@ -18,6 +19,7 @@ import { renderStatusLine } from "../tui";
18
19
  import { CachedOutputBlock } from "../tui/output-block";
19
20
  import { getSixelLineMask } from "../utils/sixel";
20
21
  import type { ToolSession } from ".";
22
+ import { checkBashAllowedPrefixes } from "./bash-allowed-prefixes";
21
23
  import { applyBashFixups } from "./bash-command-fixup";
22
24
  import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-interactive";
23
25
  import { checkBashInterception } from "./bash-interceptor";
@@ -253,6 +255,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
253
255
  hasAstEdit: this.session.settings.get("astEdit.enabled"),
254
256
  hasSearch: this.session.settings.get("search.enabled"),
255
257
  hasFind: this.session.settings.get("find.enabled"),
258
+ restrictedAllowedPrefixes: this.session.bashAllowedPrefixes,
256
259
  });
257
260
  }
258
261
 
@@ -377,6 +380,13 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
377
380
  latestText = tailBuffer.text();
378
381
  void reportProgress(latestText, { async: { state: "running", jobId, type: "bash" } });
379
382
  },
383
+ onRawChunk: chunk => {
384
+ // Forward the unthrottled sanitized chunk to the async-job
385
+ // substrate so the Monitor tool can read the complete process
386
+ // stream by byte offset, independent of the throttled preview
387
+ // path above.
388
+ manager.appendOutput(jobId, chunk);
389
+ },
380
390
  onMinimizedSave: originalText => saveBashOriginalArtifact(this.session, originalText),
381
391
  });
382
392
  const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
@@ -454,27 +464,29 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
454
464
  return Math.max(0, Math.min(this.#autoBackgroundThresholdMs, timeoutMs - timeoutBufferMs));
455
465
  }
456
466
 
457
- async execute(
458
- _toolCallId: string,
459
- {
460
- command: rawCommand,
461
- env: rawEnv,
462
- timeout: rawTimeout = 300,
463
- cwd,
464
-
465
- async: asyncRequested = false,
466
- pty = false,
467
- }: BashToolInput,
468
- signal?: AbortSignal,
469
- onUpdate?: AgentToolUpdateCallback<BashToolDetails>,
467
+ /**
468
+ * Build the fully-prepared parameters for a `bash`-flavored execution
469
+ * (interceptors, internal URL expansion, env resolution, cwd validation,
470
+ * timeout clamp). Used by both `execute()` and the public `startMonitorJob`
471
+ * helper after `AgentSession` has applied the public-tool permission gate, so
472
+ * Monitor inherits Bash's cwd / env / artifact / interceptor pipeline 1:1.
473
+ */
474
+ async #prepareBashExecution(
475
+ input: { command: string; env?: Record<string, string>; timeout?: number; cwd?: string },
470
476
  ctx?: AgentToolContext,
471
- ): Promise<AgentToolResult<BashToolDetails>> {
472
- let command = rawCommand;
473
- const env = normalizeBashEnv(rawEnv);
477
+ ): Promise<{
478
+ command: string;
479
+ commandCwd: string;
480
+ resolvedEnv: Record<string, string>;
481
+ requestedTimeoutSec: number;
482
+ timeoutSec: number;
483
+ timeoutMs: number;
484
+ notices: string[];
485
+ }> {
486
+ let command = input.command;
487
+ let cwd = input.cwd;
488
+ const env = normalizeBashEnv(input.env);
474
489
 
475
- // Apply conservative bash fixups (strip trailing `| head|tail` and redundant
476
- // `2>&1`). The helper is single-line only and refuses anything that could
477
- // change semantics.
478
490
  if (this.session.settings.get("bash.stripTrailingHeadTail")) {
479
491
  const fixup = applyBashFixups(command);
480
492
  if (fixup.stripped.length > 0) {
@@ -482,9 +494,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
482
494
  }
483
495
  }
484
496
 
485
- // Extract leading `cd <path> && ...` into cwd when the model ignores the cwd parameter.
486
- // Constrained to a single line so a `&&` that sits on a later line of a multiline
487
- // script can't pull the entire script into the "cwd" capture.
488
497
  if (!cwd) {
489
498
  const cdMatch = command.match(/^cd[ \t]+((?:[^&\\\n\r]|\\.)+?)[ \t]*&&[ \t]*/);
490
499
  if (cdMatch) {
@@ -492,8 +501,20 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
492
501
  command = command.slice(cdMatch[0].length);
493
502
  }
494
503
  }
495
- if (asyncRequested && !this.#asyncEnabled) {
496
- throw new ToolError("Async bash execution is disabled. Enable async.enabled to use async mode.");
504
+
505
+ const rawCommand = input.command;
506
+ const allowedPrefixes = this.session.bashAllowedPrefixes;
507
+ if (allowedPrefixes && allowedPrefixes.length > 0) {
508
+ if (env && Object.keys(env).length > 0) {
509
+ throw new ToolError("Restricted role-agent bash does not allow per-command env overrides.");
510
+ }
511
+ const commandsToCheck = rawCommand === command ? [command] : [rawCommand, command];
512
+ for (const commandToCheck of commandsToCheck) {
513
+ const allowlist = checkBashAllowedPrefixes(commandToCheck, allowedPrefixes);
514
+ if (!allowlist.allowed) {
515
+ throw new ToolError(allowlist.reason ?? "Command blocked by restricted role-agent bash allowlist.");
516
+ }
517
+ }
497
518
  }
498
519
 
499
520
  // Check both the original command and the cwd-normalized command so
@@ -541,9 +562,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
541
562
  cwd: this.session.cwd,
542
563
  }),
543
564
  ...expandedEnv,
565
+ ...(allowedPrefixes && allowedPrefixes.length > 0 ? { [GJC_RESTRICTED_ROLE_AGENT_BASH_ENV]: "1" } : {}),
544
566
  };
545
567
 
546
- // Resolve protocol URLs (agent://, artifact://, etc.) in extracted cwd.
547
568
  if (cwd?.includes("://") || cwd?.includes("local:/")) {
548
569
  cwd = await expandInternalUrls(cwd, { ...internalUrlOptions, noEscape: true });
549
570
  }
@@ -562,13 +583,153 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
562
583
  throw new ToolError(`Working directory is not a directory: ${commandCwd}`);
563
584
  }
564
585
 
565
- // Clamp to reasonable range: 1s - 3600s (1 hour)
566
- const requestedTimeoutSec = rawTimeout;
586
+ const requestedTimeoutSec = input.timeout ?? 300;
567
587
  const timeoutSec = clampTimeout("bash", requestedTimeoutSec);
568
588
  const timeoutMs = timeoutSec * 1000;
569
- const pendingNotices: string[] = [];
589
+ const notices: string[] = [];
570
590
  const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec);
571
- if (timeoutClampNotice) pendingNotices.push(timeoutClampNotice);
591
+ if (timeoutClampNotice) notices.push(timeoutClampNotice);
592
+
593
+ return { command, commandCwd, resolvedEnv, requestedTimeoutSec, timeoutSec, timeoutMs, notices };
594
+ }
595
+
596
+ /**
597
+ * Start a background bash job for the Monitor tool. Reuses the full Bash
598
+ * pipeline (interceptors, internal-URL expansion, env, cwd, timeout); the
599
+ * public `monitor` tool itself is ACP-gated by `AgentSession` before this
600
+ * helper is called. The caller-supplied `onRawLine` callback is invoked once
601
+ * per newline-terminated stdout chunk, between turns, so the upstream Claude
602
+ * Code "Each stdout line is a task-notification event" semantics are preserved
603
+ * through the agent's existing background-task delivery path.
604
+ */
605
+ async startMonitorJob(
606
+ input: { command: string; cwd?: string; timeout?: number; env?: Record<string, string> },
607
+ opts: {
608
+ ownerId?: string;
609
+ label?: string;
610
+ ctx?: AgentToolContext;
611
+ onRawLine?: (line: string, jobId: string) => void;
612
+ } = {},
613
+ ): Promise<{ jobId: string; label: string; commandCwd: string }> {
614
+ const manager = AsyncJobManager.instance();
615
+ if (!manager) {
616
+ throw new ToolError("Async job manager unavailable for this session.");
617
+ }
618
+ const prepared = await this.#prepareBashExecution(input, opts.ctx);
619
+ const label =
620
+ opts.label ?? (prepared.command.length > 120 ? `${prepared.command.slice(0, 117)}...` : prepared.command);
621
+ const monitorTimeoutMs = input.timeout === undefined ? null : prepared.timeoutMs;
622
+ const onRawLine = opts.onRawLine;
623
+ let currentJobId = "";
624
+ let cursorOffset = 0;
625
+ let lineBuffer = "";
626
+ const dispatchLines = (chunk: string) => {
627
+ if (!onRawLine) return;
628
+ lineBuffer += chunk;
629
+ let newlineIndex = lineBuffer.indexOf("\n");
630
+ while (newlineIndex !== -1) {
631
+ const line = lineBuffer.slice(0, newlineIndex);
632
+ lineBuffer = lineBuffer.slice(newlineIndex + 1);
633
+ try {
634
+ onRawLine(line, currentJobId);
635
+ } catch (error) {
636
+ logger.warn("Monitor onRawLine callback failed", {
637
+ error: error instanceof Error ? error.message : String(error),
638
+ });
639
+ }
640
+ newlineIndex = lineBuffer.indexOf("\n");
641
+ }
642
+ };
643
+ const flushTrailingLine = () => {
644
+ if (!onRawLine) return;
645
+ if (lineBuffer.length === 0) return;
646
+ const remainder = lineBuffer;
647
+ lineBuffer = "";
648
+ try {
649
+ onRawLine(remainder, currentJobId);
650
+ } catch (error) {
651
+ logger.warn("Monitor onRawLine callback failed (trailing)", {
652
+ error: error instanceof Error ? error.message : String(error),
653
+ });
654
+ }
655
+ };
656
+
657
+ const ownerId = opts.ownerId ?? this.session.getAgentId?.() ?? undefined;
658
+ const jobId = manager.register(
659
+ "bash",
660
+ label,
661
+ async ({ jobId: id, signal, reportProgress }) => {
662
+ const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
663
+ const tailBuffer = new TailBuffer(DEFAULT_MAX_BYTES);
664
+ try {
665
+ const result = await executeBash(prepared.command, {
666
+ cwd: prepared.commandCwd,
667
+ sessionKey: `${this.session.getSessionId?.() ?? ""}:monitor:${id}`,
668
+ timeout: monitorTimeoutMs,
669
+ signal,
670
+ env: prepared.resolvedEnv,
671
+ artifactPath,
672
+ artifactId,
673
+ onChunk: chunk => {
674
+ tailBuffer.append(chunk);
675
+ void reportProgress(tailBuffer.text(), {
676
+ async: { state: "running", jobId: id, type: "bash" },
677
+ });
678
+ },
679
+ onRawChunk: chunk => {
680
+ manager.appendOutput(id, chunk);
681
+ const slice = manager.readOutputSince(id, cursorOffset, ownerId ? { ownerId } : undefined);
682
+ if (!slice) return;
683
+ cursorOffset = slice.nextOffset;
684
+ dispatchLines(slice.text);
685
+ },
686
+ onMinimizedSave: originalText => saveBashOriginalArtifact(this.session, originalText),
687
+ });
688
+ flushTrailingLine();
689
+ this.#buildResultText(result, prepared.timeoutSec, result.output || "(no output)");
690
+ return result.output;
691
+ } catch (error) {
692
+ flushTrailingLine();
693
+ throw error instanceof Error ? error : new Error(String(error));
694
+ }
695
+ },
696
+ { ownerId },
697
+ );
698
+ currentJobId = jobId;
699
+ return { jobId, label, commandCwd: prepared.commandCwd };
700
+ }
701
+
702
+ async execute(
703
+ _toolCallId: string,
704
+ {
705
+ command: rawCommand,
706
+ env: rawEnv,
707
+ timeout: rawTimeout = 300,
708
+ cwd,
709
+
710
+ async: asyncRequested = false,
711
+ pty = false,
712
+ }: BashToolInput,
713
+ signal?: AbortSignal,
714
+ onUpdate?: AgentToolUpdateCallback<BashToolDetails>,
715
+ ctx?: AgentToolContext,
716
+ ): Promise<AgentToolResult<BashToolDetails>> {
717
+ if (asyncRequested && !this.#asyncEnabled) {
718
+ throw new ToolError("Async bash execution is disabled. Enable async.enabled to use async mode.");
719
+ }
720
+ const prepared = await this.#prepareBashExecution(
721
+ { command: rawCommand, env: rawEnv, timeout: rawTimeout, cwd },
722
+ ctx,
723
+ );
724
+ const {
725
+ command,
726
+ commandCwd,
727
+ resolvedEnv,
728
+ requestedTimeoutSec,
729
+ timeoutSec,
730
+ timeoutMs,
731
+ notices: pendingNotices,
732
+ } = prepared;
572
733
 
573
734
  if (asyncRequested) {
574
735
  if (!AsyncJobManager.instance()) {
@@ -916,7 +916,7 @@ export class WorkerCore {
916
916
  dispatchEvent: (event: unknown) => boolean;
917
917
  }
918
918
  const select = el as unknown as SelectLike;
919
- if (!select || select.tagName !== "SELECT") throw new Error("tab.select() requires a <select> element");
919
+ if (select?.tagName !== "SELECT") throw new Error("tab.select() requires a <select> element");
920
920
  const EventCtor = (
921
921
  globalThis as unknown as { Event: new (type: string, init?: { bubbles: boolean }) => unknown }
922
922
  ).Event;