@gajae-code/coding-agent 0.15.4 → 0.15.6
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 +46 -2
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/update-cli.d.ts +27 -5
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +99 -0
- package/dist/types/config/file-lock.d.ts +49 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -4
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/sdk/broker/broker.d.ts +3 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/cli/session-cli.d.ts +42 -1
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +19 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +19 -5
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/tools/path-utils.d.ts +5 -6
- package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -3
- package/src/async/job-manager.ts +671 -39
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +3 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/update-cli.ts +142 -6
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +5 -0
- package/src/config/autorouting-tier-map.ts +33 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +775 -59
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2799 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1028 -123
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +6 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/policy.ts +1 -1
- package/src/coordinator-mcp/projection-scan.ts +571 -0
- package/src/coordinator-mcp/server.ts +19 -24
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree.ts +44 -7
- package/src/gjc-runtime/ralplan-runtime.ts +14 -1
- package/src/gjc-runtime/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/gjc-runtime/tmux-sessions.ts +145 -48
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +9 -2
- package/src/modes/acp/acp-agent.ts +11 -5
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +23 -6
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/prompt-action-autocomplete.ts +26 -47
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +48 -21
- package/src/sdk/broker/lifecycle.ts +32 -6
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
- package/src/sdk/bus/kind-aware-reconciliation.ts +46 -8
- package/src/sdk/bus/prompt-reconciliation.ts +33 -6
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/cli/session-cli.ts +161 -5
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -0
- package/src/sdk/host/host.ts +7 -0
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +1240 -107
- package/src/sdk/host/session-runtime.ts +1029 -115
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +2 -2
- package/src/sdk/protocol/operation-registry.ts +4 -1
- package/src/sdk/router/session-router.ts +297 -111
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +887 -95
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +64 -26
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +29 -10
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +142 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +786 -67
- package/src/tools/index.ts +7 -5
- package/src/tools/path-utils.ts +77 -29
- package/src/tools/tmux-self-injection-guard.ts +359 -0
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
package/src/tools/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
} from "../session/agent-session";
|
|
18
18
|
import type { ArtifactManager } from "../session/artifacts";
|
|
19
19
|
import type { ClientBridge } from "../session/client-bridge";
|
|
20
|
+
import type { FoldAdapter } from "../session/fold-coordinator";
|
|
20
21
|
import type { CustomMessage } from "../session/messages";
|
|
21
22
|
import type { ToolChoiceQueue } from "../session/tool-choice-queue";
|
|
22
23
|
import type { SkillActiveEntry } from "../skill-state/active-state";
|
|
@@ -235,12 +236,13 @@ export interface ToolSession {
|
|
|
235
236
|
assertEvalExecutionAllowed?: () => void;
|
|
236
237
|
/** Track tool-owned eval work so session disposal can await/abort it like direct session eval runs. */
|
|
237
238
|
trackEvalExecution?<T>(execution: Promise<T>, abortController: AbortController): Promise<T>;
|
|
238
|
-
/** Register a
|
|
239
|
-
|
|
240
|
-
/** Whether a
|
|
239
|
+
/** Register a foldable foreground wait so the fold chord can move it into a background job. */
|
|
240
|
+
registerForegroundFoldParticipant?: (adapter: FoldAdapter) => () => void;
|
|
241
|
+
/** Whether a foreground wait is currently foldable into a background job. */
|
|
241
242
|
hasForegroundBashBackgroundRequestHandler?: () => boolean;
|
|
242
|
-
/** Request that the active
|
|
243
|
-
requestForegroundBashBackground?: () => boolean
|
|
243
|
+
/** Request that the active foreground wait fold into a background job, if supported. */
|
|
244
|
+
requestForegroundBashBackground?: () => Promise<boolean>;
|
|
245
|
+
|
|
244
246
|
/** Get the session-owned or inherited async job manager. */
|
|
245
247
|
getAsyncJobManager?: () => AsyncJobManager | undefined;
|
|
246
248
|
/** Resolves when the session queues user steering (or `signal` aborts) without consuming it; wait-style tools use this to end their observation early. */
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -11,17 +11,20 @@ const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
|
11
11
|
const FILE_LINE_RANGE_RE = /^(?:L?\d+(?:[-+]L?\d+|-)?(?:,L?\d+(?:[-+]L?\d+|-)?)*|raw|conflicts)$/i;
|
|
12
12
|
const FILE_LINE_RANGE_ONLY_RE = /^L?\d+(?:[-+]L?\d+|-)?(?:,L?\d+(?:[-+]L?\d+|-)?)*$/i;
|
|
13
13
|
const FILE_RAW_ONLY_RE = /^raw$/i;
|
|
14
|
-
// Schemes whose authority grammar is identifier-shaped and
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
const
|
|
14
|
+
// Schemes whose authority grammar is identifier-shaped and cannot contain a
|
|
15
|
+
// literal colon. Colons in a path, query, or fragment remain part of the URL
|
|
16
|
+
// and are never considered selectors.
|
|
17
|
+
const INTERNAL_SCHEMES_WITH_UNAMBIGUOUS_AUTHORITIES: Record<string, true> = {
|
|
18
18
|
agent: true,
|
|
19
19
|
artifact: true,
|
|
20
|
-
issue: true,
|
|
21
|
-
local: true,
|
|
22
20
|
memory: true,
|
|
23
|
-
|
|
21
|
+
issue: true,
|
|
24
22
|
pr: true,
|
|
23
|
+
};
|
|
24
|
+
const INTERNAL_SCHEMES_WITH_SELECTORS: Record<string, true> = {
|
|
25
|
+
...INTERNAL_SCHEMES_WITH_UNAMBIGUOUS_AUTHORITIES,
|
|
26
|
+
gjc: true,
|
|
27
|
+
local: true,
|
|
25
28
|
rule: true,
|
|
26
29
|
skill: true,
|
|
27
30
|
};
|
|
@@ -33,6 +36,18 @@ function normalizeUnicodeSpaces(str: string): string {
|
|
|
33
36
|
return str.replace(UNICODE_SPACES, " ");
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
function rawSkillPrefixForName(authority: string, skillName: string): string | undefined {
|
|
40
|
+
for (let index = 1; index < authority.length; index++) {
|
|
41
|
+
if (authority[index] !== ":") continue;
|
|
42
|
+
try {
|
|
43
|
+
if (decodeURIComponent(authority.slice(0, index)) === skillName) return authority.slice(0, index);
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
function tryMacOSScreenshotPath(filePath: string): string {
|
|
37
52
|
// macOS writes a narrow no-break space before AM/PM, but the model normalizes it
|
|
38
53
|
// to a plain space. The original name is not always `…PM.png`: attachment paths
|
|
@@ -149,12 +164,11 @@ export function splitPathAndSel(rawPath: string): { path: string; sel?: string }
|
|
|
149
164
|
/**
|
|
150
165
|
* Variant of {@link splitPathAndSel} for internal URLs (`scheme://...`).
|
|
151
166
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* skill name from its selector without making path-utils depend on skills.
|
|
167
|
+
* Selector-capable internal authorities use identifier-shaped resource names,
|
|
168
|
+
* so an authority-only `:<tail>` is an explicit read selector even when the
|
|
169
|
+
* selector is malformed. Skill authorities are the exception: namespaced
|
|
170
|
+
* skill names already contain a colon, so the active registry gets first
|
|
171
|
+
* refusal and the fallback recognizes a later colon as the selector boundary.
|
|
158
172
|
*/
|
|
159
173
|
export function splitInternalUrlSel(
|
|
160
174
|
rawPath: string,
|
|
@@ -167,32 +181,66 @@ export function splitInternalUrlSel(
|
|
|
167
181
|
|
|
168
182
|
const schemeEnd = schemeMatch[0].length;
|
|
169
183
|
const authorityTerminator = rawPath.slice(schemeEnd).search(/[/?#]/);
|
|
170
|
-
|
|
171
|
-
const authority = rawPath.slice(schemeEnd);
|
|
184
|
+
const authorityEnd = authorityTerminator === -1 ? rawPath.length : schemeEnd + authorityTerminator;
|
|
185
|
+
const authority = rawPath.slice(schemeEnd, authorityEnd);
|
|
186
|
+
const authoritySuffix = rawPath.slice(authorityEnd);
|
|
172
187
|
const firstColon = authority.indexOf(":");
|
|
173
188
|
if (firstColon === -1) return { path: rawPath };
|
|
174
|
-
|
|
175
|
-
if (scheme === "artifact") {
|
|
176
|
-
return { path: rawPath.slice(0, schemeEnd + firstColon), sel: authority.slice(firstColon + 1) };
|
|
177
|
-
}
|
|
189
|
+
if (firstColon === 0) return { path: rawPath };
|
|
178
190
|
|
|
179
191
|
if (scheme === "skill") {
|
|
180
192
|
const activeSkillNames = options.activeSkillNames ?? [];
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
193
|
+
let decodedAuthority: string | undefined;
|
|
194
|
+
try {
|
|
195
|
+
decodedAuthority = decodeURIComponent(authority);
|
|
196
|
+
} catch {
|
|
197
|
+
// Let the resolver report malformed URL encoding when no selector boundary
|
|
198
|
+
// can be established from the raw authority.
|
|
199
|
+
}
|
|
200
|
+
if (decodedAuthority !== undefined && activeSkillNames.includes(decodedAuthority)) return { path: rawPath };
|
|
201
|
+
const rawSkillPrefix = activeSkillNames
|
|
202
|
+
.map(name => ({ name, prefix: rawSkillPrefixForName(authority, name) }))
|
|
203
|
+
.filter(item => item.prefix !== undefined)
|
|
204
|
+
.sort((a, b) => b.name.length - a.name.length)[0]?.prefix;
|
|
205
|
+
if (rawSkillPrefix) {
|
|
186
206
|
return {
|
|
187
|
-
path: `${rawPath.slice(0, schemeEnd)}${
|
|
188
|
-
sel: authority.slice(
|
|
207
|
+
path: `${rawPath.slice(0, schemeEnd)}${rawSkillPrefix}${authoritySuffix}`,
|
|
208
|
+
sel: authority.slice(rawSkillPrefix.length + 1),
|
|
189
209
|
};
|
|
190
210
|
}
|
|
211
|
+
const strict = splitPathAndSel(authority);
|
|
212
|
+
if (strict.sel !== undefined) {
|
|
213
|
+
return { path: `${rawPath.slice(0, schemeEnd)}${strict.path}${authoritySuffix}`, sel: strict.sel };
|
|
214
|
+
}
|
|
215
|
+
const firstTail = authority.slice(firstColon + 1);
|
|
216
|
+
if (FILE_LINE_RANGE_RE.test(firstTail)) {
|
|
217
|
+
return { path: `${rawPath.slice(0, schemeEnd + firstColon)}${authoritySuffix}`, sel: firstTail };
|
|
218
|
+
}
|
|
219
|
+
const selectorColon = authority.indexOf(":", firstColon + 1);
|
|
220
|
+
if (selectorColon === -1) return { path: rawPath };
|
|
221
|
+
return {
|
|
222
|
+
path: `${rawPath.slice(0, schemeEnd + selectorColon)}${authoritySuffix}`,
|
|
223
|
+
sel: authority.slice(selectorColon + 1),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!INTERNAL_SCHEMES_WITH_UNAMBIGUOUS_AUTHORITIES[scheme]) {
|
|
228
|
+
// A path/query/fragment after a path-like authority makes a colon-bearing
|
|
229
|
+
// resource identity indistinguishable from an authority selector. Preserve
|
|
230
|
+
// the complete URL; path-like selectors are accepted only on authority-only
|
|
231
|
+
// URLs, where the existing strict grammar is the sole interpretation.
|
|
232
|
+
if (authoritySuffix !== "") return { path: rawPath };
|
|
233
|
+
const strict = splitPathAndSel(authority);
|
|
234
|
+
if (strict.sel !== undefined) {
|
|
235
|
+
return { path: `${rawPath.slice(0, schemeEnd)}${strict.path}`, sel: strict.sel };
|
|
236
|
+
}
|
|
237
|
+
return { path: rawPath };
|
|
191
238
|
}
|
|
192
239
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
240
|
+
return {
|
|
241
|
+
path: `${rawPath.slice(0, schemeEnd + firstColon)}${authoritySuffix}`,
|
|
242
|
+
sel: authority.slice(firstColon + 1),
|
|
243
|
+
};
|
|
196
244
|
}
|
|
197
245
|
|
|
198
246
|
function assertNotInternalUrl(expanded: string, original: string): void {
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prevent agent-authored tmux input from being delivered to the agent pane.
|
|
3
|
+
*
|
|
4
|
+
* This check runs before bash spawns anything. It is intentionally limited to
|
|
5
|
+
* tmux input verbs; inspection and control of another pane remain available.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
|
|
10
|
+
const INPUT_VERBS = new Set(["send-keys", "paste-buffer", "send-prefix"]);
|
|
11
|
+
const SHELL_RUNNERS = new Set(["sh", "bash", "dash", "zsh", "ksh", "busybox"]);
|
|
12
|
+
const COMMAND_WRAPPERS = new Set(["eval", "exec", "command", "builtin", "nohup", "time"]);
|
|
13
|
+
const CONTROL_WORDS = new Set(["then", "do", "else", "fi", "done", "esac"]);
|
|
14
|
+
const MAX_INDIRECTION_DEPTH = 4;
|
|
15
|
+
|
|
16
|
+
export interface TmuxSelfInjectionResult {
|
|
17
|
+
block: boolean;
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type TmuxPaneResolver = (options: {
|
|
22
|
+
socketArgs: string[];
|
|
23
|
+
target: string;
|
|
24
|
+
env: Record<string, string>;
|
|
25
|
+
}) => Promise<string | undefined>;
|
|
26
|
+
|
|
27
|
+
export interface TmuxSelfInjectionOptions {
|
|
28
|
+
env?: Record<string, string | undefined>;
|
|
29
|
+
cwd?: string;
|
|
30
|
+
resolvePaneId?: TmuxPaneResolver;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Token {
|
|
34
|
+
text: string;
|
|
35
|
+
quoted: boolean;
|
|
36
|
+
commandStart: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isSeparator(ch: string): boolean {
|
|
40
|
+
return ch === ";" || ch === "&" || ch === "|" || ch === "(" || ch === ")" || ch === "\n";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isAssignment(text: string): boolean {
|
|
44
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(text);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Small quote-aware lexer. Unknown shell syntax is represented conservatively. */
|
|
48
|
+
function tokenize(command: string): Token[] {
|
|
49
|
+
const tokens: Token[] = [];
|
|
50
|
+
let text = "";
|
|
51
|
+
let quoted = false;
|
|
52
|
+
let quote: "'" | '"' | undefined;
|
|
53
|
+
let atCommandStart = true;
|
|
54
|
+
let wrapperPending = false;
|
|
55
|
+
let previous = "";
|
|
56
|
+
|
|
57
|
+
const flush = () => {
|
|
58
|
+
if (text.length === 0) return;
|
|
59
|
+
const token: Token = { text, quoted, commandStart: atCommandStart };
|
|
60
|
+
tokens.push(token);
|
|
61
|
+
if (atCommandStart) {
|
|
62
|
+
if (isAssignment(text) || CONTROL_WORDS.has(text) || wrapperPending) {
|
|
63
|
+
atCommandStart = true;
|
|
64
|
+
} else if (COMMAND_WRAPPERS.has(text)) {
|
|
65
|
+
atCommandStart = true;
|
|
66
|
+
wrapperPending = true;
|
|
67
|
+
} else if (text === "sudo" || text === "env") {
|
|
68
|
+
atCommandStart = true;
|
|
69
|
+
wrapperPending = true;
|
|
70
|
+
} else {
|
|
71
|
+
atCommandStart = false;
|
|
72
|
+
wrapperPending = false;
|
|
73
|
+
}
|
|
74
|
+
} else if (wrapperPending) {
|
|
75
|
+
// A wrapper's options and option arguments are not command names.
|
|
76
|
+
// The first ordinary word after them is handled by the special case
|
|
77
|
+
// below when the next token is seen.
|
|
78
|
+
atCommandStart = false;
|
|
79
|
+
}
|
|
80
|
+
text = "";
|
|
81
|
+
quoted = false;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (let index = 0; index < command.length; index++) {
|
|
85
|
+
const ch = command[index];
|
|
86
|
+
if (quote !== undefined) {
|
|
87
|
+
if (ch === quote) {
|
|
88
|
+
quote = undefined;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (quote === '"' && ch === "\\" && index + 1 < command.length) {
|
|
92
|
+
text += command[++index];
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
text += ch;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ch === "'" || ch === '"') {
|
|
99
|
+
quote = ch;
|
|
100
|
+
quoted = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === "\\" && index + 1 < command.length) {
|
|
104
|
+
text += command[++index];
|
|
105
|
+
quoted = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (/\s/.test(ch)) {
|
|
109
|
+
flush();
|
|
110
|
+
previous = ch;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (isSeparator(ch)) {
|
|
114
|
+
flush();
|
|
115
|
+
atCommandStart = true;
|
|
116
|
+
wrapperPending = false;
|
|
117
|
+
previous = ch;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
// `sudo -u user tmux` and `env FOO=bar tmux` need the wrapped command
|
|
121
|
+
// recognized. Options, option arguments, and environment assignments
|
|
122
|
+
// stay outside command position.
|
|
123
|
+
if (text.length === 0 && wrapperPending && previous && /\s/.test(previous)) {
|
|
124
|
+
if (text.length === 0 && ch === "-") {
|
|
125
|
+
// The option itself is not a command; its argument is also skipped.
|
|
126
|
+
atCommandStart = false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (text.length === 0 && wrapperPending && atCommandStart) {
|
|
130
|
+
atCommandStart = true;
|
|
131
|
+
}
|
|
132
|
+
text += ch;
|
|
133
|
+
}
|
|
134
|
+
flush();
|
|
135
|
+
return tokens;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface Invocation {
|
|
139
|
+
verb: string;
|
|
140
|
+
args: string[];
|
|
141
|
+
socketArgs: string[];
|
|
142
|
+
socket?: { flag: "-L" | "-S"; value: string };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseInvocation(tokens: Token[]): Invocation | undefined {
|
|
146
|
+
let index = 0;
|
|
147
|
+
const socketArgs: string[] = [];
|
|
148
|
+
let socket: Invocation["socket"];
|
|
149
|
+
while (index < tokens.length && !tokens[index].commandStart) {
|
|
150
|
+
const option = tokens[index].text;
|
|
151
|
+
if (option === "--") {
|
|
152
|
+
index++;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
if (option === "-L" || option === "-S") {
|
|
156
|
+
const value = tokens[index + 1];
|
|
157
|
+
if (!value || value.commandStart) return undefined;
|
|
158
|
+
socket = { flag: option, value: value.text };
|
|
159
|
+
socketArgs.push(option, value.text);
|
|
160
|
+
index += 2;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (option.startsWith("-L") || option.startsWith("-S")) {
|
|
164
|
+
const flag = option.slice(0, 2) as "-L" | "-S";
|
|
165
|
+
const value = option.slice(2);
|
|
166
|
+
if (!value) return undefined;
|
|
167
|
+
socket = { flag, value };
|
|
168
|
+
socketArgs.push(flag, value);
|
|
169
|
+
index++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (option.startsWith("-")) {
|
|
173
|
+
index++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
const verb = tokens[index];
|
|
179
|
+
if (!verb || verb.commandStart) return undefined;
|
|
180
|
+
const args: string[] = [];
|
|
181
|
+
for (index++; index < tokens.length && !tokens[index].commandStart; index++) args.push(tokens[index].text);
|
|
182
|
+
return { verb: verb.text, args, socketArgs, socket };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function targetFromArgs(args: string[]): string | undefined {
|
|
186
|
+
for (let index = 0; index < args.length; index++) {
|
|
187
|
+
if (args[index] === "-t") return args[index + 1];
|
|
188
|
+
if (args[index].startsWith("-t") && args[index].length > 2) return args[index].slice(2);
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function collectShellPayloads(tokens: Token[]): string[] {
|
|
194
|
+
const payloads: string[] = [];
|
|
195
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
196
|
+
const token = tokens[index];
|
|
197
|
+
if (!token.commandStart || !SHELL_RUNNERS.has(token.text)) continue;
|
|
198
|
+
for (let cursor = index + 1; cursor < tokens.length && !tokens[cursor].commandStart; cursor++) {
|
|
199
|
+
if (tokens[cursor].text === "-c" && tokens[cursor + 1]?.quoted) {
|
|
200
|
+
payloads.push(tokens[cursor + 1].text);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return payloads;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function collectShellScripts(tokens: Token[], cwd: string): string[] {
|
|
209
|
+
const scripts: string[] = [];
|
|
210
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
211
|
+
const token = tokens[index];
|
|
212
|
+
if (token.commandStart && (token.text.startsWith("./") || token.text.startsWith("/"))) {
|
|
213
|
+
scripts.push(path.resolve(cwd, token.text));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!token.commandStart || !SHELL_RUNNERS.has(token.text)) continue;
|
|
217
|
+
for (let cursor = index + 1; cursor < tokens.length && !tokens[cursor].commandStart; cursor++) {
|
|
218
|
+
const argument = tokens[cursor].text;
|
|
219
|
+
if (argument === "-c") break;
|
|
220
|
+
if (argument.startsWith("-")) continue;
|
|
221
|
+
scripts.push(path.resolve(cwd, argument));
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return scripts;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
interface Identity {
|
|
229
|
+
socketPath: string;
|
|
230
|
+
paneId: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function currentIdentity(env: Record<string, string | undefined>): Identity | undefined {
|
|
234
|
+
const tmux = env.TMUX;
|
|
235
|
+
const paneId = env.TMUX_PANE;
|
|
236
|
+
const socketPath = tmux?.split(",")[0];
|
|
237
|
+
return socketPath && paneId ? { socketPath, paneId } : undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function socketPathFor(
|
|
241
|
+
socket: NonNullable<Invocation["socket"]>,
|
|
242
|
+
env: Record<string, string | undefined>,
|
|
243
|
+
cwd: string,
|
|
244
|
+
uid: number,
|
|
245
|
+
): string {
|
|
246
|
+
if (socket.flag === "-S") return path.isAbsolute(socket.value) ? socket.value : path.resolve(cwd, socket.value);
|
|
247
|
+
return path.join(env.TMUX_TMPDIR ?? "/tmp", `tmux-${uid}`, socket.value);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function sameSocket(left: string, right: string): boolean {
|
|
251
|
+
if (left === right) return true;
|
|
252
|
+
try {
|
|
253
|
+
return fs.realpathSync(left) === fs.realpathSync(right);
|
|
254
|
+
} catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function assignedTmuxSocket(tokens: Token[], commandIndex: number): string | undefined {
|
|
260
|
+
for (let index = commandIndex - 1; index >= 0 && commandIndex - index <= 4; index--) {
|
|
261
|
+
const assignment = tokens[index].text.match(/^TMUX=([^,\s]+)/);
|
|
262
|
+
if (assignment) return assignment[1];
|
|
263
|
+
if (!tokens[index].commandStart) break;
|
|
264
|
+
}
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const defaultResolvePaneId: TmuxPaneResolver = async ({ socketArgs, target, env }) => {
|
|
269
|
+
const processHandle = Bun.spawn(["tmux", ...socketArgs, "display-message", "-p", "-t", target, "#{pane_id}"], {
|
|
270
|
+
env,
|
|
271
|
+
stdin: "ignore",
|
|
272
|
+
stdout: "pipe",
|
|
273
|
+
stderr: "ignore",
|
|
274
|
+
});
|
|
275
|
+
const killTimer = setTimeout(() => processHandle.kill(), 2_000);
|
|
276
|
+
try {
|
|
277
|
+
const output = await new Response(processHandle.stdout).text();
|
|
278
|
+
await processHandle.exited;
|
|
279
|
+
const paneId = output.trim().split("\n")[0];
|
|
280
|
+
return paneId?.startsWith("%") ? paneId : undefined;
|
|
281
|
+
} finally {
|
|
282
|
+
clearTimeout(killTimer);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export async function checkTmuxSelfInjection(
|
|
287
|
+
command: string,
|
|
288
|
+
options: TmuxSelfInjectionOptions = {},
|
|
289
|
+
depth = 0,
|
|
290
|
+
): Promise<TmuxSelfInjectionResult> {
|
|
291
|
+
const env = options.env ?? process.env;
|
|
292
|
+
const identity = currentIdentity(env);
|
|
293
|
+
if (!identity) return { block: false };
|
|
294
|
+
const cwd = options.cwd ?? env.PWD ?? ".";
|
|
295
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
296
|
+
const resolvePaneId = options.resolvePaneId ?? defaultResolvePaneId;
|
|
297
|
+
const tokens = tokenize(command);
|
|
298
|
+
|
|
299
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
300
|
+
if (!tokens[index].commandStart || (tokens[index].text !== "tmux" && !tokens[index].text.endsWith("/tmux")))
|
|
301
|
+
continue;
|
|
302
|
+
const invocation = parseInvocation(tokens.slice(index + 1));
|
|
303
|
+
if (!invocation || !INPUT_VERBS.has(invocation.verb)) continue;
|
|
304
|
+
const assignedSocket = assignedTmuxSocket(tokens, index);
|
|
305
|
+
const commandSocket = invocation.socket ? socketPathFor(invocation.socket, env, cwd, uid) : assignedSocket;
|
|
306
|
+
if (commandSocket && !sameSocket(commandSocket, identity.socketPath)) continue;
|
|
307
|
+
|
|
308
|
+
const target = targetFromArgs(invocation.args);
|
|
309
|
+
if (target === undefined) {
|
|
310
|
+
return {
|
|
311
|
+
block: true,
|
|
312
|
+
reason: `Blocked: ${invocation.verb} without a target would inject keystrokes into this agent pane (${identity.paneId}).`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (target === identity.paneId) {
|
|
316
|
+
return {
|
|
317
|
+
block: true,
|
|
318
|
+
reason: `Blocked: ${invocation.verb} targets this agent pane (${identity.paneId}); injected bytes would become a forged user turn.`,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (!target.startsWith("%")) {
|
|
322
|
+
const resolved = await resolvePaneId({
|
|
323
|
+
socketArgs: invocation.socketArgs,
|
|
324
|
+
target,
|
|
325
|
+
env: { ...env, TMUX: `${identity.socketPath},0,0` } as Record<string, string>,
|
|
326
|
+
});
|
|
327
|
+
if (resolved !== undefined && resolved !== identity.paneId) continue;
|
|
328
|
+
if (resolved === identity.paneId || resolved === undefined) {
|
|
329
|
+
return {
|
|
330
|
+
block: true,
|
|
331
|
+
reason:
|
|
332
|
+
resolved === identity.paneId
|
|
333
|
+
? `Blocked: ${invocation.verb} target ${target} resolves to this agent pane (${identity.paneId}); injected bytes would become a forged user turn.`
|
|
334
|
+
: `Blocked: could not verify that ${invocation.verb} target ${target} is a different pane on the current tmux server; refusing fail-closed.`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (depth < MAX_INDIRECTION_DEPTH) {
|
|
341
|
+
for (const payload of collectShellPayloads(tokens)) {
|
|
342
|
+
const result = await checkTmuxSelfInjection(payload, options, depth + 1);
|
|
343
|
+
if (result.block) return result;
|
|
344
|
+
}
|
|
345
|
+
for (const scriptPath of collectShellScripts(tokens, cwd)) {
|
|
346
|
+
try {
|
|
347
|
+
const file = Bun.file(scriptPath);
|
|
348
|
+
if (!(await file.exists()) || file.size > 1_000_000) continue;
|
|
349
|
+
const script = await file.text();
|
|
350
|
+
const result = await checkTmuxSelfInjection(script, options, depth + 1);
|
|
351
|
+
if (result.block) return result;
|
|
352
|
+
} catch {
|
|
353
|
+
// An unreadable or missing script is not itself evidence that it
|
|
354
|
+
// targets this pane; let the shell report the normal file error.
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return { block: false };
|
|
359
|
+
}
|
|
@@ -2319,7 +2319,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
|
|
|
2319
2319
|
"job": {
|
|
2320
2320
|
"name": "job",
|
|
2321
2321
|
"label": "Job",
|
|
2322
|
-
"description": "Inspects, waits, or cancels async jobs.\n\nBackground job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.\n\nIn the interactive TUI,
|
|
2322
|
+
"description": "Inspects, waits, or cancels async jobs.\n\nBackground job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.\n\nIn the interactive TUI, a running foreground command can be folded into a background job by pressing the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via `app.tool.backgroundFold`). This covers managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded job keeps its original deadline, is never killed or restarted by folding, and its result wakes a later turn automatically; a folded PTY continues output-only and no longer accepts input. `task`/`subagent` waits are not foldable. Raw shell `Ctrl+Z`/`bg` is not the supported path inside GJC because it bypasses job ownership and output-routing contracts.\n\n# Operations\n\n## `list: true`\nUse to inspect what's running.\n\n## `tail: [id, …]`\nShow the retained output buffer for one or more background jobs without waiting.\n- Use this to reopen/tail a backgrounded long-running bash/tool output after folding it away.\n- Output is bounded by the manager retention window; stale cursors may report that only the retained tail is available.\n- Prefer `tail` over polling when you only need to peek at progress, so the conversation can continue without flooding the TUI.\n\n## `poll: [id, …]`\nBlock until the specified jobs finish or the wait window (~30 s, not configurable) elapses.\n- Use when you are genuinely blocked on a result and have no other work to do.\n- Returns the current snapshot when the timer elapses; running jobs remain running.\n- Completed jobs include their final output in the returned snapshot.\n\n## `cancel: [id, …]`\nStop running jobs.\n- Use when a job is stalled, hung, or no longer needed.\n- Returns immediately after cancelling.",
|
|
2323
2323
|
"parameters": {
|
|
2324
2324
|
"type": "object",
|
|
2325
2325
|
"properties": {
|