one-code-extension 0.1.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree isolation for in-process agent runs (`isolation: "worktree"` on the
|
|
3
|
+
* Agent tool and workflow `agent()` calls): a registry of the live isolation
|
|
4
|
+
* worktrees, and the inline-extension guard that applies the same git-isolation
|
|
5
|
+
* bash guard an `enter_worktree` session gets (extensions/worktree/guards.ts).
|
|
6
|
+
*
|
|
7
|
+
* The registry exists because the association can only resolve at call time:
|
|
8
|
+
* these worktrees live in tmpdirs (not statically recognizable paths), and the
|
|
9
|
+
* child loaders are cached and shared across runs with different cwds.
|
|
10
|
+
* createWorktree/cleanupWorktree (subagents/worktree.ts) register and release
|
|
11
|
+
* entries; reconstructed run records re-register kept worktrees after a
|
|
12
|
+
* process restart (subagents/index.ts); the guard resolves each bash call's
|
|
13
|
+
* cwd against them.
|
|
14
|
+
*
|
|
15
|
+
* Module state is deliberate and safe despite jiti isolation: registration
|
|
16
|
+
* (worktree creation) and lookup (the guard factory) always happen inside the
|
|
17
|
+
* SAME extension's import graph — the subagents extension, or the workflow
|
|
18
|
+
* extension via its own module copy — so the state never needs to cross
|
|
19
|
+
* extensions.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
24
|
+
import type { InlineExtension } from "@earendil-works/pi-coding-agent";
|
|
25
|
+
import { isWithin, isWritingTool, pathArgument, resolveForContainment, toAbsolute } from "../auto-mode/paths.ts";
|
|
26
|
+
import { analyzeShellCommand } from "../auto-mode/shell-analysis.ts";
|
|
27
|
+
import { worktreeBashGuardReason } from "../worktree/guards.ts";
|
|
28
|
+
|
|
29
|
+
export interface WorktreeIsolation {
|
|
30
|
+
/** The isolated worktree the run works in. */
|
|
31
|
+
worktreePath: string;
|
|
32
|
+
/** Root of the shared checkout the worktree belongs to. */
|
|
33
|
+
sharedRoot: string;
|
|
34
|
+
/**
|
|
35
|
+
* Both roots in resolved form (macOS /var → /private/var, symlinked
|
|
36
|
+
* checkouts), computed once at registration so the per-call write guard
|
|
37
|
+
* resolves only the write's own target, not two static roots per tool call.
|
|
38
|
+
*/
|
|
39
|
+
resolvedWorktreePath: string;
|
|
40
|
+
resolvedSharedRoot: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Build the registry entry for a worktree, resolving both roots once. Exported for tests. */
|
|
44
|
+
export function describeIsolation(worktreePath: string, sharedRoot: string): WorktreeIsolation {
|
|
45
|
+
return {
|
|
46
|
+
worktreePath,
|
|
47
|
+
sharedRoot,
|
|
48
|
+
resolvedWorktreePath: resolveForContainment(worktreePath) ?? worktreePath,
|
|
49
|
+
resolvedSharedRoot: resolveForContainment(sharedRoot) ?? sharedRoot,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** worktreePath → its isolation record, for every live isolation worktree. */
|
|
54
|
+
const active = new Map<string, WorktreeIsolation>();
|
|
55
|
+
|
|
56
|
+
export function registerWorktreeIsolation(worktreePath: string, sharedRoot: string): void {
|
|
57
|
+
active.set(worktreePath, describeIsolation(worktreePath, sharedRoot));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function releaseWorktreeIsolation(worktreePath: string): void {
|
|
61
|
+
active.delete(worktreePath);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The registered isolation worktree `cwd` lives in, if any. */
|
|
65
|
+
export function worktreeIsolationFor(cwd: string): WorktreeIsolation | undefined {
|
|
66
|
+
for (const isolation of active.values()) {
|
|
67
|
+
if (isWithin(isolation.worktreePath, cwd)) return isolation;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Where a write from an isolated run must not land: inside the shared checkout,
|
|
74
|
+
* outside the worktree. Returns the corresponding worktree path when `target`
|
|
75
|
+
* is such a path, else undefined (inside the worktree, or outside the
|
|
76
|
+
* repository altogether — the permission gate still judges those). Shared by
|
|
77
|
+
* the file-tool guard and the bash write-target guard below. Pure.
|
|
78
|
+
*/
|
|
79
|
+
export function sharedCheckoutWriteTarget(target: string, cwd: string, isolation: WorktreeIsolation, home = homedir()): string | undefined {
|
|
80
|
+
const { worktreePath, sharedRoot, resolvedWorktreePath, resolvedSharedRoot } = isolation;
|
|
81
|
+
const absolute = toAbsolute(cwd, target, home);
|
|
82
|
+
// Fast path: spelled under the worktree as registered — the common case for
|
|
83
|
+
// every write the model does where it was told to (no syscall).
|
|
84
|
+
if (isWithin(worktreePath, absolute)) return undefined;
|
|
85
|
+
// Resolved so a write is judged by where it lands, not how it is spelled.
|
|
86
|
+
const resolved = resolveForContainment(absolute) ?? absolute;
|
|
87
|
+
if (isWithin(resolvedWorktreePath, resolved) || !isWithin(resolvedSharedRoot, resolved)) return undefined;
|
|
88
|
+
// The tail keeps the model's spelling when the path was written under the
|
|
89
|
+
// shared root as given; the resolved forms are case-folded on darwin, which
|
|
90
|
+
// would send the model to `a.ts` for a file named `A.ts` on a case-sensitive volume.
|
|
91
|
+
const spelled = relative(sharedRoot, absolute);
|
|
92
|
+
const tail = spelled && !spelled.startsWith("..") && !isAbsolute(spelled) ? spelled : relative(resolvedSharedRoot, resolved);
|
|
93
|
+
return join(worktreePath, tail);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const refusal = (isolation: WorktreeIsolation, what: string, mapped: string): string =>
|
|
97
|
+
`This agent is isolated in the worktree ${isolation.worktreePath}, but ${what} is in the shared checkout ${isolation.sharedRoot}. ` +
|
|
98
|
+
`Refusing the write — an isolated agent's changes belong in its own worktree. Write the corresponding path there instead: ${mapped}`;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Refuse a file-tool write (`edit`/`write`/`notebook_edit`) from an isolated
|
|
102
|
+
* run that lands in the shared checkout: the model was told to work in the
|
|
103
|
+
* worktree, but a fork inherits a transcript whose every path points at the
|
|
104
|
+
* main tree, and a named agent's task may quote such paths too. Pure; exported
|
|
105
|
+
* for tests.
|
|
106
|
+
*/
|
|
107
|
+
export function worktreeWriteGuardReason(input: {
|
|
108
|
+
toolName: string;
|
|
109
|
+
target: string | undefined;
|
|
110
|
+
cwd: string;
|
|
111
|
+
isolation: WorktreeIsolation;
|
|
112
|
+
home?: string;
|
|
113
|
+
}): string | undefined {
|
|
114
|
+
if (!isWritingTool(input.toolName) || !input.target) return undefined;
|
|
115
|
+
const mapped = sharedCheckoutWriteTarget(input.target, input.cwd, input.isolation, input.home);
|
|
116
|
+
return mapped ? refusal(input.isolation, input.target, mapped) : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The same rule for a shell command: every path the command may write
|
|
121
|
+
* (redirections, `sed -i`/`cp`/`mv`/`tee` positionals, output flags — the
|
|
122
|
+
* write-target model auto-mode's shell analysis already maintains) is checked
|
|
123
|
+
* like a file-tool target. Reads of the shared checkout stay allowed: the fork
|
|
124
|
+
* may need to look at what it inherited. Interpreters (`sed -i`, `perl`,
|
|
125
|
+
* `python`) are not modelled as writers there, so a write hidden in one reaches
|
|
126
|
+
* the permission gate and, in auto mode, the classifier instead — steering, not
|
|
127
|
+
* a sandbox (same contract as extensions/worktree/guards.ts). Pure; exported for tests.
|
|
128
|
+
*/
|
|
129
|
+
export function worktreeBashWriteGuardReason(input: { command: string; cwd: string; isolation: WorktreeIsolation; home?: string }): string | undefined {
|
|
130
|
+
const home = input.home ?? homedir();
|
|
131
|
+
const evidence = analyzeShellCommand({ command: input.command, cwd: input.cwd, home });
|
|
132
|
+
for (const write of evidence.writes) {
|
|
133
|
+
const mapped = sharedCheckoutWriteTarget(write.token, input.cwd, input.isolation, home);
|
|
134
|
+
if (mapped) return refusal(input.isolation, `this command writes to ${write.token}, which`, mapped);
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The git-isolation guard for worktree-isolated child sessions, as its own
|
|
141
|
+
* inline extension so it loads AHEAD of the permission gate — the same
|
|
142
|
+
* guard-before-permissions layering the main session gets from extension load
|
|
143
|
+
* order (worktree before permissions, findings §3). buildAgentLoader places it
|
|
144
|
+
* first in `extensionFactories`; keep it there. Three checks: bash git operations
|
|
145
|
+
* must target the worktree (extensions/worktree/guards.ts), and writes — by file
|
|
146
|
+
* tool or by shell command — must land in it.
|
|
147
|
+
*/
|
|
148
|
+
export function worktreeGuardFactory(cwd: string): InlineExtension {
|
|
149
|
+
return {
|
|
150
|
+
name: "agent-worktree-guard",
|
|
151
|
+
hidden: true,
|
|
152
|
+
factory: (pi) => {
|
|
153
|
+
pi.on("tool_call", (event, ctx) => {
|
|
154
|
+
const runCwd = ctx?.cwd ?? cwd;
|
|
155
|
+
const isolation = worktreeIsolationFor(runCwd);
|
|
156
|
+
if (!isolation) return undefined;
|
|
157
|
+
const input = (event.input ?? {}) as Record<string, unknown>;
|
|
158
|
+
if (event.toolName === "bash") {
|
|
159
|
+
if (typeof input.command !== "string") return undefined;
|
|
160
|
+
const reason =
|
|
161
|
+
worktreeBashGuardReason({ command: input.command, worktreePath: isolation.worktreePath, sharedRoot: isolation.sharedRoot }) ??
|
|
162
|
+
worktreeBashWriteGuardReason({ command: input.command, cwd: runCwd, isolation });
|
|
163
|
+
return reason ? { block: true, reason } : undefined;
|
|
164
|
+
}
|
|
165
|
+
const reason = worktreeWriteGuardReason({ toolName: event.toolName, target: pathArgument(input), cwd: runCwd, isolation });
|
|
166
|
+
return reason ? { block: true, reason } : undefined;
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
package/extensions/lsp/client.ts
CHANGED
|
@@ -20,15 +20,41 @@ import { readFileSync } from "node:fs";
|
|
|
20
20
|
import { pathToFileURL } from "node:url";
|
|
21
21
|
import type { LspDiagnostic } from "./format.ts";
|
|
22
22
|
import { createReaderState, encodeMessage, type JsonRpcMessage, readMessages } from "./protocol.ts";
|
|
23
|
-
import type { ServerConfig } from "./servers.ts";
|
|
24
23
|
|
|
25
24
|
const INITIALIZE_TIMEOUT_MS = 15_000;
|
|
26
25
|
const DIAGNOSTICS_TIMEOUT_MS = 6_000;
|
|
27
26
|
|
|
27
|
+
/** What a client actually needs to spawn — the built-in table's ServerConfig satisfies it. */
|
|
28
|
+
export interface SpawnConfig {
|
|
29
|
+
command: string;
|
|
30
|
+
args: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Per-server extras a plugin config can carry (built-in servers pass none). */
|
|
34
|
+
export interface LspClientOptions {
|
|
35
|
+
/** Merged over process.env at spawn. */
|
|
36
|
+
env?: Record<string, string>;
|
|
37
|
+
/** Sent in the `initialize` request. */
|
|
38
|
+
initializationOptions?: unknown;
|
|
39
|
+
/** Answers the server's workspace/configuration requests. */
|
|
40
|
+
settings?: unknown;
|
|
41
|
+
startupTimeoutMs?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
28
44
|
export function pathToUri(path: string): string {
|
|
29
45
|
return pathToFileURL(path).toString();
|
|
30
46
|
}
|
|
31
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The reply to a server's workspace/configuration request: one settings object
|
|
50
|
+
* per requested item (LSP 3.6 shape), `{}` when none are configured.
|
|
51
|
+
*/
|
|
52
|
+
export function configurationResponse(settings: unknown, params: unknown): unknown[] {
|
|
53
|
+
const items = (params as { items?: unknown[] } | undefined)?.items;
|
|
54
|
+
const count = Array.isArray(items) && items.length > 0 ? items.length : 1;
|
|
55
|
+
return Array.from({ length: count }, () => settings ?? {});
|
|
56
|
+
}
|
|
57
|
+
|
|
32
58
|
function unrefStream(stream: unknown): void {
|
|
33
59
|
(stream as { unref?: () => void } | null | undefined)?.unref?.();
|
|
34
60
|
}
|
|
@@ -48,11 +74,12 @@ export class LspClient {
|
|
|
48
74
|
private open = new Map<string, OpenDocument>();
|
|
49
75
|
private ready = false;
|
|
50
76
|
private failure?: string;
|
|
77
|
+
private publishes = 0;
|
|
51
78
|
|
|
52
79
|
constructor(
|
|
53
|
-
readonly
|
|
54
|
-
private readonly config: ServerConfig,
|
|
80
|
+
private readonly config: SpawnConfig,
|
|
55
81
|
private readonly root: string,
|
|
82
|
+
private readonly options: LspClientOptions = {},
|
|
56
83
|
) {}
|
|
57
84
|
|
|
58
85
|
get isRunning(): boolean {
|
|
@@ -75,7 +102,7 @@ export class LspClient {
|
|
|
75
102
|
const child = spawn(this.config.command, this.config.args, {
|
|
76
103
|
cwd: this.root,
|
|
77
104
|
stdio: ["pipe", "pipe", "pipe"],
|
|
78
|
-
env: process.env,
|
|
105
|
+
env: this.options.env ? { ...process.env, ...this.options.env } : process.env,
|
|
79
106
|
});
|
|
80
107
|
this.child = child;
|
|
81
108
|
|
|
@@ -98,6 +125,12 @@ export class LspClient {
|
|
|
98
125
|
this.failure = `Could not start ${this.config.command}: ${error.message}`;
|
|
99
126
|
this.rejectAll(new Error(this.failure));
|
|
100
127
|
});
|
|
128
|
+
// A write to a dead server's stdin is an EPIPE on the stream; without a
|
|
129
|
+
// listener that is an unhandled 'error' event, i.e. a crash (review T10).
|
|
130
|
+
child.stdin?.on("error", (error) => {
|
|
131
|
+
this.failure ??= `${this.config.command} stopped accepting input: ${error.message}`;
|
|
132
|
+
this.ready = false;
|
|
133
|
+
});
|
|
101
134
|
child.on("exit", (code, signal) => {
|
|
102
135
|
this.ready = false;
|
|
103
136
|
if (!this.failure && code !== 0) {
|
|
@@ -113,6 +146,7 @@ export class LspClient {
|
|
|
113
146
|
processId: process.pid,
|
|
114
147
|
rootUri: pathToUri(this.root),
|
|
115
148
|
workspaceFolders: [{ uri: pathToUri(this.root), name: "workspace" }],
|
|
149
|
+
initializationOptions: this.options.initializationOptions,
|
|
116
150
|
capabilities: {
|
|
117
151
|
textDocument: {
|
|
118
152
|
synchronization: { dynamicRegistration: false, didSave: false },
|
|
@@ -121,7 +155,7 @@ export class LspClient {
|
|
|
121
155
|
workspace: { workspaceFolders: true, configuration: true },
|
|
122
156
|
},
|
|
123
157
|
},
|
|
124
|
-
INITIALIZE_TIMEOUT_MS,
|
|
158
|
+
this.options.startupTimeoutMs ?? INITIALIZE_TIMEOUT_MS,
|
|
125
159
|
);
|
|
126
160
|
|
|
127
161
|
this.notify("initialized", {});
|
|
@@ -131,6 +165,7 @@ export class LspClient {
|
|
|
131
165
|
private handle(message: JsonRpcMessage): void {
|
|
132
166
|
if (message.method === "textDocument/publishDiagnostics") {
|
|
133
167
|
const params = message.params as { uri: string; diagnostics: LspDiagnostic[] };
|
|
168
|
+
this.publishes++;
|
|
134
169
|
this.diagnostics.set(params.uri, params.diagnostics ?? []);
|
|
135
170
|
const waiting = this.waiters.get(params.uri);
|
|
136
171
|
if (waiting) {
|
|
@@ -142,7 +177,10 @@ export class LspClient {
|
|
|
142
177
|
|
|
143
178
|
// Requests from the server: answer the ones that block startup.
|
|
144
179
|
if (message.id !== undefined && message.method) {
|
|
145
|
-
const result =
|
|
180
|
+
const result =
|
|
181
|
+
message.method === "workspace/configuration"
|
|
182
|
+
? configurationResponse(this.options.settings, message.params)
|
|
183
|
+
: null;
|
|
146
184
|
this.send({ id: message.id, result });
|
|
147
185
|
return;
|
|
148
186
|
}
|
|
@@ -193,8 +231,12 @@ export class LspClient {
|
|
|
193
231
|
this.waiters.clear();
|
|
194
232
|
}
|
|
195
233
|
|
|
196
|
-
/**
|
|
197
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Push the file's current on-disk content and return its uri. `languageId`
|
|
236
|
+
* is per-document: one plugin server process can map different extensions to
|
|
237
|
+
* different language ids.
|
|
238
|
+
*/
|
|
239
|
+
syncFile(path: string, languageId: string): string {
|
|
198
240
|
const uri = pathToUri(path);
|
|
199
241
|
let text: string;
|
|
200
242
|
try {
|
|
@@ -207,7 +249,7 @@ export class LspClient {
|
|
|
207
249
|
if (!existing) {
|
|
208
250
|
this.open.set(uri, { version: 1, text });
|
|
209
251
|
this.notify("textDocument/didOpen", {
|
|
210
|
-
textDocument: { uri, languageId
|
|
252
|
+
textDocument: { uri, languageId, version: 1, text },
|
|
211
253
|
});
|
|
212
254
|
} else if (existing.text !== text) {
|
|
213
255
|
const version = existing.version + 1;
|
|
@@ -240,10 +282,10 @@ export class LspClient {
|
|
|
240
282
|
* nothing when a file is unchanged, hence the timeout plus last-known
|
|
241
283
|
* fallback.
|
|
242
284
|
*/
|
|
243
|
-
async getDiagnostics(path: string, timeoutMs = DIAGNOSTICS_TIMEOUT_MS): Promise<LspDiagnostic[]> {
|
|
285
|
+
async getDiagnostics(path: string, languageId: string, timeoutMs = DIAGNOSTICS_TIMEOUT_MS): Promise<LspDiagnostic[]> {
|
|
244
286
|
const uri = pathToUri(path);
|
|
245
287
|
const hadContent = this.open.get(uri)?.text;
|
|
246
|
-
const synced = this.syncFile(path);
|
|
288
|
+
const synced = this.syncFile(path, languageId);
|
|
247
289
|
const changed = this.open.get(uri)?.text !== hadContent;
|
|
248
290
|
|
|
249
291
|
if (changed || !this.diagnostics.has(synced)) {
|
|
@@ -252,17 +294,32 @@ export class LspClient {
|
|
|
252
294
|
return this.diagnostics.get(synced) ?? [];
|
|
253
295
|
}
|
|
254
296
|
|
|
297
|
+
/** Everything the server has published, per document uri (for the watcher). */
|
|
298
|
+
allDiagnostics(): Map<string, LspDiagnostic[]> {
|
|
299
|
+
return new Map(this.diagnostics);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Monotonic count of publishDiagnostics received — the watcher's cheap
|
|
304
|
+
* "anything new since last look?" check, so the per-tool-round delta scan
|
|
305
|
+
* only runs when a server actually republished.
|
|
306
|
+
*/
|
|
307
|
+
get publishCount(): number {
|
|
308
|
+
return this.publishes;
|
|
309
|
+
}
|
|
310
|
+
|
|
255
311
|
async stop(): Promise<void> {
|
|
256
312
|
const child = this.child;
|
|
257
313
|
if (!child) return;
|
|
258
|
-
this.child = undefined;
|
|
259
314
|
this.ready = false;
|
|
260
315
|
try {
|
|
316
|
+
// Send while this.child is still set — send() writes through it.
|
|
261
317
|
this.send({ id: this.nextId++, method: "shutdown", params: null });
|
|
262
318
|
this.notify("exit", null);
|
|
263
319
|
} catch {
|
|
264
320
|
// Server may already be gone.
|
|
265
321
|
}
|
|
322
|
+
this.child = undefined;
|
|
266
323
|
await new Promise<void>((resolve) => {
|
|
267
324
|
const timer = setTimeout(() => {
|
|
268
325
|
child.kill("SIGKILL");
|
package/extensions/lsp/format.ts
CHANGED
|
@@ -12,10 +12,20 @@ export interface LspDiagnostic {
|
|
|
12
12
|
|
|
13
13
|
export type SeverityFilter = "error" | "warning" | "all";
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/** LSP severity metadata (1..4; missing severity is treated as error). */
|
|
16
|
+
export const SEVERITY: Record<number, { name: string; symbol: string }> = {
|
|
17
|
+
1: { name: "error", symbol: "✘" },
|
|
18
|
+
2: { name: "warning", symbol: "⚠" },
|
|
19
|
+
3: { name: "info", symbol: "ℹ" },
|
|
20
|
+
4: { name: "hint", symbol: "★" },
|
|
21
|
+
};
|
|
16
22
|
|
|
17
23
|
export function severityName(severity: number | undefined): string {
|
|
18
|
-
return
|
|
24
|
+
return SEVERITY[severity ?? 1]?.name ?? "error";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function severitySymbol(severity: number | undefined): string {
|
|
28
|
+
return SEVERITY[severity ?? 1]?.symbol ?? "✘";
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
export function filterDiagnostics(diagnostics: LspDiagnostic[], filter: SeverityFilter): LspDiagnostic[] {
|