@gajae-code/coding-agent 0.18.12 → 0.19.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.
- package/CHANGELOG.md +467 -0
- package/dist/types/capability/rule.d.ts +7 -2
- package/dist/types/cli/append-system-prompt.d.ts +6 -0
- package/dist/types/cli/args.d.ts +7 -2
- package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
- package/dist/types/cli/customize-doctor.d.ts +4 -3
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/root-flags.d.ts +0 -12
- package/dist/types/cli/update-cli.d.ts +7 -2
- package/dist/types/cli-main.d.ts +0 -6
- package/dist/types/commands/launch.d.ts +0 -6
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +8 -0
- package/dist/types/commit/map-reduce/index.d.ts +4 -4
- package/dist/types/config/autorouting-tier-map.d.ts +23 -1
- package/dist/types/config/config-hot-reload.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +44 -2
- package/dist/types/config/model-profile-activation.d.ts +19 -5
- package/dist/types/config/model-registry.d.ts +16 -3
- package/dist/types/config/model-resolver.d.ts +5 -4
- package/dist/types/config/settings-schema.d.ts +11 -1
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/customization/import.d.ts +2 -0
- package/dist/types/customization/types.d.ts +1 -1
- package/dist/types/dap/session.d.ts +11 -2
- package/dist/types/discovery/helpers.d.ts +7 -2
- package/dist/types/edit/index.d.ts +5 -0
- package/dist/types/edit/modes/patch.d.ts +9 -0
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/export/ttsr.d.ts +19 -0
- package/dist/types/extensibility/extensions/types.d.ts +5 -1
- package/dist/types/extensibility/slash-commands.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/content.d.ts +5 -6
- package/dist/types/internal-urls/types.d.ts +2 -0
- package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
- package/dist/types/main.d.ts +23 -18
- package/dist/types/migrate/action-planner.d.ts +10 -1
- package/dist/types/migrate/adapters/codex.d.ts +1 -1
- package/dist/types/migrate/adapters/index.d.ts +35 -3
- package/dist/types/migrate/hook-module.d.ts +23 -0
- package/dist/types/migrate/report.d.ts +2 -1
- package/dist/types/migrate/types.d.ts +69 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +43 -0
- package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +15 -11
- package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
- package/dist/types/sdk/broker/broker-exit.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +22 -1
- package/dist/types/sdk/broker/ensure.d.ts +5 -2
- package/dist/types/sdk/broker/hop.d.ts +9 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
- package/dist/types/sdk/broker/session-index.d.ts +2 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/index.d.ts +21 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
- package/dist/types/sdk/host/control/operations.d.ts +5 -1
- package/dist/types/sdk/host/events.d.ts +4 -0
- package/dist/types/sdk/host/host.d.ts +2 -1
- package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
- package/dist/types/sdk/host/session-runtime.d.ts +8 -8
- package/dist/types/sdk/host/surface-policy.d.ts +2 -1
- package/dist/types/sdk/lifecycle/service.d.ts +12 -0
- package/dist/types/sdk/prompt-status.d.ts +11 -4
- package/dist/types/session/agent-session.d.ts +50 -8
- package/dist/types/session/session-manager.d.ts +16 -0
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +13 -1
- package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
- package/dist/types/ssh/ssh-executor.d.ts +8 -1
- package/dist/types/system-prompt.d.ts +3 -2
- package/dist/types/task/role-body.d.ts +11 -0
- package/dist/types/tools/awaiting-input.d.ts +28 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +7 -0
- package/dist/types/tools/descriptors.d.ts +13 -2
- package/dist/types/tools/fetch.d.ts +6 -0
- package/dist/types/tools/irc.d.ts +5 -3
- package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +5 -0
- package/dist/types/utils/image-limits.d.ts +7 -0
- package/dist/types/utils/image-loading.d.ts +0 -1
- package/dist/types/utils/open.d.ts +0 -1
- package/dist/types/utils/pasted-image-loading.d.ts +1 -5
- package/dist/types/utils/prompt-suggestion.d.ts +2 -2
- package/dist/types/utils/shell-snapshot.d.ts +2 -0
- package/dist/types/utils/title-generator.d.ts +13 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -7
- package/package.json +9 -8
- package/scripts/acp-conformance-agent.ts +12 -2
- package/scripts/generate-sdk-operation-inventory.ts +16 -0
- package/src/capability/fs.ts +31 -3
- package/src/capability/rule.ts +11 -2
- package/src/cli/append-system-prompt.ts +12 -0
- package/src/cli/args.ts +11 -7
- package/src/cli/customize-doctor-precedence.ts +31 -0
- package/src/cli/customize-doctor.ts +49 -39
- package/src/cli/fast-help.ts +0 -2
- package/src/cli/migrate-cli.ts +14 -3
- package/src/cli/public-command-errors.ts +2 -0
- package/src/cli/root-flags.ts +0 -2
- package/src/cli/update-cli.ts +247 -6
- package/src/commands/harness.ts +25 -15
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +81 -11
- package/src/commit/agentic/agent.ts +6 -6
- package/src/commit/analysis/conventional.ts +14 -5
- package/src/commit/map-reduce/index.ts +7 -7
- package/src/commit/pipeline.ts +12 -12
- package/src/config/autorouting-tier-map.ts +20 -1
- package/src/config/config-file.ts +106 -3
- package/src/config/config-hot-reload.ts +8 -0
- package/src/config/file-lock.ts +1431 -304
- package/src/config/keybindings.ts +154 -7
- package/src/config/model-profile-activation.ts +177 -39
- package/src/config/model-registry.ts +380 -55
- package/src/config/model-resolver.ts +49 -21
- package/src/config/settings-schema.ts +13 -1
- package/src/config/settings.ts +2 -0
- package/src/coordinator-mcp/server.ts +37 -2
- package/src/crash/sanitize.ts +3 -1
- package/src/customization/import.ts +276 -7
- package/src/customization/inventory.ts +24 -8
- package/src/customization/types.ts +1 -0
- package/src/dap/client.ts +3 -2
- package/src/dap/session.ts +161 -36
- package/src/debug/crash-diagnostics.ts +87 -5
- package/src/debug/report-bundle.ts +71 -2
- package/src/discovery/builtin.ts +4 -3
- package/src/discovery/helpers.ts +24 -14
- package/src/edit/index.ts +191 -5
- package/src/edit/modes/patch.ts +27 -0
- package/src/eval/py/runtime.ts +44 -4
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/html/index.ts +60 -20
- package/src/export/ttsr.ts +176 -1
- package/src/extensibility/extensions/loader.ts +2 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
- package/src/extensibility/extensions/types.ts +5 -1
- package/src/extensibility/hooks/loader.ts +10 -2
- package/src/extensibility/plugins/marketplace/manager.ts +36 -10
- package/src/extensibility/slash-commands.ts +5 -0
- package/src/gjc-runtime/gc-runtime.ts +4 -1
- package/src/gjc-runtime/launch-tmux.ts +23 -13
- package/src/gjc-runtime/ralplan-current-final.ts +294 -0
- package/src/gjc-runtime/ralplan-runtime.ts +95 -1
- package/src/gjc-runtime/state-writer.ts +109 -5
- package/src/gjc-runtime/tmux-sessions.ts +93 -13
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
- package/src/gjc-runtime/ultragoal-guard.ts +9 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
- package/src/harness-control-plane/finalize.ts +11 -0
- package/src/harness-control-plane/receipt-spool.ts +105 -28
- package/src/harness-control-plane/storage.ts +124 -6
- package/src/hindsight/content.ts +116 -16
- package/src/hindsight/mental-models.ts +22 -10
- package/src/hindsight/state.ts +7 -4
- package/src/hooks/native-skill-hook.ts +3 -1
- package/src/internal-urls/artifact-protocol.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +14 -14
- package/src/internal-urls/local-protocol.ts +28 -3
- package/src/internal-urls/types.ts +2 -0
- package/src/lsp/client.ts +2 -1
- package/src/lsp/clients/biome-client.ts +2 -0
- package/src/lsp/clients/swiftlint-client.ts +2 -0
- package/src/main.ts +330 -92
- package/src/memories/index.ts +25 -2
- package/src/migrate/action-planner.ts +517 -2
- package/src/migrate/adapters/claude-code.ts +141 -3
- package/src/migrate/adapters/codex.ts +57 -5
- package/src/migrate/adapters/index.ts +240 -5
- package/src/migrate/adapters/opencode.ts +4 -0
- package/src/migrate/executor.ts +141 -3
- package/src/migrate/hook-module.ts +193 -0
- package/src/migrate/report.ts +45 -1
- package/src/migrate/types.ts +73 -3
- package/src/modes/acp/acp-agent.ts +10 -1
- package/src/modes/components/agent-dashboard.ts +20 -4
- package/src/modes/components/customization/customization-dashboard.ts +7 -0
- package/src/modes/components/customization/import-wizard.ts +29 -3
- package/src/modes/components/model-selector.ts +157 -103
- package/src/modes/components/welcome.ts +3 -1
- package/src/modes/controllers/command-controller.ts +271 -16
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +31 -1
- package/src/modes/controllers/goal-mode-controller.ts +79 -10
- package/src/modes/controllers/input-controller.ts +2 -0
- package/src/modes/controllers/plan-mode-controller.ts +49 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
- package/src/modes/controllers/selector-controller.ts +14 -1
- package/src/modes/interactive-mode.ts +55 -4
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +5 -1
- package/src/modes/utils/host-status.ts +21 -10
- package/src/prompts/agent-fragments/browser-aside.md +2 -2
- package/src/prompts/system/irc-incoming.md +3 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/runtime-mcp/manager.ts +8 -3
- package/src/runtime-mcp/project-host-values.ts +12 -8
- package/src/runtime-mcp/tool-bridge.ts +68 -4
- package/src/runtime-mcp/transports/stdio.ts +2 -1
- package/src/sdk/broker/broker-exit.ts +2 -0
- package/src/sdk/broker/broker.ts +143 -4
- package/src/sdk/broker/ensure.ts +74 -6
- package/src/sdk/broker/hop.ts +13 -1
- package/src/sdk/broker/lifecycle-ledger.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +102 -6
- package/src/sdk/broker/session-index.ts +29 -4
- package/src/sdk/broker/transport.ts +17 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +408 -14
- package/src/sdk/bus/session-control.ts +2 -0
- package/src/sdk/bus/slack-daemon.ts +105 -9
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -2
- package/src/sdk/bus/threaded-render.ts +20 -1
- package/src/sdk/cli/session-cli.ts +2 -0
- package/src/sdk/host/control/dispatch.ts +13 -1
- package/src/sdk/host/control/operations.ts +10 -1
- package/src/sdk/host/events.ts +67 -4
- package/src/sdk/host/host.ts +8 -2
- package/src/sdk/host/prompt-image-upload.ts +438 -0
- package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
- package/src/sdk/host/session-runtime.test.ts +1537 -24
- package/src/sdk/host/session-runtime.ts +332 -36
- package/src/sdk/host/surface-policy.ts +4 -0
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
- package/src/sdk/host/websocket-transport.ts +19 -7
- package/src/sdk/lifecycle/service.ts +14 -1
- package/src/sdk/prompt-status.ts +6 -4
- package/src/sdk/protocol/adapter-validation.ts +28 -1
- package/src/sdk/protocol/operation-inventory.generated.json +160 -0
- package/src/sdk/protocol/operation-registry.ts +33 -3
- package/src/sdk/session.ts +123 -36
- package/src/session/agent-session.ts +1614 -329
- package/src/session/history-storage.ts +182 -12
- package/src/session/internal/managed-session-scope.ts +47 -18
- package/src/session/internal/managed-session-storage.ts +71 -1
- package/src/session/session-manager.ts +90 -5
- package/src/session-import/redact.ts +53 -2
- package/src/skill-state/workflow-mutation-guard.ts +1498 -18
- package/src/slash-commands/builtin-registry.ts +6 -3
- package/src/slash-commands/helpers/mcp.ts +11 -3
- package/src/ssh/ssh-executor.ts +9 -2
- package/src/stt/recorder.ts +26 -9
- package/src/system-prompt.ts +61 -16
- package/src/task/executor.ts +12 -8
- package/src/task/role-body.ts +84 -0
- package/src/task/token-log-mode.test.ts +162 -0
- package/src/task/token-log.ts +91 -2
- package/src/tools/archive-reader.ts +650 -37
- package/src/tools/ask.ts +1 -0
- package/src/tools/ast-grep.ts +7 -2
- package/src/tools/auto-generated-guard.ts +12 -3
- package/src/tools/awaiting-input.ts +114 -0
- package/src/tools/bash-allowed-prefixes.ts +61 -1
- package/src/tools/bash-skill-urls.ts +58 -5
- package/src/tools/bash.ts +99 -26
- package/src/tools/browser/tab-worker.ts +39 -13
- package/src/tools/browser.ts +17 -7
- package/src/tools/cron.ts +10 -1
- package/src/tools/debug.ts +9 -2
- package/src/tools/descriptors.test.ts +4 -4
- package/src/tools/descriptors.ts +16 -4
- package/src/tools/eval.ts +15 -0
- package/src/tools/fetch.ts +90 -2
- package/src/tools/gh.ts +27 -12
- package/src/tools/image-gen.ts +20 -6
- package/src/tools/irc.ts +5 -3
- package/src/tools/monitor.ts +9 -1
- package/src/tools/output-meta.ts +51 -3
- package/src/tools/read.ts +9 -2
- package/src/tools/recipe/runner.ts +23 -12
- package/src/tools/render-utils.ts +2 -2
- package/src/tools/search.ts +3 -2
- package/src/tools/ssh.ts +274 -8
- package/src/tools/subagent.ts +10 -4
- package/src/tools/tmux-self-injection-guard.ts +3301 -122
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/ultragoal-ask-guard.ts +13 -1
- package/src/tools/write.ts +7 -0
- package/src/tui/tree-list.ts +15 -1
- package/src/utils/commit-message-generator.ts +3 -2
- package/src/utils/external-editor.ts +21 -7
- package/src/utils/file-mentions.ts +46 -6
- package/src/utils/git.ts +38 -7
- package/src/utils/image-limits.ts +8 -0
- package/src/utils/image-loading.ts +1 -1
- package/src/utils/open.ts +73 -4
- package/src/utils/pasted-image-loading.ts +9 -8
- package/src/utils/prompt-suggestion.ts +5 -4
- package/src/utils/shell-snapshot.ts +9 -1
- package/src/utils/title-generator.ts +65 -20
- package/src/web/search/index.ts +2 -1
- package/src/web/search/providers/codex.ts +29 -32
package/src/tools/ask.ts
CHANGED
|
@@ -953,6 +953,7 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
953
953
|
this.session.cwd,
|
|
954
954
|
{ sessionId: this.session.getSessionId?.() ?? null },
|
|
955
955
|
this.session.getSessionAgentDir?.() ?? this.session.settings.getAgentDir(),
|
|
956
|
+
params,
|
|
956
957
|
);
|
|
957
958
|
assertDeepInterviewStructuredResponseWithinLimit(params);
|
|
958
959
|
// Recheck at execution time as well: direct SDK callers and a phase change
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -11,6 +11,7 @@ import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text
|
|
|
11
11
|
import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
12
12
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
13
13
|
import type { ToolSession } from ".";
|
|
14
|
+
import { wrapUntrustedContent } from "./fetch";
|
|
14
15
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
15
16
|
import { formatGroupedFiles } from "./grouped-file-output";
|
|
16
17
|
import { formatMatchLine } from "./match-line-format";
|
|
@@ -221,7 +222,9 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
221
222
|
const parseMessage = cappedParseErrors.length
|
|
222
223
|
? `\n${formatParseErrors(cappedParseErrors, parseErrorsTotal).join("\n")}`
|
|
223
224
|
: "";
|
|
224
|
-
return toolResult(baseDetails)
|
|
225
|
+
return toolResult(baseDetails)
|
|
226
|
+
.text(wrapUntrustedContent(`${noMatchMessage}${parseMessage}`))
|
|
227
|
+
.done();
|
|
225
228
|
}
|
|
226
229
|
|
|
227
230
|
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
|
@@ -291,7 +294,9 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
291
294
|
outputLines.push("", ...formatParseErrors(cappedParseErrors, parseErrorsTotal));
|
|
292
295
|
}
|
|
293
296
|
|
|
294
|
-
return toolResult(details)
|
|
297
|
+
return toolResult(details)
|
|
298
|
+
.text(wrapUntrustedContent(outputLines.join("\n")))
|
|
299
|
+
.done();
|
|
295
300
|
});
|
|
296
301
|
}
|
|
297
302
|
}
|
|
@@ -212,8 +212,14 @@ function extractLeadingHeaderCommentText(content: string, commentStyles: readonl
|
|
|
212
212
|
* Check if a file name indicates it might be auto-generated.
|
|
213
213
|
* This is a quick pre-check before reading file content.
|
|
214
214
|
*/
|
|
215
|
+
function guardFileName(filePath: string): string {
|
|
216
|
+
// resolvePlanPath keeps the platform separator. Anchored patterns such as
|
|
217
|
+
// zz_generated.* and generated.ts must see that basename on Windows too.
|
|
218
|
+
return path.win32.basename(filePath);
|
|
219
|
+
}
|
|
220
|
+
|
|
215
221
|
function isAutoGeneratedFileName(filePath: string): boolean {
|
|
216
|
-
const fileName = filePath
|
|
222
|
+
const fileName = guardFileName(filePath);
|
|
217
223
|
return AUTO_GENERATED_FILENAME_PATTERNS.some(pattern => pattern.test(fileName));
|
|
218
224
|
}
|
|
219
225
|
|
|
@@ -254,11 +260,14 @@ const autoGeneratedMap = new LRUCache<string, { marker: string | undefined }>({
|
|
|
254
260
|
|
|
255
261
|
async function getAutoGeneratedMarker(filePath: string): Promise<string | undefined> {
|
|
256
262
|
if (isAutoGeneratedFileName(filePath)) {
|
|
257
|
-
return filePath
|
|
263
|
+
return guardFileName(filePath);
|
|
258
264
|
}
|
|
259
265
|
|
|
260
266
|
const cached = autoGeneratedMap.get(filePath);
|
|
261
|
-
|
|
267
|
+
// A cached miss only means the file was handwritten at the last read.
|
|
268
|
+
// An earlier patch entry can write a generated header before a later
|
|
269
|
+
// rename checks the same destination, so do not trust that miss.
|
|
270
|
+
if (cached && cached.marker !== undefined) return cached.marker;
|
|
262
271
|
|
|
263
272
|
let marker: string | undefined;
|
|
264
273
|
try {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects a running command that printed an interactive auth prompt and is now
|
|
3
|
+
* waiting for a human (browser sign-in URL or device code).
|
|
4
|
+
*
|
|
5
|
+
* The agent cannot see this on its own: the tool call is simply still running.
|
|
6
|
+
* The detector watches the streamed output tail and fires once per command when
|
|
7
|
+
* all of these hold:
|
|
8
|
+
* - a known auth-prompt signature is present in the recent output tail;
|
|
9
|
+
* - the command is still running (the caller stops feeding it on exit);
|
|
10
|
+
* - no new output arrived for `quietMs` after the match.
|
|
11
|
+
*
|
|
12
|
+
* Requiring the quiet window keeps `cat README.md` or `grep` over docs that
|
|
13
|
+
* merely mention "enter the code" from firing: those commands exit or keep
|
|
14
|
+
* printing. The signal carries only the prompt kind, never the URL or code,
|
|
15
|
+
* because both are short-lived credential-equivalent values.
|
|
16
|
+
*/
|
|
17
|
+
import { stripVTControlCharacters } from "node:util";
|
|
18
|
+
|
|
19
|
+
export type AwaitingInputKind = "browser_auth" | "device_code";
|
|
20
|
+
|
|
21
|
+
export interface AwaitingInputSignal {
|
|
22
|
+
kind: AwaitingInputKind;
|
|
23
|
+
/** Epoch milliseconds when the quiet window elapsed after the match. */
|
|
24
|
+
detectedAt: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Cancels a scheduled callback. */
|
|
28
|
+
export type CancelScheduled = () => void;
|
|
29
|
+
|
|
30
|
+
export interface AwaitingInputDetectorOptions {
|
|
31
|
+
onDetect: (signal: AwaitingInputSignal) => void;
|
|
32
|
+
/** Quiet window after a match before the signal fires. */
|
|
33
|
+
quietMs?: number;
|
|
34
|
+
/** Injectable scheduler; defaults to `setTimeout`. */
|
|
35
|
+
schedule?: (callback: () => void, ms: number) => CancelScheduled;
|
|
36
|
+
/** Injectable clock; defaults to `Date.now`. */
|
|
37
|
+
now?: () => number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const AWAITING_INPUT_QUIET_MS = 3_000;
|
|
41
|
+
|
|
42
|
+
/** Only the most recent output can be the prompt the process is blocked on. */
|
|
43
|
+
const TAIL_CHARS = 2_048;
|
|
44
|
+
|
|
45
|
+
const URL_PATTERN = /\bhttps?:\/\/\S+/i;
|
|
46
|
+
|
|
47
|
+
const DEVICE_CODE_PATTERNS: readonly RegExp[] = [
|
|
48
|
+
/\bone-time code\b/i,
|
|
49
|
+
/\benter the code\b/i,
|
|
50
|
+
/\benter code\b/i,
|
|
51
|
+
/\buser_code=/i,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const BROWSER_AUTH_PATTERNS: readonly RegExp[] = [
|
|
55
|
+
/\bto authenticate,? visit\b/i,
|
|
56
|
+
/\blogin\.tailscale\.com\/a\//i,
|
|
57
|
+
/\bgo to the following link in your browser\b/i,
|
|
58
|
+
/\bopen the following url\b/i,
|
|
59
|
+
/\bvisit this url to authori[sz]e\b/i,
|
|
60
|
+
/\bopen (?:this|the) (?:url|link) in (?:a|your) browser\b/i,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/** Classify an output tail; `undefined` when it does not look like an auth prompt. */
|
|
64
|
+
export function classifyAwaitingInput(tail: string): AwaitingInputKind | undefined {
|
|
65
|
+
const text = stripVTControlCharacters(tail);
|
|
66
|
+
if (DEVICE_CODE_PATTERNS.some(pattern => pattern.test(text))) return "device_code";
|
|
67
|
+
if (URL_PATTERN.test(text) && BROWSER_AUTH_PATTERNS.some(pattern => pattern.test(text))) return "browser_auth";
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function defaultSchedule(callback: () => void, ms: number): CancelScheduled {
|
|
72
|
+
const timer = setTimeout(callback, ms);
|
|
73
|
+
return () => clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class AwaitingInputDetector {
|
|
77
|
+
readonly #onDetect: (signal: AwaitingInputSignal) => void;
|
|
78
|
+
readonly #quietMs: number;
|
|
79
|
+
readonly #schedule: (callback: () => void, ms: number) => CancelScheduled;
|
|
80
|
+
readonly #now: () => number;
|
|
81
|
+
#tail = "";
|
|
82
|
+
#cancel: CancelScheduled | undefined;
|
|
83
|
+
#done = false;
|
|
84
|
+
|
|
85
|
+
constructor(options: AwaitingInputDetectorOptions) {
|
|
86
|
+
this.#onDetect = options.onDetect;
|
|
87
|
+
this.#quietMs = options.quietMs ?? AWAITING_INPUT_QUIET_MS;
|
|
88
|
+
this.#schedule = options.schedule ?? defaultSchedule;
|
|
89
|
+
this.#now = options.now ?? Date.now;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Feed one streamed output chunk. Any new output restarts the quiet window. */
|
|
93
|
+
push(chunk: string): void {
|
|
94
|
+
if (this.#done || chunk.length === 0) return;
|
|
95
|
+
this.#cancel?.();
|
|
96
|
+
this.#cancel = undefined;
|
|
97
|
+
this.#tail = (this.#tail + chunk).slice(-TAIL_CHARS);
|
|
98
|
+
const kind = classifyAwaitingInput(this.#tail);
|
|
99
|
+
if (!kind) return;
|
|
100
|
+
this.#cancel = this.#schedule(() => {
|
|
101
|
+
this.#cancel = undefined;
|
|
102
|
+
if (this.#done) return;
|
|
103
|
+
this.#done = true;
|
|
104
|
+
this.#onDetect({ kind, detectedAt: this.#now() });
|
|
105
|
+
}, this.#quietMs);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Stop watching; the command exited or was folded. Never fires afterwards. */
|
|
109
|
+
dispose(): void {
|
|
110
|
+
this.#done = true;
|
|
111
|
+
this.#cancel?.();
|
|
112
|
+
this.#cancel = undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -280,7 +280,9 @@ function shellQuote(value: string): string {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
function resolvedExternalCommand(command: string): string | undefined {
|
|
283
|
-
|
|
283
|
+
// Bun.which caches a command name until PATH is passed explicitly, so a test
|
|
284
|
+
// or a later session that changes PATH must be visible here.
|
|
285
|
+
return Bun.which(command, { PATH: process.env.PATH ?? "" }) ?? undefined;
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
function validateReadOnlyCommand(words: readonly string[]): BashAllowedPrefixesCheck {
|
|
@@ -322,16 +324,50 @@ function validateReadOnlyCommand(words: readonly string[]): BashAllowedPrefixesC
|
|
|
322
324
|
return { allowed: true };
|
|
323
325
|
}
|
|
324
326
|
|
|
327
|
+
const GIT_DIFF_FAMILY_SUBCOMMANDS = new Set(["blame", "diff", "log", "show"]);
|
|
328
|
+
|
|
329
|
+
function gitOptionName(word: string): string {
|
|
330
|
+
const equalsAt = word.indexOf("=");
|
|
331
|
+
return equalsAt === -1 ? word : word.slice(0, equalsAt);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Enabling forms only. `--no-ext-diff` and `--no-textconv` stay allowed. */
|
|
335
|
+
function gitEnablesExternalHelper(word: string): boolean {
|
|
336
|
+
if (!word.startsWith("--")) return false;
|
|
337
|
+
const name = gitOptionName(word);
|
|
338
|
+
return name === "--ext-diff" || name === "--textconv";
|
|
339
|
+
}
|
|
340
|
+
|
|
325
341
|
function rejectGitHostAccess(words: readonly string[]): BashAllowedPrefixesCheck | undefined {
|
|
326
342
|
if (words[0] !== "git") return undefined;
|
|
327
343
|
for (const option of optionWords(words)) {
|
|
328
344
|
if (isLongOption(option, "--output") || isLongOption(option, "--no-index")) {
|
|
329
345
|
return { allowed: false, reason: "restricted bash does not allow git --output or --no-index" };
|
|
330
346
|
}
|
|
347
|
+
if (gitEnablesExternalHelper(option)) {
|
|
348
|
+
return {
|
|
349
|
+
allowed: false,
|
|
350
|
+
reason: "restricted bash does not allow git external diff or textconv helpers",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
331
353
|
}
|
|
332
354
|
return undefined;
|
|
333
355
|
}
|
|
334
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Diff-family git commands run `diff.external`, `GIT_EXTERNAL_DIFF`, and textconv
|
|
359
|
+
* filters unless these flags are present. Place them immediately after the
|
|
360
|
+
* subcommand so a later value-taking option cannot consume them and they stay
|
|
361
|
+
* before a `--` pathspec. The gate rejects a later `--ext-diff` or `--textconv`,
|
|
362
|
+
* which would otherwise win.
|
|
363
|
+
*/
|
|
364
|
+
function disableGitExternalHelpers(commandName: string, argv: readonly string[]): string[] {
|
|
365
|
+
if (commandName !== "git" || !GIT_DIFF_FAMILY_SUBCOMMANDS.has(argv[1] ?? "")) return [...argv];
|
|
366
|
+
const next = [...argv];
|
|
367
|
+
next.splice(2, 0, "--no-ext-diff", "--no-textconv");
|
|
368
|
+
return next;
|
|
369
|
+
}
|
|
370
|
+
|
|
335
371
|
const GJC_SESSION_BIND_REASON = "restricted role-agent bash binds gjc to the current session";
|
|
336
372
|
const GJC_WORKTREE_BIND_REASON = "restricted role-agent bash binds gjc --worktree-root to the current repository";
|
|
337
373
|
|
|
@@ -472,6 +508,30 @@ function commandAllowedPrefixesReason(normalizedPrefixes: readonly string[], opt
|
|
|
472
508
|
: `restricted role-agent bash only allows commands starting with: ${prefixList}`;
|
|
473
509
|
}
|
|
474
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Workflow restricted bash keeps the lexical prefix check, then runs the resolved
|
|
513
|
+
* executable. Callers also skip the shell snapshot and shell prefix, because a
|
|
514
|
+
* snapshot function can be named for that absolute path.
|
|
515
|
+
*/
|
|
516
|
+
export function normalizeWorkflowBashCommand(
|
|
517
|
+
command: string,
|
|
518
|
+
allowedPrefixes: readonly string[] | undefined,
|
|
519
|
+
options: BashRestrictionOptions = {},
|
|
520
|
+
): string | undefined {
|
|
521
|
+
if (options.profile === "read-only") return undefined;
|
|
522
|
+
const gate = checkBashAllowedPrefixes(command, allowedPrefixes, { ...options, profile: "workflow" });
|
|
523
|
+
if (!gate.allowed) return undefined;
|
|
524
|
+
const parsed = parseShellWords(command.trim());
|
|
525
|
+
if (parsed.reason || parsed.words.length === 0) return undefined;
|
|
526
|
+
const [head, ...rest] = parsed.words;
|
|
527
|
+
if (!head) return undefined;
|
|
528
|
+
const resolvedHead = resolvedExternalCommand(head);
|
|
529
|
+
if (!resolvedHead) return undefined;
|
|
530
|
+
return disableGitExternalHelpers(head, [resolvedHead, ...rest])
|
|
531
|
+
.map(shellQuote)
|
|
532
|
+
.join(" ");
|
|
533
|
+
}
|
|
534
|
+
|
|
475
535
|
export function normalizeReadOnlyBashCommand(command: string): string | undefined {
|
|
476
536
|
const parsed = parseShellWords(command.trim());
|
|
477
537
|
if (parsed.reason || parsed.words.length === 0) return undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type * as fsTypes from "node:fs";
|
|
1
2
|
import * as fs from "node:fs/promises";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { isEnoent } from "@gajae-code/utils";
|
|
@@ -40,7 +41,7 @@ export interface InternalUrlExpansionOptions {
|
|
|
40
41
|
* Resolve a single skill:// URL to its absolute filesystem path.
|
|
41
42
|
* Does NOT read file content or verify existence.
|
|
42
43
|
*/
|
|
43
|
-
export function
|
|
44
|
+
export function resolveSkillUrl(url: string, skills: readonly Skill[]): { skill: Skill; lexicalPath: string } {
|
|
44
45
|
const parsed = /^skill:\/\/([^/?#]+)(\/[^?#]*)?(?:[?#].*)?$/.exec(url);
|
|
45
46
|
if (!parsed) {
|
|
46
47
|
throw new ToolError(`Invalid skill:// URL: ${url}`);
|
|
@@ -72,7 +73,7 @@ export function resolveSkillUrlToPath(url: string, skills: readonly Skill[]): st
|
|
|
72
73
|
const hasRelativePath = rawPath !== "" && rawPath !== "/";
|
|
73
74
|
|
|
74
75
|
if (!hasRelativePath) {
|
|
75
|
-
return path.resolve(skill.filePath);
|
|
76
|
+
return { skill, lexicalPath: path.resolve(skill.filePath) };
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
let relativePath: string;
|
|
@@ -95,7 +96,11 @@ export function resolveSkillUrlToPath(url: string, skills: readonly Skill[]): st
|
|
|
95
96
|
throw new ToolError("Path traversal is not allowed in skill:// URLs");
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
return resolvedPath;
|
|
99
|
+
return { skill, lexicalPath: resolvedPath };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function resolveSkillUrlToPath(url: string, skills: readonly Skill[]): string {
|
|
103
|
+
return resolveSkillUrl(url, skills).lexicalPath;
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
/**
|
|
@@ -150,7 +155,7 @@ async function canonicalLocalPath(localRoot: string, lexicalPath: string): Promi
|
|
|
150
155
|
const missing: string[] = [];
|
|
151
156
|
let cursor = lexicalPath;
|
|
152
157
|
for (;;) {
|
|
153
|
-
let stat:
|
|
158
|
+
let stat: fsTypes.Stats | undefined;
|
|
154
159
|
try {
|
|
155
160
|
stat = await fs.lstat(cursor);
|
|
156
161
|
} catch (error) {
|
|
@@ -177,6 +182,53 @@ async function canonicalLocalPath(localRoot: string, lexicalPath: string): Promi
|
|
|
177
182
|
}
|
|
178
183
|
}
|
|
179
184
|
|
|
185
|
+
const SKILL_ESCAPE = "Path traversal is not allowed in skill:// URLs";
|
|
186
|
+
|
|
187
|
+
/** Real path of `target`, or the real ancestor plus the missing suffix. */
|
|
188
|
+
async function canonicalPathOrAncestor(target: string, escapeMessage: string): Promise<string> {
|
|
189
|
+
try {
|
|
190
|
+
return await fs.realpath(target);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (!isEnoent(error)) throw error;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const missing: string[] = [];
|
|
196
|
+
let cursor = target;
|
|
197
|
+
for (;;) {
|
|
198
|
+
let stat: fsTypes.Stats | undefined;
|
|
199
|
+
try {
|
|
200
|
+
stat = await fs.lstat(cursor);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (!isEnoent(error)) throw error;
|
|
203
|
+
}
|
|
204
|
+
if (stat) {
|
|
205
|
+
try {
|
|
206
|
+
return path.join(await fs.realpath(cursor), ...missing);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (isEnoent(error)) throw new ToolError(escapeMessage);
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const parent = path.dirname(cursor);
|
|
213
|
+
if (parent === cursor) throw new ToolError(escapeMessage);
|
|
214
|
+
missing.unshift(path.basename(cursor));
|
|
215
|
+
cursor = parent;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Keep the lexical skill path, then bind the shell to the real path inside the skill directory.
|
|
221
|
+
* A missing file under a real in-skill ancestor still expands.
|
|
222
|
+
*/
|
|
223
|
+
async function canonicalSkillExpansionPath(baseDir: string, lexicalPath: string): Promise<string> {
|
|
224
|
+
const realBase = await canonicalPathOrAncestor(baseDir, SKILL_ESCAPE);
|
|
225
|
+
const realTarget = await canonicalPathOrAncestor(lexicalPath, SKILL_ESCAPE);
|
|
226
|
+
if (localPathEscapes(realBase, realTarget)) {
|
|
227
|
+
throw new ToolError(SKILL_ESCAPE);
|
|
228
|
+
}
|
|
229
|
+
return realTarget;
|
|
230
|
+
}
|
|
231
|
+
|
|
180
232
|
function extractScheme(url: string): SupportedInternalScheme | undefined {
|
|
181
233
|
const match = /^([a-z][a-z0-9+.-]*):\/\//i.exec(url);
|
|
182
234
|
if (!match) return undefined;
|
|
@@ -211,7 +263,8 @@ async function resolveInternalUrlToPath(
|
|
|
211
263
|
}
|
|
212
264
|
|
|
213
265
|
if (scheme === "skill") {
|
|
214
|
-
|
|
266
|
+
const { skill, lexicalPath } = resolveSkillUrl(url, skills);
|
|
267
|
+
return canonicalSkillExpansionPath(skill.baseDir, lexicalPath);
|
|
215
268
|
}
|
|
216
269
|
|
|
217
270
|
// Managed artifact and agent resources carry session authority. Expanding
|
package/src/tools/bash.ts
CHANGED
|
@@ -64,7 +64,12 @@ import { CachedOutputBlock, getOutputBlockContentWidth } from "../tui/output-blo
|
|
|
64
64
|
import { truncateToWidth } from "../tui/utils";
|
|
65
65
|
import { getSixelLineMask } from "../utils/sixel";
|
|
66
66
|
import type { ToolSession } from ".";
|
|
67
|
-
import {
|
|
67
|
+
import { AwaitingInputDetector, type AwaitingInputSignal } from "./awaiting-input";
|
|
68
|
+
import {
|
|
69
|
+
checkBashAllowedPrefixes,
|
|
70
|
+
normalizeReadOnlyBashCommand,
|
|
71
|
+
normalizeWorkflowBashCommand,
|
|
72
|
+
} from "./bash-allowed-prefixes";
|
|
68
73
|
import { applyBashFixups } from "./bash-command-fixup";
|
|
69
74
|
import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-interactive";
|
|
70
75
|
import { checkBashInterception } from "./bash-interceptor";
|
|
@@ -578,6 +583,12 @@ export interface BashToolDetails {
|
|
|
578
583
|
foldReason?: FoldReason;
|
|
579
584
|
/** The accepted model-declared activity declaration, when the call carried one. */
|
|
580
585
|
activity?: BashActivityDeclaration;
|
|
586
|
+
/**
|
|
587
|
+
* Set on a running (partial) update when the foreground command printed an
|
|
588
|
+
* interactive auth prompt and then went quiet. Carries only the prompt kind,
|
|
589
|
+
* never the URL or code.
|
|
590
|
+
*/
|
|
591
|
+
awaitingInput?: AwaitingInputSignal;
|
|
581
592
|
async?: {
|
|
582
593
|
state: "running" | "completed" | "failed";
|
|
583
594
|
jobId: string;
|
|
@@ -1125,6 +1136,17 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1125
1136
|
const spillThreshold = resolveBashOutputSinkTailBytes(this.session.settings);
|
|
1126
1137
|
const headBytes = resolveBashOutputSinkHeadBytes(this.session.settings);
|
|
1127
1138
|
const tailBuffer = new TailBuffer(spillThreshold);
|
|
1139
|
+
// Only a foreground wait blocks the turn; once folded or started in the
|
|
1140
|
+
// background the agent is free and this tool call has already settled.
|
|
1141
|
+
const awaitingInput = new AwaitingInputDetector({
|
|
1142
|
+
onDetect: signal => {
|
|
1143
|
+
if (backgrounded) return;
|
|
1144
|
+
void options.onUpdate?.({
|
|
1145
|
+
content: [{ type: "text", text: latestText }],
|
|
1146
|
+
details: { ...activityDetails, awaitingInput: signal },
|
|
1147
|
+
});
|
|
1148
|
+
},
|
|
1149
|
+
});
|
|
1128
1150
|
|
|
1129
1151
|
let executionResult: BashResult | BashInteractiveResult | undefined;
|
|
1130
1152
|
try {
|
|
@@ -1148,18 +1170,24 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1148
1170
|
spillThreshold,
|
|
1149
1171
|
headBytes,
|
|
1150
1172
|
oneShot: true,
|
|
1151
|
-
ignoreShellPrefix: this
|
|
1152
|
-
disableShellSnapshot: this
|
|
1173
|
+
ignoreShellPrefix: this.#isolateRestrictedShell(),
|
|
1174
|
+
disableShellSnapshot: this.#isolateRestrictedShell(),
|
|
1153
1175
|
onChunk: chunk => {
|
|
1154
1176
|
tailBuffer.append(chunk);
|
|
1155
1177
|
latestText = tailBuffer.text();
|
|
1156
1178
|
void reportProgress(latestText, runningDetails(jobId));
|
|
1157
1179
|
},
|
|
1158
|
-
|
|
1180
|
+
// onChunk is throttled and drops chunks inside the window; the
|
|
1181
|
+
// detector must see every chunk, so it reads the raw stream.
|
|
1182
|
+
onRawChunk: chunk => {
|
|
1183
|
+
manager.appendOutput(jobId, chunk);
|
|
1184
|
+
awaitingInput.push(chunk);
|
|
1185
|
+
},
|
|
1159
1186
|
onMinimizedSave: async originalText =>
|
|
1160
1187
|
saveBashOriginalArtifact(options.artifactPublication, originalText),
|
|
1161
1188
|
});
|
|
1162
1189
|
executionResult = result;
|
|
1190
|
+
awaitingInput.dispose();
|
|
1163
1191
|
const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
|
|
1164
1192
|
requestedTimeoutSec: options.requestedTimeoutSec,
|
|
1165
1193
|
notices: options.notices,
|
|
@@ -1173,6 +1201,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1173
1201
|
await reportProgress(finalText, completedDetails(jobId));
|
|
1174
1202
|
return finalText;
|
|
1175
1203
|
} catch (error) {
|
|
1204
|
+
awaitingInput.dispose();
|
|
1176
1205
|
const message = error instanceof Error ? error.message : String(error);
|
|
1177
1206
|
latestText = message;
|
|
1178
1207
|
options.onCompletion?.();
|
|
@@ -1323,6 +1352,11 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1323
1352
|
* helper after `AgentSession` has applied the public-tool permission gate, so
|
|
1324
1353
|
* Monitor inherits Bash's cwd / env / artifact / interceptor pipeline 1:1.
|
|
1325
1354
|
*/
|
|
1355
|
+
/** Read-only and workflow allowlists must not source a snapshot or a shell prefix. */
|
|
1356
|
+
#isolateRestrictedShell(): boolean {
|
|
1357
|
+
return this.session.bashRestrictionProfile === "read-only" || (this.session.bashAllowedPrefixes?.length ?? 0) > 0;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1326
1360
|
async #prepareBashExecution(
|
|
1327
1361
|
input: { command: string; env?: Record<string, string>; timeout?: number; cwd?: string },
|
|
1328
1362
|
ctx?: AgentToolContext,
|
|
@@ -1447,19 +1481,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1447
1481
|
...internalUrlOptions,
|
|
1448
1482
|
ensureLocalParentDirs: this.session.bashRestrictionProfile !== "read-only",
|
|
1449
1483
|
});
|
|
1450
|
-
// Issue #5039: refuse tmux write verbs whose target resolves to this
|
|
1451
|
-
// agent's own tmux pane — such bytes forge a human `user` turn in the
|
|
1452
|
-
// transcript. This is after command expansion but before any spawn, so
|
|
1453
|
-
// blocked attempts inject no bytes and create no transcript turn.
|
|
1454
|
-
if (this.session.bashRestrictionProfile !== "read-only") {
|
|
1455
|
-
const injection = await checkTmuxSelfInjection(command, {
|
|
1456
|
-
env: process.env,
|
|
1457
|
-
cwd: this.session.cwd,
|
|
1458
|
-
});
|
|
1459
|
-
if (injection.block) {
|
|
1460
|
-
throw new ToolError(injection.reason ?? "Blocked: tmux self-pane injection.");
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
1484
|
const expandedEnv = env
|
|
1464
1485
|
? Object.fromEntries(
|
|
1465
1486
|
await Promise.all(
|
|
@@ -1541,6 +1562,45 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1541
1562
|
if (this.session.bashRestrictionProfile === "read-only" || (allowedPrefixes?.length ?? 0) > 0) {
|
|
1542
1563
|
await assertCwdInsideWorkspace(this.session.cwd, commandCwd);
|
|
1543
1564
|
}
|
|
1565
|
+
// Issue #5039: refuse tmux write verbs whose target resolves to this
|
|
1566
|
+
// agent's own tmux pane — such bytes forge a human `user` turn in the
|
|
1567
|
+
// transcript. This is after command/env/cwd preparation but before any
|
|
1568
|
+
// spawn, so guard discovery matches execution without trusting caller env
|
|
1569
|
+
// for the agent's tmux identity.
|
|
1570
|
+
if (this.session.bashRestrictionProfile !== "read-only") {
|
|
1571
|
+
const shellConfigEnv = this.session.settings.getShellConfig().env;
|
|
1572
|
+
const injection = await checkTmuxSelfInjection(command, {
|
|
1573
|
+
env: process.env,
|
|
1574
|
+
cwd: commandCwd,
|
|
1575
|
+
executionEnv: { ...process.env, ...shellConfigEnv, ...resolvedEnv },
|
|
1576
|
+
});
|
|
1577
|
+
if (injection.block) {
|
|
1578
|
+
throw new ToolError(injection.reason ?? "Blocked: tmux self-pane injection.");
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
// Direct master spawn parses the original command itself and does not source
|
|
1582
|
+
// a shell snapshot. Every other restricted workflow command is rebound here,
|
|
1583
|
+
// after the allowlist and interceptor have seen the caller's text.
|
|
1584
|
+
if (
|
|
1585
|
+
!directMasterSpawn &&
|
|
1586
|
+
this.session.bashRestrictionProfile !== "read-only" &&
|
|
1587
|
+
allowedPrefixes &&
|
|
1588
|
+
allowedPrefixes.length > 0
|
|
1589
|
+
) {
|
|
1590
|
+
const normalizedWorkflowCommand = normalizeWorkflowBashCommand(command, allowedPrefixes, {
|
|
1591
|
+
profile: "workflow",
|
|
1592
|
+
gjcSessionBinding: {
|
|
1593
|
+
sessionId: this.session.getSessionId?.() ?? null,
|
|
1594
|
+
credentialSessionId: this.session.getCredentialSessionId?.() ?? null,
|
|
1595
|
+
worktreeRoot: this.session.cwd,
|
|
1596
|
+
invocationCwd: commandCwd,
|
|
1597
|
+
},
|
|
1598
|
+
});
|
|
1599
|
+
if (!normalizedWorkflowCommand) {
|
|
1600
|
+
throw new ToolError("Restricted role-agent bash command could not be normalized safely.");
|
|
1601
|
+
}
|
|
1602
|
+
command = normalizedWorkflowCommand;
|
|
1603
|
+
}
|
|
1544
1604
|
|
|
1545
1605
|
const requestedTimeoutSec = input.timeout ?? 300;
|
|
1546
1606
|
const timeoutSec = clampTimeout("bash", requestedTimeoutSec);
|
|
@@ -1724,9 +1784,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1724
1784
|
spillThreshold,
|
|
1725
1785
|
headBytes,
|
|
1726
1786
|
oneShot: true,
|
|
1727
|
-
ignoreShellPrefix: this
|
|
1728
|
-
disableShellSnapshot:
|
|
1729
|
-
this.session.bashRestrictionProfile === "read-only" || prepared.directMasterSpawn,
|
|
1787
|
+
ignoreShellPrefix: this.#isolateRestrictedShell() || prepared.directMasterSpawn,
|
|
1788
|
+
disableShellSnapshot: this.#isolateRestrictedShell() || prepared.directMasterSpawn,
|
|
1730
1789
|
onChunk: chunk => {
|
|
1731
1790
|
tailBuffer.append(chunk);
|
|
1732
1791
|
void reportProgress(tailBuffer.text(), {
|
|
@@ -2651,6 +2710,14 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2651
2710
|
|
|
2652
2711
|
// Track output for streaming updates (tail only)
|
|
2653
2712
|
const tailBuffer = new TailBuffer(spillThreshold);
|
|
2713
|
+
const awaitingInput = new AwaitingInputDetector({
|
|
2714
|
+
onDetect: signal => {
|
|
2715
|
+
void onUpdate?.({
|
|
2716
|
+
content: [{ type: "text", text: tailBuffer.text() }],
|
|
2717
|
+
details: { ...(activity === undefined ? {} : { activity }), awaitingInput: signal },
|
|
2718
|
+
});
|
|
2719
|
+
},
|
|
2720
|
+
});
|
|
2654
2721
|
|
|
2655
2722
|
const artifactPublisher = createBashArtifactPublisher(artifactPublication);
|
|
2656
2723
|
|
|
@@ -2833,11 +2900,15 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2833
2900
|
onOutput: chunk => {
|
|
2834
2901
|
ptyOutputSeen = true;
|
|
2835
2902
|
if (ptyJobId) ownedManager?.appendOutput(ptyJobId, chunk);
|
|
2903
|
+
awaitingInput.push(chunk);
|
|
2836
2904
|
},
|
|
2837
|
-
}).catch(error => {
|
|
2838
|
-
if (ptyAdmission) ownedManager?.releaseCapacity(ptyAdmission);
|
|
2839
|
-
throw error;
|
|
2840
2905
|
})
|
|
2906
|
+
.catch(error => {
|
|
2907
|
+
if (ptyAdmission) ownedManager?.releaseCapacity(ptyAdmission);
|
|
2908
|
+
throw error;
|
|
2909
|
+
})
|
|
2910
|
+
// Completion, error, and a fold all settle this foreground wait.
|
|
2911
|
+
.finally(() => awaitingInput.dispose())
|
|
2841
2912
|
: await executeBash(command, {
|
|
2842
2913
|
cwd: commandCwd,
|
|
2843
2914
|
settings: this.session.settings,
|
|
@@ -2851,10 +2922,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
2851
2922
|
spillThreshold,
|
|
2852
2923
|
headBytes,
|
|
2853
2924
|
onChunk: streamTailUpdates(tailBuffer, onUpdate),
|
|
2925
|
+
// onChunk is throttled and drops chunks; the detector needs all of them.
|
|
2926
|
+
onRawChunk: chunk => awaitingInput.push(chunk),
|
|
2854
2927
|
onMinimizedSave: async originalText => saveBashOriginalArtifact(artifactPublication, originalText),
|
|
2855
|
-
ignoreShellPrefix: this
|
|
2856
|
-
disableShellSnapshot: this
|
|
2857
|
-
});
|
|
2928
|
+
ignoreShellPrefix: this.#isolateRestrictedShell() || directMasterSpawn,
|
|
2929
|
+
disableShellSnapshot: this.#isolateRestrictedShell() || directMasterSpawn,
|
|
2930
|
+
}).finally(() => awaitingInput.dispose());
|
|
2858
2931
|
ptyFoldUnregister?.();
|
|
2859
2932
|
// Folded: the foreground was settled by the fold, so return the background
|
|
2860
2933
|
// -start result verbatim instead of presenting a half-finished command.
|
|
@@ -3,7 +3,6 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { untilAborted } from "@gajae-code/utils/abortable";
|
|
6
|
-
import { Snowflake } from "@gajae-code/utils/snowflake";
|
|
7
6
|
import type { HTMLElement } from "linkedom";
|
|
8
7
|
import type {
|
|
9
8
|
Browser,
|
|
@@ -414,6 +413,31 @@ interface ActiveRun {
|
|
|
414
413
|
pendingTools: Map<string, { resolve(value: unknown): void; reject(error: Error): void }>;
|
|
415
414
|
}
|
|
416
415
|
|
|
416
|
+
async function writePrivateScreenshot(bytes: Uint8Array): Promise<string> {
|
|
417
|
+
// Random directory plus an exclusive create: a preexisting symlink under the
|
|
418
|
+
// shared temp directory is not opened, and the screenshot is not left world-readable.
|
|
419
|
+
// Create the temp root first: base created it with a recursive mkdir, and
|
|
420
|
+
// mkdtemp fails when that root does not exist yet. Do not change the root's mode.
|
|
421
|
+
const root = os.tmpdir();
|
|
422
|
+
await fs.promises.mkdir(root, { recursive: true });
|
|
423
|
+
const tempDir = await fs.promises.mkdtemp(path.join(root, "gjc-sshots-"));
|
|
424
|
+
try {
|
|
425
|
+
if (process.platform !== "win32") await fs.promises.chmod(tempDir, 0o700);
|
|
426
|
+
const dest = path.join(tempDir, "screenshot.png");
|
|
427
|
+
const handle = await fs.promises.open(dest, "wx", 0o600);
|
|
428
|
+
try {
|
|
429
|
+
await handle.writeFile(bytes);
|
|
430
|
+
} finally {
|
|
431
|
+
await handle.close();
|
|
432
|
+
}
|
|
433
|
+
if (process.platform !== "win32") await fs.promises.chmod(dest, 0o600);
|
|
434
|
+
return dest;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
417
441
|
export class WorkerCore {
|
|
418
442
|
#transport: Transport;
|
|
419
443
|
#browser?: Browser;
|
|
@@ -852,20 +876,22 @@ export class WorkerCore {
|
|
|
852
876
|
{ maxWidth: 1024, maxHeight: 1024, maxBytes: 150 * 1024, jpegQuality: 70 },
|
|
853
877
|
);
|
|
854
878
|
const explicitPath = opts.save ? resolveTabWorkerPath(opts.save, session.cwd) : undefined;
|
|
855
|
-
|
|
856
|
-
const
|
|
857
|
-
explicitPath ??
|
|
858
|
-
(session.browserScreenshotDir
|
|
859
|
-
? path.join(
|
|
860
|
-
session.browserScreenshotDir,
|
|
861
|
-
`screenshot-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, -1)}.png`,
|
|
862
|
-
)
|
|
863
|
-
: path.join(os.tmpdir(), `gjc-sshots-${Snowflake.next()}.png`));
|
|
864
|
-
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
865
|
-
const saveFullRes = !!(explicitPath || session.browserScreenshotDir);
|
|
879
|
+
const configuredDir = session.browserScreenshotDir;
|
|
880
|
+
const saveFullRes = !!(explicitPath || configuredDir);
|
|
866
881
|
const savedBuffer = saveFullRes ? buffer : resized.buffer;
|
|
867
882
|
const savedMimeType = saveFullRes ? "image/png" : resized.mimeType;
|
|
868
|
-
|
|
883
|
+
// Caller-chosen destinations stay as they were. Only the shared temp
|
|
884
|
+
// fallback is an exclusive private file: a preexisting symlink at the old
|
|
885
|
+
// predictable name is not opened, and the bytes are not left world-readable.
|
|
886
|
+
const dest = explicitPath
|
|
887
|
+
? explicitPath
|
|
888
|
+
: configuredDir
|
|
889
|
+
? path.join(configuredDir, `screenshot-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, -1)}.png`)
|
|
890
|
+
: await writePrivateScreenshot(savedBuffer);
|
|
891
|
+
if (explicitPath || configuredDir) {
|
|
892
|
+
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
893
|
+
await Bun.write(dest, savedBuffer);
|
|
894
|
+
}
|
|
869
895
|
const info: ScreenshotResult = {
|
|
870
896
|
dest,
|
|
871
897
|
mimeType: savedMimeType,
|