@herbertgao/pi-extensions 2026.9.4 → 2026.9.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/README.md +3 -15
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
- package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
- package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
- package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +69 -14
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
- package/node_modules/@herbertgao/sol-pi/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +0 -5
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +3 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
- package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
- package/node_modules/@tifan/pi-copy-response/README.md +2 -2
- package/node_modules/@tifan/pi-copy-response/package.json +3 -2
- package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
- package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
- package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
- package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
- package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
- package/node_modules/@tifan/pi-recap/package.json +3 -2
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
- package/node_modules/pi-mcp-adapter/README.md +125 -19
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
- package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
- package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
- package/node_modules/pi-mcp-adapter/commands.ts +13 -15
- package/node_modules/pi-mcp-adapter/config.ts +198 -30
- package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
- package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
- package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
- package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
- package/node_modules/pi-mcp-adapter/index.ts +827 -164
- package/node_modules/pi-mcp-adapter/init.ts +48 -10
- package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
- package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
- package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
- package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
- package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
- package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
- package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
- package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
- package/node_modules/pi-mcp-adapter/package.json +13 -3
- package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
- package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
- package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
- package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
- package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
- package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
- package/node_modules/pi-mcp-adapter/state.ts +10 -1
- package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
- package/node_modules/pi-mcp-adapter/types.ts +57 -4
- package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
- package/node_modules/pi-mcp-adapter/utils.ts +64 -6
- package/package.json +13 -21
- package/examples/pi-footer.json +0 -275
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +0 -70
- package/node_modules/@czottmann/pi-automode/LICENSE.md +0 -21
- package/node_modules/@czottmann/pi-automode/README.md +0 -215
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +0 -39
- package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +0 -46
- package/node_modules/@czottmann/pi-automode/docs/adr/ADR-002-global-config-in-extension-data-directory.md +0 -60
- package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +0 -6
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +0 -428
- package/node_modules/@czottmann/pi-automode/docs/configuration.md +0 -171
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +0 -178
- package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +0 -90
- package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +0 -160
- package/node_modules/@czottmann/pi-automode/examples/automode.local.json +0 -44
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/bash.ts +0 -692
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +0 -852
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +0 -904
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +0 -228
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +0 -954
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +0 -429
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +0 -173
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model-selector.ts +0 -113
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model.ts +0 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +0 -297
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +0 -623
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +0 -102
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/transcript.ts +0 -236
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +0 -205
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +0 -54
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode.ts +0 -26
- package/node_modules/@czottmann/pi-automode/package.json +0 -61
- package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +0 -63
|
@@ -1,954 +0,0 @@
|
|
|
1
|
-
import { realpathSync, statSync } from "node:fs";
|
|
2
|
-
import { dirname, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import type {
|
|
5
|
-
ExtensionAPI,
|
|
6
|
-
ExtensionCommandContext,
|
|
7
|
-
ExtensionContext,
|
|
8
|
-
} from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
10
|
-
import { Type } from "typebox";
|
|
11
|
-
import {
|
|
12
|
-
classifierReasoningForConfig,
|
|
13
|
-
defaultClassifyAction,
|
|
14
|
-
serializeClassifierAction,
|
|
15
|
-
} from "./classifier.ts";
|
|
16
|
-
import { analyzeBash, type BashAnalysis } from "./bash.ts";
|
|
17
|
-
import {
|
|
18
|
-
AUTO_MODE_GUIDANCE,
|
|
19
|
-
DEFAULT_ALLOW,
|
|
20
|
-
DEFAULT_ENVIRONMENT,
|
|
21
|
-
DEFAULT_HARD_DENY,
|
|
22
|
-
DEFAULT_PROTECTED_PATHS,
|
|
23
|
-
DEFAULT_SOFT_DENY,
|
|
24
|
-
PATH_BEARING_TOOLS,
|
|
25
|
-
PI_GLOBAL_SETTINGS,
|
|
26
|
-
READ_ONLY_TOOLS,
|
|
27
|
-
} from "./constants.ts";
|
|
28
|
-
import {
|
|
29
|
-
type GlobalConfigPreparation,
|
|
30
|
-
loadEffectiveConfigWithDiagnostics,
|
|
31
|
-
prepareGlobalConfig,
|
|
32
|
-
writeGlobalClassifierModel,
|
|
33
|
-
} from "./config.ts";
|
|
34
|
-
import { deterministicHardDeny } from "./hard-deny.ts";
|
|
35
|
-
import {
|
|
36
|
-
createLogger,
|
|
37
|
-
newDecisionId,
|
|
38
|
-
resolveLogPath,
|
|
39
|
-
type Logger,
|
|
40
|
-
} from "./log.ts";
|
|
41
|
-
import { formatModelSpec, parseModelSpec } from "./model.ts";
|
|
42
|
-
import { promptForClassifierModel } from "./model-selector.ts";
|
|
43
|
-
import {
|
|
44
|
-
matchesAllowedToolPatterns,
|
|
45
|
-
matchesDeniedPath,
|
|
46
|
-
matchesToolPattern,
|
|
47
|
-
matchingBashCommandText,
|
|
48
|
-
recursiveSearchMayReachDeniedPath,
|
|
49
|
-
} from "./permissions.ts";
|
|
50
|
-
import {
|
|
51
|
-
extractInputPath,
|
|
52
|
-
isInside,
|
|
53
|
-
isProtectedPath,
|
|
54
|
-
resolvePathForPolicy,
|
|
55
|
-
resolveToolInputPath,
|
|
56
|
-
} from "./paths.ts";
|
|
57
|
-
import {
|
|
58
|
-
actionSummary,
|
|
59
|
-
formatDenials,
|
|
60
|
-
pushDenial,
|
|
61
|
-
restoreState,
|
|
62
|
-
statusLine,
|
|
63
|
-
statusText,
|
|
64
|
-
} from "./state.ts";
|
|
65
|
-
import { loadedContextFromSystemPromptOptions } from "./transcript.ts";
|
|
66
|
-
import type {
|
|
67
|
-
AutoModeState,
|
|
68
|
-
ClassifierReasoningLog,
|
|
69
|
-
ClassifyAction,
|
|
70
|
-
ClassifyResult,
|
|
71
|
-
ConfigLoadResult,
|
|
72
|
-
DecisionKind,
|
|
73
|
-
DenialRecord,
|
|
74
|
-
EffectiveConfig,
|
|
75
|
-
} from "./types.ts";
|
|
76
|
-
import { safeJson, truncateMiddle } from "./utils.ts";
|
|
77
|
-
|
|
78
|
-
const INSPECT_TOOL = "automode_inspect";
|
|
79
|
-
const INSPECTION_ACTIONS = ["status", "config", "defaults", "denials"] as const;
|
|
80
|
-
type InspectionAction = (typeof INSPECTION_ACTIONS)[number];
|
|
81
|
-
|
|
82
|
-
function matchedCommandSummary(command: string | undefined): string | undefined {
|
|
83
|
-
return command ? truncateMiddle(command, 500) : undefined;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function canonicalPath(path: string): string {
|
|
87
|
-
try {
|
|
88
|
-
return realpathSync(path);
|
|
89
|
-
} catch {
|
|
90
|
-
return resolve(path);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const EXTENSION_PATH = canonicalPath(fileURLToPath(import.meta.url));
|
|
95
|
-
const EXTENSION_ENTRY_PATH = canonicalPath(
|
|
96
|
-
resolve(dirname(EXTENSION_PATH), "../auto-mode.ts"),
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
export function modelVisibleConfigDiagnostics(
|
|
100
|
-
diagnostics: string[],
|
|
101
|
-
): string[] {
|
|
102
|
-
return diagnostics.map((diagnostic) =>
|
|
103
|
-
diagnostic.replace(
|
|
104
|
-
/invalid JSON \([\s\S]*\)$/,
|
|
105
|
-
"invalid JSON (parser details omitted from model-visible output)",
|
|
106
|
-
)
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function projectIsTrusted(
|
|
111
|
-
ctx: { isProjectTrusted?: () => boolean },
|
|
112
|
-
): boolean {
|
|
113
|
-
return typeof ctx.isProjectTrusted === "function"
|
|
114
|
-
? ctx.isProjectTrusted()
|
|
115
|
-
: false;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export type PiAutomodeOptions = {
|
|
119
|
-
/** Override config loading in tests. Runtime code uses Pi-owned disk settings. */
|
|
120
|
-
loadConfig?: (cwd: string, projectTrusted: boolean) => EffectiveConfig;
|
|
121
|
-
/** Override classifier calls in tests so unit tests never need a real LLM/API key. */
|
|
122
|
-
classifyAction?: ClassifyAction;
|
|
123
|
-
/** Override classifier-model persistence in tests. Runtime code writes the active global config. */
|
|
124
|
-
saveClassifierModel?: (classifierModel: string) => void;
|
|
125
|
-
/** Override global config migration and path selection in tests. */
|
|
126
|
-
prepareGlobalConfig?: () => GlobalConfigPreparation;
|
|
127
|
-
/** Override the application-owned observability log root in tests. */
|
|
128
|
-
logRoot?: string;
|
|
129
|
-
/** Override the observability log clock in tests. */
|
|
130
|
-
now?: () => Date;
|
|
131
|
-
/** Override Bash analysis in tests. Runtime code uses unbash. */
|
|
132
|
-
analyzeBash?: typeof analyzeBash;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
type LogCtx = {
|
|
136
|
-
logger: Logger;
|
|
137
|
-
decisionId: string;
|
|
138
|
-
classifierModel?: string;
|
|
139
|
-
reasoning: ClassifierReasoningLog;
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
/** Append ccusage-compatible usage and optional classifier I/O entries. */
|
|
143
|
-
function logClassifierIo(decision: ClassifyResult, log: LogCtx): void {
|
|
144
|
-
if (decision.reasoning) log.reasoning = decision.reasoning;
|
|
145
|
-
if (decision.io) log.reasoning = decision.io.reasoning;
|
|
146
|
-
if (!log.logger.enabled || !decision.io) return;
|
|
147
|
-
|
|
148
|
-
for (const attempt of decision.io.attempts) {
|
|
149
|
-
const response = attempt.response;
|
|
150
|
-
if (!response) continue;
|
|
151
|
-
log.logger.append({
|
|
152
|
-
type: "message",
|
|
153
|
-
timestamp: new Date(response.timestamp).toISOString(),
|
|
154
|
-
message: {
|
|
155
|
-
role: "assistant",
|
|
156
|
-
model: response.model,
|
|
157
|
-
usage: response.usage,
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (!log.logger.classifierIo) return;
|
|
163
|
-
log.logger.append({
|
|
164
|
-
type: "classifier",
|
|
165
|
-
ts: new Date().toISOString(),
|
|
166
|
-
decisionId: log.decisionId,
|
|
167
|
-
model: decision.io.model,
|
|
168
|
-
reasoning: decision.io.reasoning,
|
|
169
|
-
prompt: decision.io.prompt,
|
|
170
|
-
attempts: decision.io.attempts,
|
|
171
|
-
durationMs: decision.io.durationMs,
|
|
172
|
-
parsed: {
|
|
173
|
-
decision: decision.decision,
|
|
174
|
-
tier: decision.tier,
|
|
175
|
-
reason: decision.reason,
|
|
176
|
-
},
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Create a Pi extension instance. Default export uses production dependencies. */
|
|
181
|
-
export function createPiAutomode(options: PiAutomodeOptions = {}) {
|
|
182
|
-
const classify = options.classifyAction ?? defaultClassifyAction;
|
|
183
|
-
const now = options.now ?? (() => new Date());
|
|
184
|
-
|
|
185
|
-
return function piAutomode(pi: ExtensionAPI) {
|
|
186
|
-
const globalConfig = options.prepareGlobalConfig?.() ??
|
|
187
|
-
(options.loadConfig
|
|
188
|
-
? { status: "current" as const, activePath: PI_GLOBAL_SETTINGS[0] }
|
|
189
|
-
: prepareGlobalConfig());
|
|
190
|
-
const loadConfigWithDiagnostics = (
|
|
191
|
-
cwd: string,
|
|
192
|
-
projectTrusted: boolean,
|
|
193
|
-
): ConfigLoadResult => {
|
|
194
|
-
const result = options.loadConfig
|
|
195
|
-
? {
|
|
196
|
-
config: options.loadConfig(cwd, projectTrusted),
|
|
197
|
-
diagnostics: [],
|
|
198
|
-
}
|
|
199
|
-
: loadEffectiveConfigWithDiagnostics(
|
|
200
|
-
cwd,
|
|
201
|
-
projectTrusted,
|
|
202
|
-
globalConfig.activePath,
|
|
203
|
-
);
|
|
204
|
-
return globalConfig.diagnostic
|
|
205
|
-
? {
|
|
206
|
-
...result,
|
|
207
|
-
diagnostics: [...result.diagnostics, globalConfig.diagnostic],
|
|
208
|
-
}
|
|
209
|
-
: result;
|
|
210
|
-
};
|
|
211
|
-
const persistClassifierModel = options.saveClassifierModel ??
|
|
212
|
-
((classifierModel: string) =>
|
|
213
|
-
writeGlobalClassifierModel(classifierModel, globalConfig.activePath));
|
|
214
|
-
const saveClassifierModel = globalConfig.writeBlockedReason
|
|
215
|
-
? (_classifierModel: string) => {
|
|
216
|
-
throw new Error(globalConfig.writeBlockedReason);
|
|
217
|
-
}
|
|
218
|
-
: persistClassifierModel;
|
|
219
|
-
let loadResult = loadConfigWithDiagnostics(process.cwd(), false);
|
|
220
|
-
let config: EffectiveConfig = loadResult.config;
|
|
221
|
-
let configDiagnostics: string[] = loadResult.diagnostics;
|
|
222
|
-
let state: AutoModeState = {
|
|
223
|
-
checkedActions: 0,
|
|
224
|
-
blockedActions: 0,
|
|
225
|
-
classifierAllowed: 0,
|
|
226
|
-
classifierDenied: 0,
|
|
227
|
-
recentDenials: [],
|
|
228
|
-
};
|
|
229
|
-
let loadedContext = "";
|
|
230
|
-
let globalConfigNoticeShown = false;
|
|
231
|
-
|
|
232
|
-
function effectiveConfig(): EffectiveConfig {
|
|
233
|
-
return {
|
|
234
|
-
...config,
|
|
235
|
-
enabled: state.enabledOverride ?? config.enabled,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function ownsInspectionTool(): boolean {
|
|
240
|
-
const tool = pi.getAllTools().find(({ name }) => name === INSPECT_TOOL);
|
|
241
|
-
if (!tool) return false;
|
|
242
|
-
const sourcePath = canonicalPath(tool.sourceInfo.path);
|
|
243
|
-
return sourcePath === EXTENSION_PATH || sourcePath === EXTENSION_ENTRY_PATH;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function persist(): void {
|
|
247
|
-
pi.appendEntry("pi-automode-state", state);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function updateUi(ctx: ExtensionContext): void {
|
|
251
|
-
if (!ctx.hasUI) return;
|
|
252
|
-
const cfg = effectiveConfig();
|
|
253
|
-
const text = statusLine(cfg, state);
|
|
254
|
-
ctx.ui.setStatus(
|
|
255
|
-
"pi-automode",
|
|
256
|
-
cfg.enabled
|
|
257
|
-
? ctx.ui.theme.fg("accent", text)
|
|
258
|
-
: ctx.ui.theme.fg("dim", text),
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function inspectAutomode(
|
|
263
|
-
action: InspectionAction,
|
|
264
|
-
ctx: ExtensionContext,
|
|
265
|
-
): unknown {
|
|
266
|
-
const cfg = effectiveConfig();
|
|
267
|
-
if (action === "status") {
|
|
268
|
-
const status = [
|
|
269
|
-
`enabled: ${cfg.enabled ? "yes" : "no"}`,
|
|
270
|
-
`classifier: ${cfg.classifierModel ?? "current session model"}`,
|
|
271
|
-
`classifier reasoning: ${cfg.classifierReasoningLevel ?? "server default"}`,
|
|
272
|
-
`checked actions: ${state.checkedActions}`,
|
|
273
|
-
`blocked actions: ${state.blockedActions}`,
|
|
274
|
-
`classifier allowed: ${state.classifierAllowed}`,
|
|
275
|
-
`classifier denied: ${state.classifierDenied}`,
|
|
276
|
-
`permissions.deny rules: ${cfg.permissionDeny.length}`,
|
|
277
|
-
`permissions.ask rules: ${cfg.permissionAsk.length}`,
|
|
278
|
-
`permissions.allow rules: ${cfg.permissionAllow.length}`,
|
|
279
|
-
`environment entries: ${cfg.environment.length}`,
|
|
280
|
-
`allow entries: ${cfg.allow.length}`,
|
|
281
|
-
`soft_deny entries: ${cfg.softDeny.length}`,
|
|
282
|
-
`hard_deny entries: ${cfg.hardDeny.length}`,
|
|
283
|
-
`last decision: ${state.lastDecision ?? "none"}`,
|
|
284
|
-
"last reason: omitted from model-visible inspection",
|
|
285
|
-
].join("\n");
|
|
286
|
-
return {
|
|
287
|
-
status,
|
|
288
|
-
state: {
|
|
289
|
-
enabledOverride: state.enabledOverride,
|
|
290
|
-
lastDecision: state.lastDecision,
|
|
291
|
-
checkedActions: state.checkedActions,
|
|
292
|
-
blockedActions: state.blockedActions,
|
|
293
|
-
classifierAllowed: state.classifierAllowed,
|
|
294
|
-
classifierDenied: state.classifierDenied,
|
|
295
|
-
},
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
if (action === "config") {
|
|
299
|
-
return {
|
|
300
|
-
config: cfg,
|
|
301
|
-
logFile: resolveLogPath(
|
|
302
|
-
ctx.sessionManager.getSessionFile?.(),
|
|
303
|
-
ctx.sessionManager.getSessionDir?.() ?? "",
|
|
304
|
-
ctx.sessionManager.getSessionId?.() ?? "unknown",
|
|
305
|
-
ctx.cwd,
|
|
306
|
-
options.logRoot,
|
|
307
|
-
now(),
|
|
308
|
-
),
|
|
309
|
-
diagnostics: modelVisibleConfigDiagnostics(configDiagnostics),
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
if (action === "defaults") {
|
|
313
|
-
return {
|
|
314
|
-
environment: DEFAULT_ENVIRONMENT,
|
|
315
|
-
allow: DEFAULT_ALLOW,
|
|
316
|
-
protectedPaths: DEFAULT_PROTECTED_PATHS,
|
|
317
|
-
soft_deny: DEFAULT_SOFT_DENY,
|
|
318
|
-
hard_deny: DEFAULT_HARD_DENY,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
const denials = state.recentDenials.slice().reverse().map((denial) => ({
|
|
322
|
-
timestamp: denial.timestamp,
|
|
323
|
-
kind: denial.kind,
|
|
324
|
-
toolName: denial.toolName,
|
|
325
|
-
}));
|
|
326
|
-
return {
|
|
327
|
-
summary: denials.length === 0
|
|
328
|
-
? "No recent auto-mode denials."
|
|
329
|
-
: `${denials.length} recent auto-mode denial(s). Reasons and action payloads are omitted.`,
|
|
330
|
-
denials,
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function block(
|
|
335
|
-
ctx: ExtensionContext,
|
|
336
|
-
denial: DenialRecord,
|
|
337
|
-
logCtx: LogCtx,
|
|
338
|
-
): { block: true; reason: string } {
|
|
339
|
-
state.blockedActions += 1;
|
|
340
|
-
state.lastDecision = "block";
|
|
341
|
-
state.lastReason = denial.reason;
|
|
342
|
-
pushDenial(state, denial);
|
|
343
|
-
persist();
|
|
344
|
-
updateUi(ctx);
|
|
345
|
-
if (logCtx.logger.enabled) {
|
|
346
|
-
logCtx.logger.append({
|
|
347
|
-
type: "decision",
|
|
348
|
-
ts: new Date().toISOString(),
|
|
349
|
-
decisionId: logCtx.decisionId,
|
|
350
|
-
sessionId: ctx.sessionManager.getSessionId?.(),
|
|
351
|
-
cwd: ctx.cwd,
|
|
352
|
-
tool: denial.toolName,
|
|
353
|
-
summary: denial.action,
|
|
354
|
-
kind: denial.kind,
|
|
355
|
-
outcome: "block",
|
|
356
|
-
reason: denial.reason,
|
|
357
|
-
classifierModel: logCtx.classifierModel,
|
|
358
|
-
reasoning: logCtx.reasoning,
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
if (ctx.hasUI) {
|
|
362
|
-
ctx.ui.notify(
|
|
363
|
-
`Auto mode blocked ${denial.toolName}: ${denial.reason}`,
|
|
364
|
-
"warning",
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
return { block: true, reason: `[pi-automode] ${denial.reason}` };
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function allow(
|
|
371
|
-
ctx: ExtensionContext,
|
|
372
|
-
kind: DecisionKind,
|
|
373
|
-
reason: string,
|
|
374
|
-
toolName: string,
|
|
375
|
-
summary: string,
|
|
376
|
-
logCtx: LogCtx,
|
|
377
|
-
): undefined {
|
|
378
|
-
state.lastDecision = "allow";
|
|
379
|
-
state.lastReason = reason;
|
|
380
|
-
persist();
|
|
381
|
-
updateUi(ctx);
|
|
382
|
-
if (logCtx.logger.enabled) {
|
|
383
|
-
logCtx.logger.append({
|
|
384
|
-
type: "decision",
|
|
385
|
-
ts: new Date().toISOString(),
|
|
386
|
-
decisionId: logCtx.decisionId,
|
|
387
|
-
sessionId: ctx.sessionManager.getSessionId?.(),
|
|
388
|
-
cwd: ctx.cwd,
|
|
389
|
-
tool: toolName,
|
|
390
|
-
summary,
|
|
391
|
-
kind,
|
|
392
|
-
outcome: "allow",
|
|
393
|
-
reason,
|
|
394
|
-
classifierModel: logCtx.classifierModel,
|
|
395
|
-
reasoning: logCtx.reasoning,
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
return undefined;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
pi.on("session_start", (_event, ctx) => {
|
|
402
|
-
loadResult = loadConfigWithDiagnostics(
|
|
403
|
-
ctx.cwd,
|
|
404
|
-
projectIsTrusted(ctx),
|
|
405
|
-
);
|
|
406
|
-
config = loadResult.config;
|
|
407
|
-
configDiagnostics = loadResult.diagnostics;
|
|
408
|
-
state = restoreState(ctx);
|
|
409
|
-
if (
|
|
410
|
-
ctx.hasUI &&
|
|
411
|
-
globalConfig.notification &&
|
|
412
|
-
!globalConfigNoticeShown
|
|
413
|
-
) {
|
|
414
|
-
ctx.ui.notify(
|
|
415
|
-
globalConfig.notification,
|
|
416
|
-
globalConfig.status === "migrated" ? "info" : "warning",
|
|
417
|
-
);
|
|
418
|
-
globalConfigNoticeShown = true;
|
|
419
|
-
}
|
|
420
|
-
updateUi(ctx);
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
pi.on("before_agent_start", (event) => {
|
|
424
|
-
const cfg = effectiveConfig();
|
|
425
|
-
if (!cfg.enabled) return undefined;
|
|
426
|
-
loadedContext = loadedContextFromSystemPromptOptions(
|
|
427
|
-
event.systemPromptOptions,
|
|
428
|
-
);
|
|
429
|
-
return { systemPrompt: `${event.systemPrompt}\n\n${AUTO_MODE_GUIDANCE}` };
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
433
|
-
// Enforcement order:
|
|
434
|
-
// 1. permission deny/ask rules,
|
|
435
|
-
// 2. deterministic hard-deny checks that never consult the model,
|
|
436
|
-
// 3. extension-owned read-only inspection tool,
|
|
437
|
-
// 4. deterministic path denials,
|
|
438
|
-
// 5. accepted ask rules force classifier review and skip all allow tiers,
|
|
439
|
-
// 6. inside-CWD, permissions.allow, and read-only allow tiers,
|
|
440
|
-
// 7. classifier for every remaining action, fail-closed on setup/parse errors.
|
|
441
|
-
const cfg = effectiveConfig();
|
|
442
|
-
if (!cfg.enabled) return undefined;
|
|
443
|
-
if (ctx.signal?.aborted) return { block: true, reason: "Cancelled" };
|
|
444
|
-
|
|
445
|
-
const isOwnedInspection = event.toolName === INSPECT_TOOL &&
|
|
446
|
-
ownsInspectionTool();
|
|
447
|
-
const input = event.input as Record<string, unknown>;
|
|
448
|
-
let bashAnalysis: BashAnalysis | undefined;
|
|
449
|
-
if (event.toolName === "bash") {
|
|
450
|
-
const source = typeof input.command === "string" ? input.command : "";
|
|
451
|
-
try {
|
|
452
|
-
bashAnalysis = (options.analyzeBash ?? analyzeBash)(source);
|
|
453
|
-
} catch (error) {
|
|
454
|
-
bashAnalysis = {
|
|
455
|
-
source,
|
|
456
|
-
commands: [],
|
|
457
|
-
redirects: [],
|
|
458
|
-
redirectTargets: [],
|
|
459
|
-
structure: [],
|
|
460
|
-
allowStructureSafe: false,
|
|
461
|
-
errors: [{
|
|
462
|
-
message: `Bash analysis failed: ${
|
|
463
|
-
error instanceof Error ? error.message : String(error)
|
|
464
|
-
}`,
|
|
465
|
-
}],
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
const summary = actionSummary(event.toolName, input);
|
|
470
|
-
if (!isOwnedInspection) state.checkedActions += 1;
|
|
471
|
-
const logCtx: LogCtx = {
|
|
472
|
-
logger: createLogger({
|
|
473
|
-
enabled: cfg.log.enabled,
|
|
474
|
-
classifierIo: cfg.log.classifierIo,
|
|
475
|
-
sessionFile: ctx.sessionManager.getSessionFile?.(),
|
|
476
|
-
sessionDir: ctx.sessionManager.getSessionDir?.() ?? "",
|
|
477
|
-
sessionCwd: ctx.cwd,
|
|
478
|
-
sessionId: ctx.sessionManager.getSessionId?.() ?? "unknown",
|
|
479
|
-
logRoot: options.logRoot,
|
|
480
|
-
now: now(),
|
|
481
|
-
}),
|
|
482
|
-
decisionId: newDecisionId(),
|
|
483
|
-
classifierModel: cfg.classifierModel,
|
|
484
|
-
reasoning: classifierReasoningForConfig(cfg.classifierReasoningLevel),
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
for (const pattern of cfg.permissionDeny) {
|
|
488
|
-
if (
|
|
489
|
-
matchesToolPattern(
|
|
490
|
-
pattern,
|
|
491
|
-
event.toolName,
|
|
492
|
-
input,
|
|
493
|
-
ctx.cwd,
|
|
494
|
-
"match",
|
|
495
|
-
bashAnalysis,
|
|
496
|
-
)
|
|
497
|
-
) {
|
|
498
|
-
const matchedCommand = matchedCommandSummary(
|
|
499
|
-
matchingBashCommandText(pattern, bashAnalysis),
|
|
500
|
-
);
|
|
501
|
-
if (isOwnedInspection) state.checkedActions += 1;
|
|
502
|
-
return block(ctx, {
|
|
503
|
-
timestamp: Date.now(),
|
|
504
|
-
toolName: event.toolName,
|
|
505
|
-
reason: `Blocked by permissions.deny: ${pattern.raw}${
|
|
506
|
-
matchedCommand ? `; matched command: ${matchedCommand}` : ""
|
|
507
|
-
}`,
|
|
508
|
-
action: summary,
|
|
509
|
-
kind: "permissions.deny",
|
|
510
|
-
}, logCtx);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
let askRequiresClassifier = false;
|
|
515
|
-
for (const pattern of cfg.permissionAsk) {
|
|
516
|
-
if (
|
|
517
|
-
!matchesToolPattern(
|
|
518
|
-
pattern,
|
|
519
|
-
event.toolName,
|
|
520
|
-
input,
|
|
521
|
-
ctx.cwd,
|
|
522
|
-
"match",
|
|
523
|
-
bashAnalysis,
|
|
524
|
-
)
|
|
525
|
-
) {
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
if (!ctx.hasUI) {
|
|
529
|
-
const matchedCommand = matchedCommandSummary(
|
|
530
|
-
matchingBashCommandText(pattern, bashAnalysis),
|
|
531
|
-
);
|
|
532
|
-
if (isOwnedInspection) state.checkedActions += 1;
|
|
533
|
-
return block(ctx, {
|
|
534
|
-
timestamp: Date.now(),
|
|
535
|
-
toolName: event.toolName,
|
|
536
|
-
reason:
|
|
537
|
-
`Matched permissions.ask (${pattern.raw})${
|
|
538
|
-
matchedCommand ? ` for command: ${matchedCommand}` : ""
|
|
539
|
-
} but no UI is available`,
|
|
540
|
-
action: summary,
|
|
541
|
-
kind: "permissions.ask",
|
|
542
|
-
}, logCtx);
|
|
543
|
-
}
|
|
544
|
-
const allowed = await ctx.ui.confirm(
|
|
545
|
-
"Auto mode permission ask",
|
|
546
|
-
`Rule: ${pattern.raw}\n\nAction:\n${summary}\n\nAllow this action to continue to auto-mode classification?`,
|
|
547
|
-
{ signal: ctx.signal },
|
|
548
|
-
);
|
|
549
|
-
if (!allowed) {
|
|
550
|
-
const matchedCommand = matchedCommandSummary(
|
|
551
|
-
matchingBashCommandText(pattern, bashAnalysis),
|
|
552
|
-
);
|
|
553
|
-
if (isOwnedInspection) state.checkedActions += 1;
|
|
554
|
-
return block(ctx, {
|
|
555
|
-
timestamp: Date.now(),
|
|
556
|
-
toolName: event.toolName,
|
|
557
|
-
reason: `Declined permissions.ask: ${pattern.raw}${
|
|
558
|
-
matchedCommand ? `; matched command: ${matchedCommand}` : ""
|
|
559
|
-
}`,
|
|
560
|
-
action: summary,
|
|
561
|
-
kind: "permissions.ask",
|
|
562
|
-
}, logCtx);
|
|
563
|
-
}
|
|
564
|
-
askRequiresClassifier = true;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
const deterministicReason = deterministicHardDeny(
|
|
568
|
-
event.toolName,
|
|
569
|
-
input,
|
|
570
|
-
ctx.cwd,
|
|
571
|
-
bashAnalysis,
|
|
572
|
-
);
|
|
573
|
-
if (deterministicReason) {
|
|
574
|
-
if (isOwnedInspection) state.checkedActions += 1;
|
|
575
|
-
return block(ctx, {
|
|
576
|
-
timestamp: Date.now(),
|
|
577
|
-
toolName: event.toolName,
|
|
578
|
-
reason: deterministicReason,
|
|
579
|
-
action: summary,
|
|
580
|
-
kind: "deterministic-hard-deny",
|
|
581
|
-
}, logCtx);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
if (isOwnedInspection && !askRequiresClassifier) return undefined;
|
|
585
|
-
if (isOwnedInspection) state.checkedActions += 1;
|
|
586
|
-
|
|
587
|
-
// Deterministic path gate for file tools.
|
|
588
|
-
//
|
|
589
|
-
// `deniedPaths` always applies: a matching path is hard-denied before any
|
|
590
|
-
// classifier or fast path, so secrets and system dirs never reach the
|
|
591
|
-
// model. `allowInsideWorkingDirectory` adds a deterministic silent-allow
|
|
592
|
-
// tier for file tools whose resolved path is inside the working
|
|
593
|
-
// directory, and routes outside-CWD file access to the classifier
|
|
594
|
-
// (bypassing the read-only fast path so reads outside the tree are
|
|
595
|
-
// reviewed too).
|
|
596
|
-
//
|
|
597
|
-
// The gate is skipped entirely when both features are off, so the
|
|
598
|
-
// default configuration costs no extra filesystem calls.
|
|
599
|
-
let readOnlyFastPath =
|
|
600
|
-
!askRequiresClassifier &&
|
|
601
|
-
!cfg.classifyReadOnlyTools &&
|
|
602
|
-
READ_ONLY_TOOLS.has(event.toolName);
|
|
603
|
-
if (
|
|
604
|
-
(cfg.deniedPaths.length > 0 || cfg.allowInsideWorkingDirectory) &&
|
|
605
|
-
PATH_BEARING_TOOLS.has(event.toolName)
|
|
606
|
-
) {
|
|
607
|
-
const inputPath = extractInputPath(event.toolName, input);
|
|
608
|
-
if (inputPath !== undefined) {
|
|
609
|
-
const resolved =
|
|
610
|
-
resolveToolInputPath(event.toolName, ctx.cwd, inputPath) ??
|
|
611
|
-
inputPath;
|
|
612
|
-
const policyPath = resolvePathForPolicy(resolved) ?? resolved;
|
|
613
|
-
const denied =
|
|
614
|
-
cfg.deniedPaths.length > 0 &&
|
|
615
|
-
(matchesDeniedPath(resolved, cfg.deniedPaths) ||
|
|
616
|
-
matchesDeniedPath(policyPath, cfg.deniedPaths));
|
|
617
|
-
if (denied) {
|
|
618
|
-
return block(ctx, {
|
|
619
|
-
timestamp: Date.now(),
|
|
620
|
-
toolName: event.toolName,
|
|
621
|
-
reason: `Path denied by policy: ${policyPath}`,
|
|
622
|
-
action: summary,
|
|
623
|
-
kind: "deterministic-path-deny",
|
|
624
|
-
}, logCtx);
|
|
625
|
-
}
|
|
626
|
-
let recursiveSearch =
|
|
627
|
-
event.toolName === "grep" || event.toolName === "find";
|
|
628
|
-
if (recursiveSearch) {
|
|
629
|
-
try {
|
|
630
|
-
recursiveSearch = statSync(policyPath).isDirectory();
|
|
631
|
-
} catch {
|
|
632
|
-
// A missing search root will fail in the tool. Treat it as a
|
|
633
|
-
// directory here so a denied scope cannot fail open in a race.
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
const deniedSearchScope =
|
|
637
|
-
recursiveSearch &&
|
|
638
|
-
cfg.deniedPaths.length > 0 &&
|
|
639
|
-
(recursiveSearchMayReachDeniedPath(resolved, cfg.deniedPaths) ||
|
|
640
|
-
recursiveSearchMayReachDeniedPath(
|
|
641
|
-
policyPath,
|
|
642
|
-
cfg.deniedPaths,
|
|
643
|
-
));
|
|
644
|
-
if (deniedSearchScope) {
|
|
645
|
-
return block(ctx, {
|
|
646
|
-
timestamp: Date.now(),
|
|
647
|
-
toolName: event.toolName,
|
|
648
|
-
reason: `Search scope can contain a path denied by policy: ${policyPath}`,
|
|
649
|
-
action: summary,
|
|
650
|
-
kind: "deterministic-path-deny",
|
|
651
|
-
}, logCtx);
|
|
652
|
-
}
|
|
653
|
-
if (cfg.allowInsideWorkingDirectory) {
|
|
654
|
-
const policyCwd = resolvePathForPolicy(ctx.cwd) ?? ctx.cwd;
|
|
655
|
-
if (isInside(policyPath, policyCwd)) {
|
|
656
|
-
// Protected in-tree writes and accepted ask rules must still
|
|
657
|
-
// reach the classifier. They cannot use the inside-CWD tier.
|
|
658
|
-
const protectedWrite =
|
|
659
|
-
(event.toolName === "write" || event.toolName === "edit") &&
|
|
660
|
-
isProtectedPath(policyPath, policyCwd, cfg.protectedPaths);
|
|
661
|
-
if (!askRequiresClassifier && !protectedWrite) {
|
|
662
|
-
return allow(
|
|
663
|
-
ctx,
|
|
664
|
-
"inside-working-directory",
|
|
665
|
-
`Path inside working directory: ${policyPath}`,
|
|
666
|
-
event.toolName,
|
|
667
|
-
summary,
|
|
668
|
-
logCtx,
|
|
669
|
-
);
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
// Outside the working directory, protected writes, and accepted
|
|
673
|
-
// ask rules must not use the read-only fast path.
|
|
674
|
-
readOnlyFastPath = false;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Deterministic allow tier. It runs after every deterministic denial.
|
|
680
|
-
// Accepted ask rules skip this tier and always reach the classifier.
|
|
681
|
-
if (!askRequiresClassifier) {
|
|
682
|
-
if (
|
|
683
|
-
matchesAllowedToolPatterns(
|
|
684
|
-
cfg.permissionAllow,
|
|
685
|
-
event.toolName,
|
|
686
|
-
input,
|
|
687
|
-
ctx.cwd,
|
|
688
|
-
bashAnalysis,
|
|
689
|
-
)
|
|
690
|
-
) {
|
|
691
|
-
// A protected-path write/edit is never covered by permissions.allow;
|
|
692
|
-
// it stays on the classifier path (same rule as the inside-CWD tier).
|
|
693
|
-
let protectedWrite = false;
|
|
694
|
-
if (event.toolName === "write" || event.toolName === "edit") {
|
|
695
|
-
const inputPath = extractInputPath(event.toolName, input);
|
|
696
|
-
const resolved = inputPath === undefined
|
|
697
|
-
? undefined
|
|
698
|
-
: resolveToolInputPath(event.toolName, ctx.cwd, inputPath) ??
|
|
699
|
-
inputPath;
|
|
700
|
-
if (
|
|
701
|
-
resolved !== undefined &&
|
|
702
|
-
isProtectedPath(resolved, ctx.cwd, cfg.protectedPaths)
|
|
703
|
-
) {
|
|
704
|
-
protectedWrite = true;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
if (!protectedWrite) {
|
|
708
|
-
return allow(
|
|
709
|
-
ctx,
|
|
710
|
-
"permissions.allow",
|
|
711
|
-
"Allowed by permissions.allow",
|
|
712
|
-
event.toolName,
|
|
713
|
-
summary,
|
|
714
|
-
logCtx,
|
|
715
|
-
);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
if (readOnlyFastPath) {
|
|
721
|
-
return allow(
|
|
722
|
-
ctx,
|
|
723
|
-
"read-only",
|
|
724
|
-
`Read-only built-in tool: ${event.toolName}`,
|
|
725
|
-
event.toolName,
|
|
726
|
-
summary,
|
|
727
|
-
logCtx,
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
const decision = await classify(
|
|
732
|
-
ctx,
|
|
733
|
-
cfg,
|
|
734
|
-
serializeClassifierAction(event.toolName, input),
|
|
735
|
-
loadedContext,
|
|
736
|
-
);
|
|
737
|
-
logClassifierIo(decision, logCtx);
|
|
738
|
-
if (decision.decision === "allow") {
|
|
739
|
-
state.classifierAllowed += 1;
|
|
740
|
-
return allow(
|
|
741
|
-
ctx,
|
|
742
|
-
"classifier",
|
|
743
|
-
decision.reason,
|
|
744
|
-
event.toolName,
|
|
745
|
-
summary,
|
|
746
|
-
logCtx,
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
state.classifierDenied += 1;
|
|
751
|
-
return block(ctx, {
|
|
752
|
-
timestamp: Date.now(),
|
|
753
|
-
toolName: event.toolName,
|
|
754
|
-
reason: decision.reason,
|
|
755
|
-
action: summary,
|
|
756
|
-
kind: "classifier",
|
|
757
|
-
}, logCtx);
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
pi.registerTool({
|
|
761
|
-
name: INSPECT_TOOL,
|
|
762
|
-
label: "Inspect Auto Mode",
|
|
763
|
-
description:
|
|
764
|
-
"Inspect the active pi-automode status, effective config, built-in defaults, or recent denial metadata. This tool is read-only and cannot enable, disable, reload, reset, or reconfigure auto mode. Its output is sent to the current model; denial reasons and action payloads are omitted.",
|
|
765
|
-
promptSnippet:
|
|
766
|
-
"Inspect active pi-automode state and diagnostic information without changing it",
|
|
767
|
-
parameters: Type.Object({
|
|
768
|
-
action: StringEnum(INSPECTION_ACTIONS, {
|
|
769
|
-
description: "The read-only auto-mode view to return",
|
|
770
|
-
}),
|
|
771
|
-
}),
|
|
772
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
773
|
-
if (signal?.aborted) throw new Error("Auto-mode inspection cancelled");
|
|
774
|
-
const result = inspectAutomode(params.action, ctx);
|
|
775
|
-
return {
|
|
776
|
-
content: [{ type: "text", text: safeJson(result, 16000) }],
|
|
777
|
-
details: result,
|
|
778
|
-
};
|
|
779
|
-
},
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
async function handleAutomodeCommand(
|
|
783
|
-
args: string,
|
|
784
|
-
ctx: ExtensionCommandContext,
|
|
785
|
-
): Promise<void> {
|
|
786
|
-
const [command = "status", ...rest] = args
|
|
787
|
-
.trim()
|
|
788
|
-
.split(/\s+/)
|
|
789
|
-
.filter(Boolean);
|
|
790
|
-
const remainder = rest.join(" ").trim();
|
|
791
|
-
|
|
792
|
-
if (command === "status") {
|
|
793
|
-
ctx.ui.notify(statusText(effectiveConfig(), state), "info");
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
if (command === "on") {
|
|
797
|
-
state.enabledOverride = true;
|
|
798
|
-
persist();
|
|
799
|
-
updateUi(ctx);
|
|
800
|
-
ctx.ui.notify("pi-automode enabled for this session", "info");
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
if (command === "off") {
|
|
804
|
-
state.enabledOverride = false;
|
|
805
|
-
persist();
|
|
806
|
-
updateUi(ctx);
|
|
807
|
-
ctx.ui.notify("pi-automode disabled for this session", "warning");
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
if (command === "reload") {
|
|
811
|
-
loadResult = loadConfigWithDiagnostics(
|
|
812
|
-
ctx.cwd,
|
|
813
|
-
projectIsTrusted(ctx),
|
|
814
|
-
);
|
|
815
|
-
config = loadResult.config;
|
|
816
|
-
configDiagnostics = loadResult.diagnostics;
|
|
817
|
-
persist();
|
|
818
|
-
updateUi(ctx);
|
|
819
|
-
ctx.ui.notify(
|
|
820
|
-
"pi-automode config reloaded",
|
|
821
|
-
configDiagnostics.length > 0 ? "warning" : "info",
|
|
822
|
-
);
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
if (command === "reset") {
|
|
826
|
-
state = {
|
|
827
|
-
checkedActions: 0,
|
|
828
|
-
blockedActions: 0,
|
|
829
|
-
classifierAllowed: 0,
|
|
830
|
-
classifierDenied: 0,
|
|
831
|
-
recentDenials: [],
|
|
832
|
-
enabledOverride: state.enabledOverride,
|
|
833
|
-
};
|
|
834
|
-
persist();
|
|
835
|
-
updateUi(ctx);
|
|
836
|
-
ctx.ui.notify("pi-automode counters reset", "info");
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
if (command === "defaults") {
|
|
840
|
-
ctx.ui.notify(
|
|
841
|
-
safeJson(
|
|
842
|
-
{
|
|
843
|
-
environment: DEFAULT_ENVIRONMENT,
|
|
844
|
-
allow: DEFAULT_ALLOW,
|
|
845
|
-
protectedPaths: DEFAULT_PROTECTED_PATHS,
|
|
846
|
-
soft_deny: DEFAULT_SOFT_DENY,
|
|
847
|
-
hard_deny: DEFAULT_HARD_DENY,
|
|
848
|
-
},
|
|
849
|
-
12000,
|
|
850
|
-
),
|
|
851
|
-
"info",
|
|
852
|
-
);
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
if (command === "config") {
|
|
856
|
-
const logFile = resolveLogPath(
|
|
857
|
-
ctx.sessionManager.getSessionFile?.(),
|
|
858
|
-
ctx.sessionManager.getSessionDir?.() ?? "",
|
|
859
|
-
ctx.sessionManager.getSessionId?.() ?? "unknown",
|
|
860
|
-
ctx.cwd,
|
|
861
|
-
options.logRoot,
|
|
862
|
-
now(),
|
|
863
|
-
);
|
|
864
|
-
ctx.ui.notify(
|
|
865
|
-
safeJson(
|
|
866
|
-
{
|
|
867
|
-
config: effectiveConfig(),
|
|
868
|
-
logFile,
|
|
869
|
-
diagnostics: configDiagnostics,
|
|
870
|
-
},
|
|
871
|
-
16000,
|
|
872
|
-
),
|
|
873
|
-
configDiagnostics.length > 0 ? "warning" : "info",
|
|
874
|
-
);
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
if (command === "denials") {
|
|
878
|
-
ctx.ui.notify(
|
|
879
|
-
formatDenials(state),
|
|
880
|
-
state.recentDenials.length > 0 ? "warning" : "info",
|
|
881
|
-
);
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
if (command === "model") {
|
|
885
|
-
const selected = remainder || await promptForClassifierModel(
|
|
886
|
-
ctx,
|
|
887
|
-
effectiveConfig().classifierModel,
|
|
888
|
-
);
|
|
889
|
-
if (!selected) {
|
|
890
|
-
ctx.ui.notify("Classifier model unchanged", "info");
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
const parsed = parseModelSpec(selected);
|
|
894
|
-
const model = parsed
|
|
895
|
-
? ctx.modelRegistry.find(parsed.provider, parsed.id)
|
|
896
|
-
: undefined;
|
|
897
|
-
if (!model) {
|
|
898
|
-
ctx.ui.notify(`Model not found: ${selected}`, "error");
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
902
|
-
if (!auth.ok) {
|
|
903
|
-
ctx.ui.notify(auth.error, "error");
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
const modelSpec = formatModelSpec(model);
|
|
907
|
-
try {
|
|
908
|
-
saveClassifierModel(modelSpec);
|
|
909
|
-
} catch (error) {
|
|
910
|
-
ctx.ui.notify(
|
|
911
|
-
`Failed to save classifier model: ${
|
|
912
|
-
error instanceof Error ? error.message : String(error)
|
|
913
|
-
}`,
|
|
914
|
-
"error",
|
|
915
|
-
);
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
loadResult = loadConfigWithDiagnostics(
|
|
919
|
-
ctx.cwd,
|
|
920
|
-
projectIsTrusted(ctx),
|
|
921
|
-
);
|
|
922
|
-
config = loadResult.config;
|
|
923
|
-
configDiagnostics = loadResult.diagnostics;
|
|
924
|
-
persist();
|
|
925
|
-
updateUi(ctx);
|
|
926
|
-
const active = effectiveConfig().classifierModel ??
|
|
927
|
-
"current session model";
|
|
928
|
-
ctx.ui.notify(
|
|
929
|
-
active === modelSpec
|
|
930
|
-
? `pi-automode classifier saved globally: ${modelSpec}`
|
|
931
|
-
: `pi-automode classifier saved globally: ${modelSpec}; current config uses ${active}`,
|
|
932
|
-
"info",
|
|
933
|
-
);
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
ctx.ui.notify(
|
|
938
|
-
"Usage: /automode [status|on|off|reload|reset|defaults|config|denials|model [provider/id]]",
|
|
939
|
-
"error",
|
|
940
|
-
);
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
pi.registerCommand("automode", {
|
|
944
|
-
description:
|
|
945
|
-
"Control pi-automode: status, on, off, reload, reset, defaults, config, denials, model",
|
|
946
|
-
handler: handleAutomodeCommand,
|
|
947
|
-
});
|
|
948
|
-
|
|
949
|
-
pi.registerCommand("auto-mode", {
|
|
950
|
-
description: "Alias for /automode",
|
|
951
|
-
handler: handleAutomodeCommand,
|
|
952
|
-
});
|
|
953
|
-
};
|
|
954
|
-
}
|