@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.1
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 +110 -0
- package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
- package/dist/cli.js +10009 -9654
- package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
- package/dist/types/advisor/runtime.d.ts +8 -2
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/session-picker.d.ts +14 -7
- package/dist/types/collab/guest.d.ts +21 -2
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/model-discovery.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +73 -8
- package/dist/types/cursor-bridge-tools.d.ts +54 -0
- package/dist/types/cursor.d.ts +193 -8
- package/dist/types/edit/edit-clipboard.d.ts +19 -0
- package/dist/types/edit/hashline/diff.d.ts +9 -1
- package/dist/types/edit/index.d.ts +8 -1
- package/dist/types/edit/streaming.d.ts +8 -1
- package/dist/types/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- package/dist/types/internal-urls/security-protocol.d.ts +15 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
- package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
- package/dist/types/launch/terminal-output-worker.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +33 -4
- package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
- package/dist/types/modes/components/read-tool-group.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +6 -0
- package/dist/types/modes/components/status-line/component.d.ts +23 -0
- package/dist/types/modes/components/tool-execution.d.ts +16 -7
- package/dist/types/modes/components/usage-row.d.ts +2 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +4 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/types/modes/theme/theme.d.ts +12 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/sdk.d.ts +18 -0
- package/dist/types/security/auth.d.ts +30 -0
- package/dist/types/security/cloud.d.ts +79 -0
- package/dist/types/security/comparison.d.ts +49 -0
- package/dist/types/security/contracts/ids.d.ts +15 -0
- package/dist/types/security/contracts/index.d.ts +4 -0
- package/dist/types/security/contracts/schemas.d.ts +660 -0
- package/dist/types/security/contracts/types.d.ts +234 -0
- package/dist/types/security/contracts/validation.d.ts +5 -0
- package/dist/types/security/coordinator.d.ts +100 -0
- package/dist/types/security/importers/codex-security.d.ts +7 -0
- package/dist/types/security/importers/index.d.ts +2 -0
- package/dist/types/security/importers/sarif.d.ts +9 -0
- package/dist/types/security/index.d.ts +13 -0
- package/dist/types/security/preflight.d.ts +61 -0
- package/dist/types/security/provenance.d.ts +24 -0
- package/dist/types/security/publication.d.ts +78 -0
- package/dist/types/security/remediation.d.ts +27 -0
- package/dist/types/security/resource-output.d.ts +8 -0
- package/dist/types/security/sarif.d.ts +2 -0
- package/dist/types/security/store.d.ts +40 -0
- package/dist/types/session/agent-session-events.d.ts +2 -0
- package/dist/types/session/agent-session-types.d.ts +36 -1
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/claude-session-store.d.ts +13 -0
- package/dist/types/session/codex-session-store.d.ts +14 -0
- package/dist/types/session/credential-pin.d.ts +58 -0
- package/dist/types/session/foreign-session-import.d.ts +15 -0
- package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
- package/dist/types/session/foreign-session-store.d.ts +23 -0
- package/dist/types/session/session-advisors.d.ts +47 -2
- package/dist/types/session/session-entries.d.ts +19 -1
- package/dist/types/session/session-handoff.d.ts +4 -1
- package/dist/types/session/session-loader.d.ts +2 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-tools.d.ts +25 -1
- package/dist/types/session/turn-recovery.d.ts +10 -4
- package/dist/types/slash-commands/helpers/security.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +4 -1
- package/dist/types/task/executor.d.ts +14 -2
- package/dist/types/task/render.d.ts +7 -0
- package/dist/types/thinking.d.ts +18 -6
- package/dist/types/tools/acp-bridge.d.ts +28 -5
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/grep.d.ts +17 -1
- package/dist/types/tools/hub/launch.d.ts +5 -1
- package/dist/types/tools/index.d.ts +10 -2
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/security-scan.d.ts +96 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/utils/changelog.d.ts +19 -0
- package/dist/types/utils/git.d.ts +10 -0
- package/dist/types/utils/jj.d.ts +11 -4
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/scripts/security-compare.ts +40 -0
- package/src/advisor/runtime.ts +138 -12
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cli/args.ts +6 -0
- package/src/cli/flag-tables.ts +2 -0
- package/src/cli/gallery-fixtures/fs.ts +25 -0
- package/src/cli/session-picker.ts +34 -17
- package/src/cli/ttsr-cli.ts +19 -1
- package/src/cli.ts +9 -0
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-discovery.ts +32 -5
- package/src/config/model-registry.ts +107 -25
- package/src/config/settings-schema.ts +77 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/hashline/diff.ts +49 -10
- package/src/edit/hashline/execute.ts +38 -6
- package/src/edit/hashline/filesystem.ts +27 -3
- package/src/edit/index.ts +12 -2
- package/src/edit/renderer.ts +4 -4
- package/src/edit/streaming.ts +17 -2
- package/src/eval/py/prelude.py +7 -3
- package/src/export/html/template.js +1 -1
- package/src/extensibility/extensions/runner.ts +70 -0
- package/src/extensibility/extensions/types.ts +30 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/internal-urls/router.ts +4 -1
- package/src/internal-urls/security-protocol.ts +261 -0
- package/src/internal-urls/types.ts +1 -1
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/lsp/client.ts +18 -3
- package/src/lsp/index.ts +7 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +127 -56
- package/src/mcp/manager.ts +120 -18
- package/src/modes/acp/acp-agent.ts +15 -9
- package/src/modes/components/chat-transcript-builder.ts +30 -11
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/read-tool-group.ts +159 -17
- package/src/modes/components/session-selector.ts +19 -3
- package/src/modes/components/status-line/component.ts +381 -54
- package/src/modes/components/tool-execution.ts +78 -31
- package/src/modes/components/usage-row.ts +10 -5
- package/src/modes/controllers/event-controller.ts +26 -4
- package/src/modes/controllers/mcp-command-controller.ts +68 -3
- package/src/modes/controllers/selector-controller.ts +94 -36
- package/src/modes/controllers/tan-command-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +31 -17
- package/src/modes/rpc/host-uris.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +18 -0
- package/src/modes/rpc/rpc-mode.ts +16 -1
- package/src/modes/rpc/rpc-types.ts +11 -0
- package/src/modes/theme/theme.ts +151 -6
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +38 -21
- package/src/prompts/agents/security-reviewer.md +75 -0
- package/src/prompts/security/scan-coordinator.md +7 -0
- package/src/prompts/security/scan-request.md +21 -0
- package/src/prompts/security/validate-request.md +8 -0
- package/src/prompts/system/auto-thinking-difficulty.md +4 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/grep.md +7 -6
- package/src/prompts/tools/security-publish.md +1 -0
- package/src/prompts/tools/security-scan.md +1 -0
- package/src/sdk.ts +212 -21
- package/src/security/auth.ts +98 -0
- package/src/security/cloud.ts +686 -0
- package/src/security/comparison.ts +255 -0
- package/src/security/contracts/ids.ts +111 -0
- package/src/security/contracts/index.ts +4 -0
- package/src/security/contracts/schemas.ts +201 -0
- package/src/security/contracts/types.ts +254 -0
- package/src/security/contracts/validation.ts +65 -0
- package/src/security/coordinator.ts +708 -0
- package/src/security/importers/codex-security.ts +387 -0
- package/src/security/importers/index.ts +2 -0
- package/src/security/importers/sarif.ts +357 -0
- package/src/security/index.ts +13 -0
- package/src/security/preflight.ts +405 -0
- package/src/security/provenance.ts +106 -0
- package/src/security/publication.ts +326 -0
- package/src/security/remediation.ts +93 -0
- package/src/security/resource-output.ts +50 -0
- package/src/security/sarif.ts +78 -0
- package/src/security/store.ts +430 -0
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +302 -175
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/model-controls.ts +11 -3
- package/src/session/session-advisors.ts +140 -16
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-loader.ts +25 -9
- package/src/session/session-maintenance.ts +59 -11
- package/src/session/session-manager.ts +61 -0
- package/src/session/session-tools.ts +107 -5
- package/src/session/settings-stream-fn.ts +1 -0
- package/src/session/turn-recovery.ts +97 -51
- package/src/slash-commands/builtin-registry.ts +152 -3
- package/src/slash-commands/helpers/security.ts +451 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +20 -4
- package/src/task/render.ts +20 -4
- package/src/task/structured-subagent.ts +6 -4
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/browser.ts +5 -0
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +37 -5
- package/src/tools/path-utils.ts +91 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/security-scan.ts +287 -0
- package/src/tools/write.ts +9 -3
- package/src/tools/xdev.ts +39 -14
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/web/search/providers/codex.ts +34 -0
- package/src/web/search/types.ts +1 -1
|
@@ -373,10 +373,12 @@ function buildExecutorOptions(
|
|
|
373
373
|
getArtifactsDir: session.getArtifactsDir ?? (() => null),
|
|
374
374
|
getSessionId: session.getSessionId ?? (() => null),
|
|
375
375
|
};
|
|
376
|
-
const
|
|
376
|
+
const restrictToolNames = policy.planMode || session.restrictToolNames === true;
|
|
377
|
+
const enableMCP = !restrictToolNames && (session.enableMCP ?? true);
|
|
377
378
|
return {
|
|
378
379
|
cwd: session.cwd,
|
|
379
380
|
additionalDirectories: session.additionalDirectories,
|
|
381
|
+
getApiKey: session.getApiKey,
|
|
380
382
|
agent: policy.effectiveAgent,
|
|
381
383
|
task: renderSubagentPrompt(request.assignment),
|
|
382
384
|
assignment: request.assignment.trim(),
|
|
@@ -408,7 +410,7 @@ function buildExecutorOptions(
|
|
|
408
410
|
enableLsp: policy.enableLsp,
|
|
409
411
|
enableIrc: policy.enableIrc,
|
|
410
412
|
maxRuntimeMs: request.maxRuntimeMs,
|
|
411
|
-
restrictToolNames
|
|
413
|
+
restrictToolNames,
|
|
412
414
|
keepAlive: request.keepAlive,
|
|
413
415
|
signal: request.signal,
|
|
414
416
|
eventBus: session.eventBus,
|
|
@@ -424,8 +426,8 @@ function buildExecutorOptions(
|
|
|
424
426
|
workspaceTree: session.workspaceTree,
|
|
425
427
|
promptTemplates: session.promptTemplates,
|
|
426
428
|
rules: session.rules,
|
|
427
|
-
preloadedExtensionPaths:
|
|
428
|
-
preloadedCustomToolPaths:
|
|
429
|
+
preloadedExtensionPaths: restrictToolNames ? [] : session.extensionPaths,
|
|
430
|
+
preloadedCustomToolPaths: restrictToolNames ? [] : session.customToolPaths,
|
|
429
431
|
localProtocolOptions,
|
|
430
432
|
parentArtifactManager: session.getArtifactManager?.() ?? undefined,
|
|
431
433
|
parentHindsightSessionState: session.getHindsightSessionState?.(),
|
package/src/thinking.ts
CHANGED
|
@@ -191,7 +191,7 @@ export interface ConfiguredThinkingLevelMetadata {
|
|
|
191
191
|
const AUTO_THINKING_METADATA: ConfiguredThinkingLevelMetadata = {
|
|
192
192
|
value: AUTO_THINKING,
|
|
193
193
|
label: "auto",
|
|
194
|
-
description: "Auto-detect per prompt
|
|
194
|
+
description: "Auto-detect per prompt",
|
|
195
195
|
};
|
|
196
196
|
|
|
197
197
|
/**
|
|
@@ -236,6 +236,12 @@ export function parseCliThinkingLevel(value: string | null | undefined): Configu
|
|
|
236
236
|
* above Low (falling back to the full supported set only when the model maxes
|
|
237
237
|
* out below Low). Within that pool the request snaps to the highest level not
|
|
238
238
|
* exceeding it, or the pool minimum when the request is below the pool.
|
|
239
|
+
* `ceiling` bounds the pool from above, so a policy ceiling survives the model
|
|
240
|
+
* clamp: a sparse ladder such as `["max"]` must not snap an `xhigh` request up
|
|
241
|
+
* to `max`. The Low floor is resolved against the model's own ladder *before*
|
|
242
|
+
* the ceiling applies — a ceiling that hides every tier at or above Low means
|
|
243
|
+
* there is nothing legal to pick (`undefined`), not a licence to fall through
|
|
244
|
+
* to a sub-Low tier the model happens to expose.
|
|
239
245
|
*
|
|
240
246
|
* Returns `undefined` for reasoning-capable models without a controllable
|
|
241
247
|
* effort surface (`thinking.efforts` empty — e.g. devin-agent models, where
|
|
@@ -244,12 +250,19 @@ export function parseCliThinkingLevel(value: string | null | undefined): Configu
|
|
|
244
250
|
* forward a concrete effort that would then trip {@link requireSupportedEffort}
|
|
245
251
|
* downstream.
|
|
246
252
|
*/
|
|
247
|
-
export function clampAutoThinkingEffort(
|
|
253
|
+
export function clampAutoThinkingEffort(
|
|
254
|
+
model: Model | undefined,
|
|
255
|
+
effort: Effort,
|
|
256
|
+
ceiling: Effort = Effort.Max,
|
|
257
|
+
): Effort | undefined {
|
|
248
258
|
const supported = model ? getSupportedEfforts(model) : THINKING_EFFORTS;
|
|
249
259
|
if (supported.length === 0) return undefined;
|
|
250
260
|
const lowIndex = THINKING_EFFORTS.indexOf(Effort.Low);
|
|
251
|
-
const
|
|
252
|
-
const
|
|
261
|
+
const ceilingIndex = THINKING_EFFORTS.indexOf(ceiling);
|
|
262
|
+
const atOrAboveLow = supported.filter(level => THINKING_EFFORTS.indexOf(level) >= lowIndex);
|
|
263
|
+
const floored = atOrAboveLow.length > 0 ? atOrAboveLow : supported;
|
|
264
|
+
const pool = floored.filter(level => THINKING_EFFORTS.indexOf(level) <= ceilingIndex);
|
|
265
|
+
if (pool.length === 0) return undefined;
|
|
253
266
|
const requestedIndex = THINKING_EFFORTS.indexOf(effort);
|
|
254
267
|
let chosen = pool[0];
|
|
255
268
|
for (const candidate of pool) {
|
|
@@ -351,14 +364,20 @@ export function modelSupportsEffortCeiling(model: Model, ceiling: Effort): boole
|
|
|
351
364
|
|
|
352
365
|
/**
|
|
353
366
|
* The provisional concrete level shown while `auto` is configured but before a
|
|
354
|
-
* turn has been classified
|
|
355
|
-
* clamped into the auto range.
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
367
|
+
* turn has been classified, and the fallback when classification fails. Prefers
|
|
368
|
+
* the model's `defaultLevel`, otherwise High, clamped into the auto range.
|
|
369
|
+
*
|
|
370
|
+
* Deliberately stays below {@link Effort.Max}: the placeholder must not bill the
|
|
371
|
+
* top tier for a turn nobody classified, so XHigh is passed as a hard ceiling
|
|
372
|
+
* rather than only capping the preferred level — otherwise a sparse `["max"]`
|
|
373
|
+
* ladder would snap straight back up. A model whose ladder offers nothing at or
|
|
374
|
+
* below XHigh therefore has no provisional level, and `auto` leaves the current
|
|
375
|
+
* one in place. Classification itself may still resolve Max on models that
|
|
376
|
+
* expose the tier when the user opts in. Returns `undefined` for non-reasoning
|
|
377
|
+
* models.
|
|
359
378
|
*/
|
|
360
379
|
export function resolveProvisionalAutoLevel(model: Model | undefined): Effort | undefined {
|
|
361
380
|
if (!model?.reasoning) return undefined;
|
|
362
381
|
const preferred = model.thinking?.defaultLevel ?? Effort.High;
|
|
363
|
-
return clampAutoThinkingEffort(model, preferred === Effort.Max ? Effort.XHigh : preferred);
|
|
382
|
+
return clampAutoThinkingEffort(model, preferred === Effort.Max ? Effort.XHigh : preferred, Effort.XHigh);
|
|
364
383
|
}
|
package/src/tools/acp-bridge.ts
CHANGED
|
@@ -40,15 +40,39 @@ export function shouldRouteWriteThroughBridge(
|
|
|
40
40
|
return absolutePath !== resolvePlanPath(session, state.planFilePath);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Result of a bridge-routed write: the content actually verified on disk
|
|
45
|
+
* after the client processed the write, plus whether that content diverges
|
|
46
|
+
* from what the tool asked to persist.
|
|
47
|
+
*
|
|
48
|
+
* ACP's `fs/write_text_file` has no "verbatim, no side effects" guarantee —
|
|
49
|
+
* a client (e.g. Zed with `format_on_save: on`) may reformat the buffer as
|
|
50
|
+
* part of handling the write before it settles on disk. Silently trusting
|
|
51
|
+
* the requested `content` as "what's now on disk" lets that drift poison
|
|
52
|
+
* every snapshot/tag/hash a caller derives from the write, which then reads
|
|
53
|
+
* back as unrelated whole-file corruption on the *next* edit. Reading the
|
|
54
|
+
* file back and reporting what's actually there keeps callers honest.
|
|
55
|
+
*/
|
|
56
|
+
export interface BridgeWriteResult {
|
|
57
|
+
/** Content actually present on disk immediately after the bridge write. */
|
|
58
|
+
text: string;
|
|
59
|
+
/** `true` when `text` differs from the content the tool asked to write. */
|
|
60
|
+
driftedFromRequest: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
43
63
|
/**
|
|
44
64
|
* Try to route a file write through the ACP client bridge.
|
|
45
65
|
*
|
|
46
66
|
* Performs the full guard check, bridge call (wrapped in {@link ToolError}),
|
|
47
|
-
*
|
|
67
|
+
* a post-write read-back to detect client-side transformation (e.g.
|
|
68
|
+
* format-on-save), FS-scan cache invalidation, and session mutation-version
|
|
69
|
+
* bump.
|
|
48
70
|
*
|
|
49
|
-
* Returns `
|
|
50
|
-
*
|
|
51
|
-
*
|
|
71
|
+
* Returns `undefined` when the bridge is unavailable or the path should not
|
|
72
|
+
* be routed through it — the caller must fall back to the writethrough path.
|
|
73
|
+
* Returns a {@link BridgeWriteResult} when the bridge was used; callers MUST
|
|
74
|
+
* use `result.text` (not the content they requested) for any snapshot, hash,
|
|
75
|
+
* or tag derived from this write.
|
|
52
76
|
*/
|
|
53
77
|
export async function routeWriteThroughBridge(
|
|
54
78
|
session: ToolSession,
|
|
@@ -56,11 +80,11 @@ export async function routeWriteThroughBridge(
|
|
|
56
80
|
absolutePath: string,
|
|
57
81
|
content: string,
|
|
58
82
|
signal?: AbortSignal,
|
|
59
|
-
): Promise<
|
|
60
|
-
if (!shouldRouteWriteThroughBridge(session, requestedPath, absolutePath)) return
|
|
83
|
+
): Promise<BridgeWriteResult | undefined> {
|
|
84
|
+
if (!shouldRouteWriteThroughBridge(session, requestedPath, absolutePath)) return undefined;
|
|
61
85
|
|
|
62
86
|
const bridge = session.getClientBridge?.();
|
|
63
|
-
if (!bridge?.capabilities.writeTextFile || !bridge.writeTextFile) return
|
|
87
|
+
if (!bridge?.capabilities.writeTextFile || !bridge.writeTextFile) return undefined;
|
|
64
88
|
|
|
65
89
|
const changeType = (await Bun.file(absolutePath).exists()) ? FileChangeType.Changed : FileChangeType.Created;
|
|
66
90
|
// The ACP protocol has no cancellation for fs writes; the most we can do is
|
|
@@ -77,5 +101,25 @@ export async function routeWriteThroughBridge(
|
|
|
77
101
|
}
|
|
78
102
|
invalidateFsScanAfterWrite(absolutePath);
|
|
79
103
|
session.bumpFileMutationVersion?.(absolutePath);
|
|
80
|
-
|
|
104
|
+
|
|
105
|
+
// Best-effort verification: the client already flushed the write (that's
|
|
106
|
+
// the whole point of `fs/write_text_file`), so the file on disk reflects
|
|
107
|
+
// whatever the client actually persisted, formatter and all. If the
|
|
108
|
+
// read-back itself fails, fall back to trusting `content` rather than
|
|
109
|
+
// failing an otherwise-successful write. This is a best-effort signal,
|
|
110
|
+
// not a guarantee: ACP defines no ordering between a client acking the
|
|
111
|
+
// write and its own async format-on-save settling, so a client that acks
|
|
112
|
+
// before its formatter runs will look verbatim here. That degrades to
|
|
113
|
+
// the pre-fix behavior for THIS write, but never corrupts state: the
|
|
114
|
+
// next `readText` still observes whatever the client eventually settles
|
|
115
|
+
// on, and a real desync there surfaces as an honest stale-tag error
|
|
116
|
+
// instead of a silently wrong tag.
|
|
117
|
+
let actualText = content;
|
|
118
|
+
try {
|
|
119
|
+
actualText = await Bun.file(absolutePath).text();
|
|
120
|
+
} catch {
|
|
121
|
+
// Unreadable right after a reported-successful write; nothing more we
|
|
122
|
+
// can verify here.
|
|
123
|
+
}
|
|
124
|
+
return { text: actualText, driftedFromRequest: actualText !== content };
|
|
81
125
|
}
|
package/src/tools/browser.ts
CHANGED
|
@@ -95,6 +95,11 @@ function resolveBrowserKind(params: BrowserParams, session: ToolSession): Browse
|
|
|
95
95
|
const exe = resolveToCwd(app.path, session.cwd);
|
|
96
96
|
return { kind: "spawned", path: exe };
|
|
97
97
|
}
|
|
98
|
+
// A configured endpoint is a default, not an override: explicit app options win.
|
|
99
|
+
const configuredCdpUrl = (session.settings.get("browser.cdpUrl") as string | undefined)?.trim();
|
|
100
|
+
if (configuredCdpUrl) {
|
|
101
|
+
return { kind: "connected", cdpUrl: configuredCdpUrl.replace(/\/+$/, "") };
|
|
102
|
+
}
|
|
98
103
|
const cmuxKind = resolveCmuxKind({
|
|
99
104
|
settingEnabled: session.settings.get("browser.cmux") as boolean | undefined,
|
|
100
105
|
});
|
package/src/tools/checkpoint.ts
CHANGED
|
@@ -50,11 +50,6 @@ export interface RewindToolDetails {
|
|
|
50
50
|
meta?: OutputMeta;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function isTopLevelSession(session: ToolSession): boolean {
|
|
54
|
-
const depth = session.taskDepth;
|
|
55
|
-
return depth === undefined || depth === 0;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
53
|
export class CheckpointTool implements AgentTool<typeof checkpointSchema, CheckpointToolDetails> {
|
|
59
54
|
readonly name = "checkpoint";
|
|
60
55
|
readonly approval = "read" as const;
|
|
@@ -71,7 +66,6 @@ export class CheckpointTool implements AgentTool<typeof checkpointSchema, Checkp
|
|
|
71
66
|
}
|
|
72
67
|
|
|
73
68
|
static createIf(session: ToolSession): CheckpointTool | null {
|
|
74
|
-
if (!isTopLevelSession(session)) return null;
|
|
75
69
|
return new CheckpointTool(session);
|
|
76
70
|
}
|
|
77
71
|
|
|
@@ -82,9 +76,6 @@ export class CheckpointTool implements AgentTool<typeof checkpointSchema, Checkp
|
|
|
82
76
|
_onUpdate?: AgentToolUpdateCallback<CheckpointToolDetails>,
|
|
83
77
|
_context?: AgentToolContext,
|
|
84
78
|
): Promise<AgentToolResult<CheckpointToolDetails>> {
|
|
85
|
-
if (!isTopLevelSession(this.session)) {
|
|
86
|
-
throw new ToolError("Checkpoint not available in subagents.");
|
|
87
|
-
}
|
|
88
79
|
if (this.session.getCheckpointState?.()) {
|
|
89
80
|
throw new ToolError("Checkpoint already active.");
|
|
90
81
|
}
|
|
@@ -117,7 +108,6 @@ export class RewindTool implements AgentTool<typeof rewindSchema, RewindToolDeta
|
|
|
117
108
|
}
|
|
118
109
|
|
|
119
110
|
static createIf(session: ToolSession): RewindTool | null {
|
|
120
|
-
if (!isTopLevelSession(session)) return null;
|
|
121
111
|
return new RewindTool(session);
|
|
122
112
|
}
|
|
123
113
|
|
|
@@ -128,9 +118,6 @@ export class RewindTool implements AgentTool<typeof rewindSchema, RewindToolDeta
|
|
|
128
118
|
_onUpdate?: AgentToolUpdateCallback<RewindToolDetails>,
|
|
129
119
|
_context?: AgentToolContext,
|
|
130
120
|
): Promise<AgentToolResult<RewindToolDetails>> {
|
|
131
|
-
if (!isTopLevelSession(this.session)) {
|
|
132
|
-
throw new ToolError("Checkpoint not available in subagents.");
|
|
133
|
-
}
|
|
134
121
|
if (!this.session.getCheckpointState?.()) {
|
|
135
122
|
if (this.session.getLastCompletedRewind?.()) {
|
|
136
123
|
throw new ToolError(
|
package/src/tools/grep.ts
CHANGED
|
@@ -884,6 +884,22 @@ export interface GrepToolDetails {
|
|
|
884
884
|
|
|
885
885
|
type SearchParams = typeof searchSchema.infer;
|
|
886
886
|
|
|
887
|
+
/**
|
|
888
|
+
* Construction-time overrides for callers that are not the model.
|
|
889
|
+
*
|
|
890
|
+
* The model-facing schema deliberately does not grow these: they exist for
|
|
891
|
+
* wire bridges (the Cursor `pi_grep` frame) whose protocol carries an explicit
|
|
892
|
+
* context width and total match cap, and which would otherwise have to drop
|
|
893
|
+
* them. Unset means "use the session settings / built-in caps" — the behavior
|
|
894
|
+
* every model-issued call keeps.
|
|
895
|
+
*/
|
|
896
|
+
export interface GrepToolOptions {
|
|
897
|
+
/** Overrides `grep.contextBefore`/`grep.contextAfter` for every call on this instance. */
|
|
898
|
+
context?: number;
|
|
899
|
+
/** Caps total surfaced matches. Applied on top of the built-in per-file and file-window caps, never above them. */
|
|
900
|
+
totalMatchLimit?: number;
|
|
901
|
+
}
|
|
902
|
+
|
|
887
903
|
export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails> {
|
|
888
904
|
readonly name = "grep";
|
|
889
905
|
readonly approval = (args: unknown): ToolTier => {
|
|
@@ -897,7 +913,17 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
897
913
|
readonly parameters = searchSchema;
|
|
898
914
|
readonly strict = true;
|
|
899
915
|
|
|
900
|
-
|
|
916
|
+
readonly #contextOverride?: number;
|
|
917
|
+
readonly #totalMatchLimit?: number;
|
|
918
|
+
|
|
919
|
+
constructor(
|
|
920
|
+
private readonly session: ToolSession,
|
|
921
|
+
options?: GrepToolOptions,
|
|
922
|
+
) {
|
|
923
|
+
const context = options?.context;
|
|
924
|
+
this.#contextOverride = context !== undefined ? Math.max(0, Math.floor(context)) : undefined;
|
|
925
|
+
const total = options?.totalMatchLimit;
|
|
926
|
+
this.#totalMatchLimit = total !== undefined ? Math.max(1, Math.floor(total)) : undefined;
|
|
901
927
|
const displayMode = resolveFileDisplayMode(session);
|
|
902
928
|
this.description = prompt.render(grepDescription, {
|
|
903
929
|
IS_HL_MODE: displayMode.hashLines,
|
|
@@ -978,8 +1004,8 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
978
1004
|
`or pass a UTF-8 text member.`,
|
|
979
1005
|
);
|
|
980
1006
|
}
|
|
981
|
-
const normalizedContextBefore = this.session.settings.get("grep.contextBefore");
|
|
982
|
-
const normalizedContextAfter = this.session.settings.get("grep.contextAfter");
|
|
1007
|
+
const normalizedContextBefore = this.#contextOverride ?? this.session.settings.get("grep.contextBefore");
|
|
1008
|
+
const normalizedContextAfter = this.#contextOverride ?? this.session.settings.get("grep.contextAfter");
|
|
983
1009
|
const ignoreCase = !(caseSensitive ?? true);
|
|
984
1010
|
const useGitignore = gitignore ?? true;
|
|
985
1011
|
const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
|
|
@@ -1297,9 +1323,22 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1297
1323
|
// Single-file scopes can't paginate — there is one file by definition.
|
|
1298
1324
|
const canPaginate = isMultiScope;
|
|
1299
1325
|
const skipFiles = canPaginate ? Math.min(normalizedSkip, totalFiles) : 0;
|
|
1300
|
-
|
|
1301
|
-
|
|
1326
|
+
// A caller with a total match cap is not paginating: the cap bounds the
|
|
1327
|
+
// output, and the only consumer that sets one (`pi_grep`) has no `skip`
|
|
1328
|
+
// field to follow a "use skip=N" suggestion with. Windowing it to the
|
|
1329
|
+
// first 20 files would silently return fewer matches than it asked for
|
|
1330
|
+
// while reporting the cap as unreached.
|
|
1331
|
+
//
|
|
1332
|
+
// The window is cap+1 files, not cap: with one match per file, a cap
|
|
1333
|
+
// of N over exactly N files is complete, while over N+1 files it is
|
|
1334
|
+
// clipped — and only reading that extra file distinguishes the two.
|
|
1335
|
+
// The cap below then does the trimming and records that it bit, so
|
|
1336
|
+
// `match_limit_reached` reaches the frame set.
|
|
1337
|
+
const fileWindow = this.#totalMatchLimit !== undefined ? this.#totalMatchLimit + 1 : DEFAULT_FILE_LIMIT;
|
|
1338
|
+
const windowFiles = canPaginate ? fileOrder.slice(skipFiles, skipFiles + fileWindow) : fileOrder;
|
|
1339
|
+
const fileLimitReached = canPaginate && totalFiles > skipFiles + fileWindow;
|
|
1302
1340
|
const selectedMatches: GrepMatch[] = [];
|
|
1341
|
+
let totalMatchLimitReached = false;
|
|
1303
1342
|
if (windowFiles.length > 0) {
|
|
1304
1343
|
const lists = windowFiles.map(file => matchesByPath.get(file) ?? []);
|
|
1305
1344
|
const cursors = new Array<number>(lists.length).fill(0);
|
|
@@ -1313,6 +1352,14 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1313
1352
|
}
|
|
1314
1353
|
}
|
|
1315
1354
|
}
|
|
1355
|
+
// Round-robin above interleaves files for diversity, so the cap is
|
|
1356
|
+
// applied after selection rather than as a per-list bound: trimming
|
|
1357
|
+
// mid-rotation would silently favour whichever files sort first.
|
|
1358
|
+
const cap = this.#totalMatchLimit;
|
|
1359
|
+
if (cap !== undefined && selectedMatches.length > cap) {
|
|
1360
|
+
selectedMatches.length = cap;
|
|
1361
|
+
totalMatchLimitReached = true;
|
|
1362
|
+
}
|
|
1316
1363
|
}
|
|
1317
1364
|
const nextSkip = skipFiles + windowFiles.length;
|
|
1318
1365
|
const limitMessage = fileLimitReached
|
|
@@ -1503,7 +1550,12 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1503
1550
|
const output = truncation.content;
|
|
1504
1551
|
const displayText = displayLines.join("\n");
|
|
1505
1552
|
const truncated = Boolean(
|
|
1506
|
-
fileLimitReached ||
|
|
1553
|
+
fileLimitReached ||
|
|
1554
|
+
perFileLimitReached ||
|
|
1555
|
+
totalMatchLimitReached ||
|
|
1556
|
+
result.limitReached ||
|
|
1557
|
+
truncation.truncated ||
|
|
1558
|
+
linesTruncated,
|
|
1507
1559
|
);
|
|
1508
1560
|
const details: GrepToolDetails = {
|
|
1509
1561
|
scopePath,
|
|
@@ -1517,8 +1569,12 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1517
1569
|
count: fileMatchCounts.get(path) ?? 0,
|
|
1518
1570
|
})),
|
|
1519
1571
|
truncated,
|
|
1520
|
-
fileLimitReached: fileLimitReached ?
|
|
1521
|
-
perFileLimitReached:
|
|
1572
|
+
fileLimitReached: fileLimitReached ? fileWindow : undefined,
|
|
1573
|
+
perFileLimitReached: totalMatchLimitReached
|
|
1574
|
+
? this.#totalMatchLimit
|
|
1575
|
+
: perFileLimitReached
|
|
1576
|
+
? perFileMatchCap
|
|
1577
|
+
: undefined,
|
|
1522
1578
|
displayContent: displayText,
|
|
1523
1579
|
missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
|
|
1524
1580
|
};
|
package/src/tools/hub/launch.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
|
12
12
|
import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
|
|
13
13
|
import { daemonClientForProject } from "../../launch/client";
|
|
14
14
|
import type { DaemonOperation, DaemonRpcResult, DaemonSnapshot, DaemonSpec, DaemonState } from "../../launch/protocol";
|
|
15
|
-
import
|
|
15
|
+
import { renderTerminalOutputIsolated } from "../../launch/terminal-output-worker-client";
|
|
16
16
|
import type { Theme, ThemeColor } from "../../modes/theme/theme";
|
|
17
17
|
import { framedBlock, outputBlockContentWidth, renderStatusLine } from "../../tui";
|
|
18
18
|
import type { ToolSession } from "..";
|
|
@@ -271,15 +271,14 @@ function toolContent(result: DaemonRpcResult, params: LaunchParams): string {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return runtime.renderTerminalOutput(terminalText, {
|
|
274
|
+
/** Resolve display rows while keeping legacy raw replay outside the client process. */
|
|
275
|
+
export async function renderLaunchLogTerminalRows(
|
|
276
|
+
result: Extract<DaemonRpcResult, { op: "logs" }>,
|
|
277
|
+
params: Pick<LaunchParams, "head" | "lines">,
|
|
278
|
+
): Promise<string[] | undefined> {
|
|
279
|
+
if (result.terminalRows !== undefined) return result.terminalRows;
|
|
280
|
+
if (result.terminalText === undefined) return undefined;
|
|
281
|
+
return renderTerminalOutputIsolated(result.terminalText, {
|
|
283
282
|
head: params.head ?? false,
|
|
284
283
|
maxRows: Math.min(1_000, Math.floor(params.lines ?? 100)),
|
|
285
284
|
});
|
|
@@ -297,11 +296,7 @@ async function toolDetails(result: DaemonRpcResult, params: LaunchParams): Promi
|
|
|
297
296
|
cursor: result.cursor,
|
|
298
297
|
timedOut: result.timedOut,
|
|
299
298
|
state: result.state,
|
|
300
|
-
terminalRows:
|
|
301
|
-
result.terminalRows ??
|
|
302
|
-
(result.terminalText === undefined
|
|
303
|
-
? undefined
|
|
304
|
-
: await renderLegacyTerminalOutput(result.terminalText, params)),
|
|
299
|
+
terminalRows: await renderLaunchLogTerminalRows(result, params).catch(() => undefined),
|
|
305
300
|
};
|
|
306
301
|
case "wait":
|
|
307
302
|
return { op: "wait", daemon: result.daemon, timedOut: result.timedOut, matched: result.matched };
|
package/src/tools/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
2
|
-
import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
1
|
+
import type { Clipboard, InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
2
|
+
import type { AgentOptions, AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { FetchImpl, ImageContent, Model, ServiceTierByFamily, ToolChoice } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import type { AsyncJobManager } from "../async/job-manager";
|
|
@@ -60,6 +60,7 @@ import { MemoryRetainTool } from "./memory-retain";
|
|
|
60
60
|
import { wrapToolWithMetaNotice } from "./output-meta";
|
|
61
61
|
import { ReadTool } from "./read";
|
|
62
62
|
import type { PlanProposalHandler } from "./resolve";
|
|
63
|
+
import { SecurityScanTool } from "./security-scan";
|
|
63
64
|
import { type TodoPhase, TodoTool } from "./todo";
|
|
64
65
|
import { WriteTool } from "./write";
|
|
65
66
|
import { isMountableUnderXdev, type XdevState } from "./xdev";
|
|
@@ -99,6 +100,7 @@ export * from "./read";
|
|
|
99
100
|
export * from "./report-tool-issue";
|
|
100
101
|
export * from "./resolve";
|
|
101
102
|
export * from "./review";
|
|
103
|
+
export * from "./security-scan";
|
|
102
104
|
export * from "./todo";
|
|
103
105
|
export * from "./tts";
|
|
104
106
|
export * from "./vibe";
|
|
@@ -162,6 +164,8 @@ export interface ToolSession {
|
|
|
162
164
|
suppressSpawnAdvisory?: boolean;
|
|
163
165
|
/** Optional fetch implementation injected into the URL read pipeline (tests, proxies). Defaults to global fetch. */
|
|
164
166
|
fetch?: FetchImpl;
|
|
167
|
+
/** Provider credential resolver forwarded unchanged to restricted child sessions. */
|
|
168
|
+
getApiKey?: AgentOptions["getApiKey"];
|
|
165
169
|
/** Skip subprocess-kernel availability checks and warmup */
|
|
166
170
|
skipPythonPreflight?: boolean;
|
|
167
171
|
/** Pre-loaded context files (AGENTS.md, etc) */
|
|
@@ -191,6 +195,8 @@ export interface ToolSession {
|
|
|
191
195
|
customToolPaths?: ToolPathWithSource[];
|
|
192
196
|
/** Whether LSP integrations are enabled */
|
|
193
197
|
enableLsp?: boolean;
|
|
198
|
+
/** Whether LSP is limited to navigation and diagnostics. */
|
|
199
|
+
lspReadOnly?: boolean;
|
|
194
200
|
/** Whether this invocation may expose IRC. `false` removes it even for subagents. */
|
|
195
201
|
enableIrc?: boolean;
|
|
196
202
|
/**
|
|
@@ -351,6 +357,10 @@ export interface ToolSession {
|
|
|
351
357
|
* file changed out-of-band. Lazily initialized by `getFileSnapshotStore`. */
|
|
352
358
|
fileSnapshotStore?: InMemorySnapshotStore;
|
|
353
359
|
|
|
360
|
+
/** Per-session `CUT`/`PASTE` clipboard register shared across edit
|
|
361
|
+
* calls. Lazily initialized by `getEditClipboard`. */
|
|
362
|
+
editClipboard?: Clipboard;
|
|
363
|
+
|
|
354
364
|
/** Per-session log of unresolved git merge conflict regions surfaced by
|
|
355
365
|
* `read`. Each entry gets a stable id N referenced by `write conflict://N`
|
|
356
366
|
* to splice the recorded region with replacement content. Lazily initialized
|
|
@@ -394,6 +404,7 @@ export type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool |
|
|
|
394
404
|
*/
|
|
395
405
|
export const BUILTIN_TOOLS: Record<BuiltinToolName, ToolFactory> = {
|
|
396
406
|
read: s => new ReadTool(s),
|
|
407
|
+
security_scan: s => new SecurityScanTool(s),
|
|
397
408
|
bash: s => new BashTool(s),
|
|
398
409
|
edit: s => new EditTool(s),
|
|
399
410
|
ast_grep: s => new AstGrepTool(s),
|
|
@@ -502,6 +513,18 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
502
513
|
// unreachable, in which case eval dispatches exclusively to the others.
|
|
503
514
|
const allowEval = effectivePythonAllowed || allowJs || effectiveRubyAllowed || effectiveJuliaAllowed;
|
|
504
515
|
|
|
516
|
+
// Checkpoint and rewind are a pair: listing one without the other strands
|
|
517
|
+
// the agent (it can checkpoint but not rewind, or vice versa). Auto-include
|
|
518
|
+
// the sister tool so a one-sided frontmatter `tools:` entry still works.
|
|
519
|
+
// Unlike the AST/auto-learn convenience auto-includes below, this is a
|
|
520
|
+
// safety pairing — it applies to restricted sessions too.
|
|
521
|
+
if (requestedTools && session.settings.get("checkpoint.enabled")) {
|
|
522
|
+
if (requestedTools.includes("checkpoint") && !requestedTools.includes("rewind")) {
|
|
523
|
+
requestedTools.push("rewind");
|
|
524
|
+
} else if (requestedTools.includes("rewind") && !requestedTools.includes("checkpoint")) {
|
|
525
|
+
requestedTools.push("checkpoint");
|
|
526
|
+
}
|
|
527
|
+
}
|
|
505
528
|
// Auto-include AST counterparts when their text-based sibling is present.
|
|
506
529
|
// Restricted callers own the active list and must not have it widened.
|
|
507
530
|
if (requestedTools && !restrictToolNames) {
|
|
@@ -571,10 +594,15 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
571
594
|
if (name === "ast_edit") return session.settings.get("astEdit.enabled");
|
|
572
595
|
if (name === "inspect_image") return isInspectImageToolActive(session);
|
|
573
596
|
if (name === "web_search") return session.settings.get("web_search.enabled");
|
|
597
|
+
if (name === "security_scan") return session.settings.get("security.enabled");
|
|
574
598
|
if (name === "ask") return session.settings.get("ask.enabled");
|
|
575
599
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
576
600
|
if (name === "computer") return session.settings.get("computer.enabled");
|
|
577
|
-
if (name === "checkpoint" || name === "rewind")
|
|
601
|
+
if (name === "checkpoint" || name === "rewind")
|
|
602
|
+
return (
|
|
603
|
+
session.settings.get("checkpoint.enabled") &&
|
|
604
|
+
((session.taskDepth ?? 0) === 0 || requestedTools !== undefined)
|
|
605
|
+
);
|
|
578
606
|
if (name === "hub") {
|
|
579
607
|
return (
|
|
580
608
|
!restrictToolNames && session.enableIrc !== false && isIrcEnabled(session.settings, session.taskDepth ?? 0)
|
|
@@ -584,11 +612,15 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
584
612
|
return ["hindsight", "mnemopi"].includes(session.settings.get("memory.backend") ?? "");
|
|
585
613
|
}
|
|
586
614
|
if (name === "memory_edit") return session.settings.get("memory.backend") === "mnemopi";
|
|
587
|
-
if (name === "manage_skill")
|
|
615
|
+
if (name === "manage_skill")
|
|
616
|
+
return (
|
|
617
|
+
session.settings.get("autolearn.enabled") &&
|
|
618
|
+
((session.taskDepth ?? 0) === 0 || requestedTools !== undefined)
|
|
619
|
+
);
|
|
588
620
|
if (name === "learn") {
|
|
589
621
|
return (
|
|
590
622
|
session.settings.get("autolearn.enabled") &&
|
|
591
|
-
(session.taskDepth ?? 0) === 0 &&
|
|
623
|
+
((session.taskDepth ?? 0) === 0 || requestedTools !== undefined) &&
|
|
592
624
|
["hindsight", "mnemopi", "local"].includes(session.settings.get("memory.backend") ?? "")
|
|
593
625
|
);
|
|
594
626
|
}
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -42,6 +42,7 @@ const INTERNAL_SCHEMES_WITH_SELECTORS: Record<string, true> = {
|
|
|
42
42
|
omp: true,
|
|
43
43
|
pr: true,
|
|
44
44
|
rule: true,
|
|
45
|
+
security: true,
|
|
45
46
|
skill: true,
|
|
46
47
|
ssh: true,
|
|
47
48
|
vault: true,
|
|
@@ -60,6 +61,7 @@ const TOP_LEVEL_INTERNAL_URL_PREFIXES = [
|
|
|
60
61
|
"artifact://",
|
|
61
62
|
"skill://",
|
|
62
63
|
"rule://",
|
|
64
|
+
"security://",
|
|
63
65
|
"local://",
|
|
64
66
|
"mcp://",
|
|
65
67
|
"ssh://",
|
|
@@ -117,6 +119,7 @@ function normalizeAtPrefix(filePath: string): string {
|
|
|
117
119
|
withoutAt.startsWith("artifact://") ||
|
|
118
120
|
withoutAt.startsWith("skill://") ||
|
|
119
121
|
withoutAt.startsWith("rule://") ||
|
|
122
|
+
withoutAt.startsWith("security://") ||
|
|
120
123
|
withoutAt.startsWith("local:") ||
|
|
121
124
|
withoutAt.startsWith("mcp://")
|
|
122
125
|
) {
|
|
@@ -520,6 +523,94 @@ export function resolveToCwd(filePath: string, cwd: string): string {
|
|
|
520
523
|
return path.resolve(cwd, expanded);
|
|
521
524
|
}
|
|
522
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Resolve a path that MUST stay inside `cwd`, or `null` when it would escape.
|
|
528
|
+
*
|
|
529
|
+
* {@link resolveToCwd} deliberately honors absolute paths, `~`, and `..` —
|
|
530
|
+
* correct for a path a user typed, wrong for one a remote peer supplied.
|
|
531
|
+
* Callers handling untrusted input (Cursor's `download_path`) use this instead:
|
|
532
|
+
* only a non-empty relative path landing under the live cwd is accepted, so
|
|
533
|
+
* neither `/etc/passwd` nor `../../escape` can be written through.
|
|
534
|
+
*
|
|
535
|
+
* The lexical check alone is not containment: a symlink inside the workspace
|
|
536
|
+
* can point anywhere, so `out/config` under a `ws/out -> /elsewhere` link is
|
|
537
|
+
* relative, `..`-free, and still writes outside. Both the target and its
|
|
538
|
+
* deepest existing ancestor are therefore realpath-resolved — the ancestor
|
|
539
|
+
* because a download names a file that does not exist yet, so the link in its
|
|
540
|
+
* path is the only thing that can be resolved before the write.
|
|
541
|
+
*
|
|
542
|
+
* The cwd itself is rejected: a download names a file, never the directory.
|
|
543
|
+
*/
|
|
544
|
+
export function confineToWorkspace(filePath: string, cwd: string): string | null {
|
|
545
|
+
if (!filePath || path.isAbsolute(filePath)) return null;
|
|
546
|
+
// `~` expands to an absolute path, and an internal URL is not a filesystem
|
|
547
|
+
// target at all; neither is a relative workspace path.
|
|
548
|
+
if (filePath.startsWith("~") || isInternalUrlPath(filePath)) return null;
|
|
549
|
+
const root = path.resolve(cwd);
|
|
550
|
+
const resolved = path.resolve(root, filePath);
|
|
551
|
+
if (!isUnderRootLexical(resolved, root)) return null;
|
|
552
|
+
|
|
553
|
+
// A workspace reached through a link of its own is legitimate (/tmp on
|
|
554
|
+
// macOS), so the real root is the comparison basis. An unresolvable root is
|
|
555
|
+
// not a workspace to contain anything in.
|
|
556
|
+
const realRoot = tryRealpath(root);
|
|
557
|
+
if (!realRoot) return null;
|
|
558
|
+
|
|
559
|
+
// An existing target is authoritative: resolve it outright.
|
|
560
|
+
const realTarget = tryRealpath(resolved);
|
|
561
|
+
if (realTarget) return isUnderRootLexical(realTarget, realRoot) ? resolved : null;
|
|
562
|
+
|
|
563
|
+
// `realpath` also fails on a *dangling* link, and a write follows that link
|
|
564
|
+
// to wherever it points. Chasing the chain to decide would mean
|
|
565
|
+
// reimplementing symlink resolution (multi-hop, relative hops, loops, and
|
|
566
|
+
// a TOCTOU window against a link that can be re-pointed between the check
|
|
567
|
+
// and the write). A download names a file to create, so a path that is
|
|
568
|
+
// already an unresolvable link is refused outright — the one shape where
|
|
569
|
+
// "cannot tell where this lands" is the whole answer.
|
|
570
|
+
if (isSymlink(resolved)) return null;
|
|
571
|
+
|
|
572
|
+
// Otherwise walk up to the deepest ancestor that does exist and check that,
|
|
573
|
+
// then re-apply the segments below it. Those segments are `..`-free by the
|
|
574
|
+
// lexical check above, so they cannot climb back out.
|
|
575
|
+
let ancestor = path.dirname(resolved);
|
|
576
|
+
const tail: string[] = [path.basename(resolved)];
|
|
577
|
+
for (;;) {
|
|
578
|
+
const real = tryRealpath(ancestor);
|
|
579
|
+
if (real) {
|
|
580
|
+
return isUnderRootLexical(path.join(real, ...tail.reverse()), realRoot) ? resolved : null;
|
|
581
|
+
}
|
|
582
|
+
const parent = path.dirname(ancestor);
|
|
583
|
+
// Ran past the root without finding anything real: the workspace itself
|
|
584
|
+
// resolved above, so this cannot happen unless it vanished mid-check.
|
|
585
|
+
if (parent === ancestor || !isUnderRootLexical(ancestor, root)) return null;
|
|
586
|
+
tail.push(path.basename(ancestor));
|
|
587
|
+
ancestor = parent;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Whether `target` is a strict descendant of `root`, ignoring symlinks. */
|
|
592
|
+
function isUnderRootLexical(target: string, root: string): boolean {
|
|
593
|
+
const relative = path.relative(root, target);
|
|
594
|
+
return !!relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function tryRealpath(target: string): string | null {
|
|
598
|
+
try {
|
|
599
|
+
return fs.realpathSync.native(target);
|
|
600
|
+
} catch {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** Whether the path itself is a symlink, without following it. */
|
|
606
|
+
function isSymlink(target: string): boolean {
|
|
607
|
+
try {
|
|
608
|
+
return fs.lstatSync(target).isSymbolicLink();
|
|
609
|
+
} catch {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
523
614
|
export function formatPathRelativeToCwd(
|
|
524
615
|
filePath: string,
|
|
525
616
|
cwd: string,
|