@czottmann/pi-automode 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +82 -114
- package/docs/GLOSSARY.md +18 -14
- package/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +46 -0
- package/docs/adr/ADR-002-global-config-in-extension-data-directory.md +60 -0
- package/docs/adr/INDEX.md +6 -0
- package/docs/automode-classifier-flow.md +201 -107
- package/docs/configuration.md +171 -0
- package/docs/defaults.md +55 -14
- package/docs/diagnostics.md +90 -0
- package/docs/observability-logging.md +61 -26
- package/examples/automode.local.json +5 -0
- package/extensions/auto-mode/bash.ts +692 -0
- package/extensions/auto-mode/classifier.ts +172 -18
- package/extensions/auto-mode/config.ts +307 -32
- package/extensions/auto-mode/constants.ts +7 -1
- package/extensions/auto-mode/extension.ts +393 -44
- package/extensions/auto-mode/hard-deny.ts +103 -148
- package/extensions/auto-mode/log.ts +60 -5
- package/extensions/auto-mode/paths.ts +124 -14
- package/extensions/auto-mode/permissions.ts +542 -30
- package/extensions/auto-mode/state.ts +1 -0
- package/extensions/auto-mode/types.ts +11 -0
- package/extensions/auto-mode/utils.ts +9 -1
- package/extensions/auto-mode.ts +1 -0
- package/package.json +12 -2
- package/skills/automode-diagnostics/SKILL.md +63 -0
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
linkSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
2
10
|
import { dirname, resolve } from "node:path";
|
|
3
11
|
import {
|
|
4
12
|
DEFAULT_ALLOW,
|
|
5
13
|
DEFAULT_ALLOW_INSIDE_WORKING_DIRECTORY,
|
|
14
|
+
DEFAULT_CLASSIFIER_TIMEOUT_MS,
|
|
6
15
|
DEFAULT_CLASSIFY_READ_ONLY_TOOLS,
|
|
7
16
|
DEFAULT_DENIED_PATHS,
|
|
8
17
|
DEFAULT_ENVIRONMENT,
|
|
@@ -14,10 +23,14 @@ import {
|
|
|
14
23
|
DEFAULT_PROTECTED_PATHS,
|
|
15
24
|
DEFAULT_SOFT_DENY,
|
|
16
25
|
PI_GLOBAL_SETTINGS,
|
|
26
|
+
PI_LEGACY_GLOBAL_SETTINGS,
|
|
17
27
|
PI_PROJECT_LOCAL_SETTINGS,
|
|
18
28
|
PI_PROJECT_SHARED_SETTINGS,
|
|
19
29
|
} from "./constants.ts";
|
|
20
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
MAX_WILDCARD_PATTERN_LENGTH,
|
|
32
|
+
parseToolPattern,
|
|
33
|
+
} from "./permissions.ts";
|
|
21
34
|
import type {
|
|
22
35
|
AutoModeSettings,
|
|
23
36
|
ClassifierReasoningLevel,
|
|
@@ -31,6 +44,169 @@ import type {
|
|
|
31
44
|
} from "./types.ts";
|
|
32
45
|
import { hasOwn, stringArray } from "./utils.ts";
|
|
33
46
|
|
|
47
|
+
export type GlobalConfigPreparation = {
|
|
48
|
+
status: "current" | "migrated" | "conflict" | "failed";
|
|
49
|
+
activePath: string;
|
|
50
|
+
diagnostic?: string;
|
|
51
|
+
notification?: string;
|
|
52
|
+
writeBlockedReason?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type PrepareGlobalConfigOptions = {
|
|
56
|
+
currentPath?: string;
|
|
57
|
+
legacyPath?: string;
|
|
58
|
+
moveFile?: (source: string, destination: string) => void;
|
|
59
|
+
unlinkFile?: (path: string) => void;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function sameFileIdentity(firstPath: string, secondPath: string): boolean {
|
|
63
|
+
try {
|
|
64
|
+
const first = lstatSync(firstPath);
|
|
65
|
+
const second = lstatSync(secondPath);
|
|
66
|
+
return first.dev === second.dev && first.ino === second.ino;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cleanupPublishedDestination(
|
|
73
|
+
destination: string,
|
|
74
|
+
unlinkFile: (path: string) => void,
|
|
75
|
+
originalError: unknown,
|
|
76
|
+
): AggregateError | undefined {
|
|
77
|
+
try {
|
|
78
|
+
unlinkFile(destination);
|
|
79
|
+
return undefined;
|
|
80
|
+
} catch (cleanupError) {
|
|
81
|
+
return new AggregateError(
|
|
82
|
+
[originalError, cleanupError],
|
|
83
|
+
`Could not clean up interrupted Auto Mode config migration at ${destination}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function moveFileWithoutOverwrite(
|
|
89
|
+
source: string,
|
|
90
|
+
destination: string,
|
|
91
|
+
unlinkFile: (path: string) => void,
|
|
92
|
+
): void {
|
|
93
|
+
linkSync(source, destination);
|
|
94
|
+
try {
|
|
95
|
+
unlinkFile(source);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw cleanupPublishedDestination(destination, unlinkFile, error) ?? error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function globalConfigFailure(
|
|
102
|
+
currentPath: string,
|
|
103
|
+
legacyPath: string,
|
|
104
|
+
error: unknown,
|
|
105
|
+
writeBlockedReason?: string,
|
|
106
|
+
): GlobalConfigPreparation {
|
|
107
|
+
const message =
|
|
108
|
+
`Could not move Auto Mode config from ${legacyPath} to ${currentPath}: ${
|
|
109
|
+
error instanceof Error ? error.message : String(error)
|
|
110
|
+
}. Using the legacy config for this session${
|
|
111
|
+
writeBlockedReason ? "; global config writes are disabled" : ""
|
|
112
|
+
}.`;
|
|
113
|
+
return {
|
|
114
|
+
status: "failed",
|
|
115
|
+
activePath: legacyPath,
|
|
116
|
+
diagnostic: message,
|
|
117
|
+
notification: message,
|
|
118
|
+
writeBlockedReason,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function globalConfigConflict(
|
|
123
|
+
currentPath: string,
|
|
124
|
+
legacyPath: string,
|
|
125
|
+
): GlobalConfigPreparation {
|
|
126
|
+
const message =
|
|
127
|
+
`Auto Mode config conflict: using ${currentPath}; legacy config ${legacyPath} is ignored and was not changed.`;
|
|
128
|
+
return {
|
|
129
|
+
status: "conflict",
|
|
130
|
+
activePath: currentPath,
|
|
131
|
+
diagnostic: message,
|
|
132
|
+
notification: message,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Select one global config path for this runtime and migrate a legacy file when possible. */
|
|
137
|
+
export function prepareGlobalConfig(
|
|
138
|
+
options: PrepareGlobalConfigOptions = {},
|
|
139
|
+
): GlobalConfigPreparation {
|
|
140
|
+
const currentPath = options.currentPath ?? PI_GLOBAL_SETTINGS[0];
|
|
141
|
+
const legacyPath = options.legacyPath ?? PI_LEGACY_GLOBAL_SETTINGS;
|
|
142
|
+
const currentExists = existsSync(currentPath);
|
|
143
|
+
const legacyExists = existsSync(legacyPath);
|
|
144
|
+
const unlinkFile = options.unlinkFile ?? unlinkSync;
|
|
145
|
+
|
|
146
|
+
if (currentExists && legacyExists) {
|
|
147
|
+
if (!sameFileIdentity(currentPath, legacyPath)) {
|
|
148
|
+
return globalConfigConflict(currentPath, legacyPath);
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
unlinkFile(legacyPath);
|
|
152
|
+
return {
|
|
153
|
+
status: "migrated",
|
|
154
|
+
activePath: currentPath,
|
|
155
|
+
notification:
|
|
156
|
+
`Completed interrupted Auto Mode config migration from ${legacyPath} to ${currentPath}.`,
|
|
157
|
+
};
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const cleanupError = cleanupPublishedDestination(
|
|
160
|
+
currentPath,
|
|
161
|
+
unlinkFile,
|
|
162
|
+
error,
|
|
163
|
+
);
|
|
164
|
+
return globalConfigFailure(
|
|
165
|
+
currentPath,
|
|
166
|
+
legacyPath,
|
|
167
|
+
cleanupError ?? error,
|
|
168
|
+
cleanupError?.message,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (currentExists || !legacyExists) {
|
|
173
|
+
return { status: "current", activePath: currentPath };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
mkdirSync(dirname(currentPath), { recursive: true });
|
|
178
|
+
const moveFile = options.moveFile ??
|
|
179
|
+
((source: string, destination: string) =>
|
|
180
|
+
moveFileWithoutOverwrite(source, destination, unlinkFile));
|
|
181
|
+
moveFile(legacyPath, currentPath);
|
|
182
|
+
return {
|
|
183
|
+
status: "migrated",
|
|
184
|
+
activePath: currentPath,
|
|
185
|
+
notification: `Moved Auto Mode config from ${legacyPath} to ${currentPath}.`,
|
|
186
|
+
};
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (existsSync(currentPath)) {
|
|
189
|
+
if (!sameFileIdentity(currentPath, legacyPath)) {
|
|
190
|
+
return globalConfigConflict(currentPath, legacyPath);
|
|
191
|
+
}
|
|
192
|
+
const cleanupError = cleanupPublishedDestination(
|
|
193
|
+
currentPath,
|
|
194
|
+
unlinkFile,
|
|
195
|
+
error,
|
|
196
|
+
);
|
|
197
|
+
if (cleanupError) {
|
|
198
|
+
return globalConfigFailure(
|
|
199
|
+
currentPath,
|
|
200
|
+
legacyPath,
|
|
201
|
+
cleanupError,
|
|
202
|
+
cleanupError.message,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return globalConfigFailure(currentPath, legacyPath, error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
34
210
|
function readSettingsFile(path: string): LoadedSettingsFile | undefined {
|
|
35
211
|
if (!existsSync(path)) return undefined;
|
|
36
212
|
try {
|
|
@@ -103,6 +279,7 @@ export function validateSettingsFile(
|
|
|
103
279
|
"enabled",
|
|
104
280
|
"classifierModel",
|
|
105
281
|
"classifierReasoningLevel",
|
|
282
|
+
"classifierTimeoutMs",
|
|
106
283
|
"classifyReadOnlyTools",
|
|
107
284
|
"fastClassifierMaxTokens",
|
|
108
285
|
"allowInsideWorkingDirectory",
|
|
@@ -144,6 +321,15 @@ export function validateSettingsFile(
|
|
|
144
321
|
`${source}: autoMode.classifierReasoningLevel must be one of low, medium, high, xhigh, max`,
|
|
145
322
|
);
|
|
146
323
|
}
|
|
324
|
+
if (
|
|
325
|
+
hasOwn(autoMode, "classifierTimeoutMs") &&
|
|
326
|
+
(!Number.isInteger(autoMode.classifierTimeoutMs) ||
|
|
327
|
+
(autoMode.classifierTimeoutMs as number) < 1000)
|
|
328
|
+
) {
|
|
329
|
+
diagnostics.push(
|
|
330
|
+
`${source}: autoMode.classifierTimeoutMs must be an integer of at least 1000`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
147
333
|
if (
|
|
148
334
|
hasOwn(autoMode, "classifyReadOnlyTools") &&
|
|
149
335
|
typeof autoMode.classifyReadOnlyTools !== "boolean"
|
|
@@ -235,11 +421,11 @@ export function validateSettingsFile(
|
|
|
235
421
|
} else {
|
|
236
422
|
const permissions = settings.permissions as Record<string, unknown>;
|
|
237
423
|
for (const key of Object.keys(permissions)) {
|
|
238
|
-
if (key !== "deny" && key !== "ask") {
|
|
424
|
+
if (key !== "deny" && key !== "ask" && key !== "allow") {
|
|
239
425
|
diagnostics.push(`${source}: unknown permissions key ${key}`);
|
|
240
426
|
}
|
|
241
427
|
}
|
|
242
|
-
for (const key of ["deny", "ask"] as const) {
|
|
428
|
+
for (const key of ["deny", "ask", "allow"] as const) {
|
|
243
429
|
const value = permissions[key];
|
|
244
430
|
if (value === undefined) continue;
|
|
245
431
|
if (!Array.isArray(value)) {
|
|
@@ -253,6 +439,10 @@ export function validateSettingsFile(
|
|
|
253
439
|
diagnostics.push(
|
|
254
440
|
`${source}: permissions.${key}[${index}] must be a tool pattern string`,
|
|
255
441
|
);
|
|
442
|
+
} else if (entry.length > MAX_WILDCARD_PATTERN_LENGTH) {
|
|
443
|
+
diagnostics.push(
|
|
444
|
+
`${source}: permissions.${key}[${index}] must be at most ${MAX_WILDCARD_PATTERN_LENGTH} characters`,
|
|
445
|
+
);
|
|
256
446
|
}
|
|
257
447
|
}
|
|
258
448
|
}
|
|
@@ -265,28 +455,36 @@ export function validateSettingsFile(
|
|
|
265
455
|
type RuleAccumulator = {
|
|
266
456
|
defaults: string[];
|
|
267
457
|
includeDefaults: boolean;
|
|
268
|
-
seen: boolean;
|
|
269
458
|
entries: string[];
|
|
270
459
|
};
|
|
271
460
|
|
|
272
461
|
function createRuleAccumulator(defaults: string[]): RuleAccumulator {
|
|
273
|
-
return { defaults, includeDefaults: true,
|
|
462
|
+
return { defaults, includeDefaults: true, entries: [] };
|
|
274
463
|
}
|
|
275
464
|
|
|
276
|
-
function applyRuleSetting(
|
|
465
|
+
function applyRuleSetting(
|
|
466
|
+
accumulator: RuleAccumulator,
|
|
467
|
+
value: unknown,
|
|
468
|
+
acceptEntry: (entry: string) => boolean = () => true,
|
|
469
|
+
): void {
|
|
277
470
|
const entries = stringArray(value);
|
|
278
471
|
if (!entries) return;
|
|
279
|
-
|
|
280
|
-
|
|
472
|
+
// Any entry that stringArray or acceptEntry drops marks the list malformed.
|
|
473
|
+
// Fail conservative: keep defaults rather than replace them with a partial list.
|
|
474
|
+
let malformed = Array.isArray(value) && value.length !== entries.length;
|
|
281
475
|
for (const entry of entries) {
|
|
282
|
-
if (entry
|
|
476
|
+
if (entry === "$defaults") continue;
|
|
477
|
+
if (acceptEntry(entry)) {
|
|
478
|
+
accumulator.entries.push(entry);
|
|
479
|
+
} else {
|
|
480
|
+
malformed = true;
|
|
481
|
+
}
|
|
283
482
|
}
|
|
483
|
+
accumulator.includeDefaults = entries.includes("$defaults") || malformed;
|
|
284
484
|
}
|
|
285
485
|
|
|
286
486
|
function finalizeRuleSetting(accumulator: RuleAccumulator): string[] {
|
|
287
|
-
const base = accumulator.includeDefaults
|
|
288
|
-
? accumulator.defaults
|
|
289
|
-
: [];
|
|
487
|
+
const base = accumulator.includeDefaults ? accumulator.defaults : [];
|
|
290
488
|
return [...new Set([...base, ...accumulator.entries])];
|
|
291
489
|
}
|
|
292
490
|
|
|
@@ -316,8 +514,8 @@ function mergeLog(
|
|
|
316
514
|
): LogConfig {
|
|
317
515
|
if (!patch) return base;
|
|
318
516
|
return {
|
|
319
|
-
enabled: patch.enabled
|
|
320
|
-
classifierIo: patch.classifierIo
|
|
517
|
+
enabled: typeof patch.enabled === "boolean" ? patch.enabled : base.enabled,
|
|
518
|
+
classifierIo: typeof patch.classifierIo === "boolean" ? patch.classifierIo : base.classifierIo,
|
|
321
519
|
};
|
|
322
520
|
}
|
|
323
521
|
|
|
@@ -345,6 +543,12 @@ function validateDeniedPathsSetting(
|
|
|
345
543
|
);
|
|
346
544
|
continue;
|
|
347
545
|
}
|
|
546
|
+
if (entry.length > MAX_WILDCARD_PATTERN_LENGTH) {
|
|
547
|
+
diagnostics.push(
|
|
548
|
+
`${source}: deniedPaths[${index}] must be at most ${MAX_WILDCARD_PATTERN_LENGTH} characters`,
|
|
549
|
+
);
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
348
552
|
if (!DENIED_PATH_PATTERN_PREFIX.test(entry)) {
|
|
349
553
|
diagnostics.push(
|
|
350
554
|
`${source}: deniedPaths[${index}] "${entry}" can never match a resolved absolute path; start it with *, ~, $HOME, \${HOME}, or / (e.g. "**/${entry}")`,
|
|
@@ -386,6 +590,10 @@ function validFastClassifierBudget(value: unknown): value is number {
|
|
|
386
590
|
return Number.isInteger(value) && Number(value) >= 16;
|
|
387
591
|
}
|
|
388
592
|
|
|
593
|
+
function validClassifierTimeout(value: unknown): value is number {
|
|
594
|
+
return Number.isInteger(value) && Number(value) >= 1000;
|
|
595
|
+
}
|
|
596
|
+
|
|
389
597
|
function applyAutoModeScalars(
|
|
390
598
|
base: EffectiveConfig,
|
|
391
599
|
settings: AutoModeSettings | undefined,
|
|
@@ -393,22 +601,28 @@ function applyAutoModeScalars(
|
|
|
393
601
|
if (!settings) return base;
|
|
394
602
|
return {
|
|
395
603
|
...base,
|
|
396
|
-
enabled: settings.enabled
|
|
604
|
+
enabled: typeof settings.enabled === "boolean" ? settings.enabled : base.enabled,
|
|
397
605
|
classifierModel: settings.classifierModel ?? base.classifierModel,
|
|
398
606
|
classifierReasoningLevel: isClassifierReasoningLevel(
|
|
399
607
|
settings.classifierReasoningLevel,
|
|
400
608
|
)
|
|
401
609
|
? settings.classifierReasoningLevel
|
|
402
610
|
: base.classifierReasoningLevel,
|
|
403
|
-
classifyReadOnlyTools: settings.classifyReadOnlyTools
|
|
404
|
-
|
|
611
|
+
classifyReadOnlyTools: typeof settings.classifyReadOnlyTools === "boolean"
|
|
612
|
+
? settings.classifyReadOnlyTools
|
|
613
|
+
: base.classifyReadOnlyTools,
|
|
405
614
|
allowInsideWorkingDirectory:
|
|
406
|
-
settings.allowInsideWorkingDirectory
|
|
615
|
+
typeof settings.allowInsideWorkingDirectory === "boolean"
|
|
616
|
+
? settings.allowInsideWorkingDirectory
|
|
617
|
+
: base.allowInsideWorkingDirectory,
|
|
407
618
|
fastClassifierMaxTokens: validFastClassifierBudget(
|
|
408
619
|
settings.fastClassifierMaxTokens,
|
|
409
620
|
)
|
|
410
621
|
? settings.fastClassifierMaxTokens
|
|
411
622
|
: base.fastClassifierMaxTokens,
|
|
623
|
+
classifierTimeoutMs: validClassifierTimeout(settings.classifierTimeoutMs)
|
|
624
|
+
? settings.classifierTimeoutMs
|
|
625
|
+
: base.classifierTimeoutMs,
|
|
412
626
|
maxUserTranscriptTokens: validTranscriptBudget(
|
|
413
627
|
settings.maxUserTranscriptTokens,
|
|
414
628
|
)
|
|
@@ -426,11 +640,12 @@ function applyAutoModeScalars(
|
|
|
426
640
|
function appendPermissionPatterns(
|
|
427
641
|
target: ToolPattern[],
|
|
428
642
|
settings: SettingsFile | undefined,
|
|
429
|
-
key: "deny" | "ask",
|
|
643
|
+
key: "deny" | "ask" | "allow",
|
|
430
644
|
): void {
|
|
431
645
|
const values = stringArray(settings?.permissions?.[key]);
|
|
432
646
|
if (!values) return;
|
|
433
647
|
for (const value of values) {
|
|
648
|
+
if (value.length > MAX_WILDCARD_PATTERN_LENGTH) continue;
|
|
434
649
|
const pattern = parseToolPattern(value);
|
|
435
650
|
if (pattern) target.push(pattern);
|
|
436
651
|
}
|
|
@@ -440,8 +655,9 @@ function appendPermissionPatterns(
|
|
|
440
655
|
* Merge settings with Claude Code-style precedence using Pi-owned config files.
|
|
441
656
|
*
|
|
442
657
|
* Important details:
|
|
443
|
-
* - shared project `.pi/automode.json` contributes `permissions
|
|
444
|
-
*
|
|
658
|
+
* - shared project `.pi/automode.json` contributes `permissions.deny` and
|
|
659
|
+
* `permissions.ask` but not `permissions.allow` or `autoMode`, so checked-in
|
|
660
|
+
* config can only add permission barriers;
|
|
445
661
|
* - global, project-local, and inline `autoMode` settings combine additively across scopes;
|
|
446
662
|
* - omitting `$defaults` in any scope for a rule list means "replace built-ins" for that list.
|
|
447
663
|
*/
|
|
@@ -454,6 +670,7 @@ export function buildEffectiveConfigFromSources(
|
|
|
454
670
|
allowInsideWorkingDirectory: DEFAULT_ALLOW_INSIDE_WORKING_DIRECTORY,
|
|
455
671
|
deniedPaths: [...DEFAULT_DENIED_PATHS],
|
|
456
672
|
fastClassifierMaxTokens: DEFAULT_FAST_CLASSIFIER_MAX_TOKENS,
|
|
673
|
+
classifierTimeoutMs: DEFAULT_CLASSIFIER_TIMEOUT_MS,
|
|
457
674
|
maxUserTranscriptTokens: DEFAULT_MAX_USER_TRANSCRIPT_TOKENS,
|
|
458
675
|
maxToolTranscriptTokens: DEFAULT_MAX_TOOL_TRANSCRIPT_TOKENS,
|
|
459
676
|
environment: [...DEFAULT_ENVIRONMENT],
|
|
@@ -463,6 +680,7 @@ export function buildEffectiveConfigFromSources(
|
|
|
463
680
|
hardDeny: [...DEFAULT_HARD_DENY],
|
|
464
681
|
permissionDeny: [],
|
|
465
682
|
permissionAsk: [],
|
|
683
|
+
permissionAllow: [],
|
|
466
684
|
log: { ...DEFAULT_LOG_CONFIG },
|
|
467
685
|
};
|
|
468
686
|
|
|
@@ -488,7 +706,11 @@ export function buildEffectiveConfigFromSources(
|
|
|
488
706
|
applyRuleSetting(environment, settings.autoMode?.environment);
|
|
489
707
|
applyRuleSetting(allow, settings.autoMode?.allow);
|
|
490
708
|
applyRuleSetting(protectedPaths, settings.autoMode?.protectedPaths);
|
|
491
|
-
applyRuleSetting(
|
|
709
|
+
applyRuleSetting(
|
|
710
|
+
deniedPaths,
|
|
711
|
+
settings.autoMode?.deniedPaths,
|
|
712
|
+
(entry) => entry.length <= MAX_WILDCARD_PATTERN_LENGTH,
|
|
713
|
+
);
|
|
492
714
|
applyRuleSetting(
|
|
493
715
|
softDeny,
|
|
494
716
|
settings.autoMode?.soft_deny ?? settings.autoMode?.softDeny,
|
|
@@ -520,6 +742,15 @@ export function buildEffectiveConfigFromSources(
|
|
|
520
742
|
appendPermissionPatterns(config.permissionDeny, settings, "deny");
|
|
521
743
|
appendPermissionPatterns(config.permissionAsk, settings, "ask");
|
|
522
744
|
}
|
|
745
|
+
for (
|
|
746
|
+
const settings of [
|
|
747
|
+
...globalSettings,
|
|
748
|
+
...projectLocalSettings,
|
|
749
|
+
...inlineSettings,
|
|
750
|
+
]
|
|
751
|
+
) {
|
|
752
|
+
appendPermissionPatterns(config.permissionAllow, settings, "allow");
|
|
753
|
+
}
|
|
523
754
|
|
|
524
755
|
return config;
|
|
525
756
|
}
|
|
@@ -536,9 +767,23 @@ function loadedSettingsDiagnostics(
|
|
|
536
767
|
return files.flatMap((file) => file?.diagnostics ?? []);
|
|
537
768
|
}
|
|
538
769
|
|
|
539
|
-
|
|
770
|
+
function ignoredSharedAllowDiagnostics(
|
|
771
|
+
files: Array<LoadedSettingsFile | undefined>,
|
|
772
|
+
): string[] {
|
|
773
|
+
return files.flatMap((file) => {
|
|
774
|
+
const permissions = file?.settings?.permissions;
|
|
775
|
+
if (!file || !permissions || !hasOwn(permissions, "allow")) return [];
|
|
776
|
+
return [
|
|
777
|
+
`${file.path}: permissions.allow is ignored in shared project config. Use a user-owned config source instead`,
|
|
778
|
+
];
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** Load config from disk and environment variables, including diagnostics for `/automode config`. Project files require explicit trust. */
|
|
540
783
|
export function loadEffectiveConfigWithDiagnostics(
|
|
541
784
|
cwd: string,
|
|
785
|
+
projectTrusted = false,
|
|
786
|
+
globalSettingsPath = PI_GLOBAL_SETTINGS[0],
|
|
542
787
|
): ConfigLoadResult {
|
|
543
788
|
const inlineSettings: SettingsFile[] = [];
|
|
544
789
|
const diagnostics: string[] = [];
|
|
@@ -560,18 +805,36 @@ export function loadEffectiveConfigWithDiagnostics(
|
|
|
560
805
|
}
|
|
561
806
|
}
|
|
562
807
|
|
|
563
|
-
const globalFiles =
|
|
564
|
-
const
|
|
565
|
-
|
|
808
|
+
const globalFiles = [readSettingsFile(globalSettingsPath)];
|
|
809
|
+
const projectLocalPaths = PI_PROJECT_LOCAL_SETTINGS.map((file) =>
|
|
810
|
+
resolve(cwd, file)
|
|
566
811
|
);
|
|
567
|
-
const
|
|
568
|
-
|
|
812
|
+
const projectSharedPaths = PI_PROJECT_SHARED_SETTINGS.map((file) =>
|
|
813
|
+
resolve(cwd, file)
|
|
569
814
|
);
|
|
815
|
+
const projectLocalFiles = projectTrusted
|
|
816
|
+
? projectLocalPaths.map(readSettingsFile)
|
|
817
|
+
: [];
|
|
818
|
+
const projectSharedFiles = projectTrusted
|
|
819
|
+
? projectSharedPaths.map(readSettingsFile)
|
|
820
|
+
: [];
|
|
821
|
+
if (!projectTrusted) {
|
|
822
|
+
for (
|
|
823
|
+
const path of [...projectLocalPaths, ...projectSharedPaths].filter(
|
|
824
|
+
existsSync,
|
|
825
|
+
)
|
|
826
|
+
) {
|
|
827
|
+
diagnostics.push(`${path}: ignored because project is not trusted`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
570
830
|
const fileDiagnostics = loadedSettingsDiagnostics([
|
|
571
831
|
...globalFiles,
|
|
572
832
|
...projectLocalFiles,
|
|
573
833
|
...projectSharedFiles,
|
|
574
834
|
]);
|
|
835
|
+
const sharedAllowDiagnostics = ignoredSharedAllowDiagnostics(
|
|
836
|
+
projectSharedFiles,
|
|
837
|
+
);
|
|
575
838
|
|
|
576
839
|
return {
|
|
577
840
|
config: buildEffectiveConfigFromSources({
|
|
@@ -580,13 +843,25 @@ export function loadEffectiveConfigWithDiagnostics(
|
|
|
580
843
|
projectSharedSettings: loadedSettingsToSettings(projectSharedFiles),
|
|
581
844
|
inlineSettings,
|
|
582
845
|
}),
|
|
583
|
-
diagnostics: [
|
|
846
|
+
diagnostics: [
|
|
847
|
+
...fileDiagnostics,
|
|
848
|
+
...sharedAllowDiagnostics,
|
|
849
|
+
...diagnostics,
|
|
850
|
+
],
|
|
584
851
|
};
|
|
585
852
|
}
|
|
586
853
|
|
|
587
854
|
/** Load config from disk and environment variables. Exported for tests and diagnostics. */
|
|
588
|
-
export function loadEffectiveConfig(
|
|
589
|
-
|
|
855
|
+
export function loadEffectiveConfig(
|
|
856
|
+
cwd: string,
|
|
857
|
+
projectTrusted = false,
|
|
858
|
+
globalSettingsPath = PI_GLOBAL_SETTINGS[0],
|
|
859
|
+
): EffectiveConfig {
|
|
860
|
+
return loadEffectiveConfigWithDiagnostics(
|
|
861
|
+
cwd,
|
|
862
|
+
projectTrusted,
|
|
863
|
+
globalSettingsPath,
|
|
864
|
+
).config;
|
|
590
865
|
}
|
|
591
866
|
|
|
592
867
|
function readWritableSettingsFile(path: string): SettingsFile {
|
|
@@ -59,6 +59,9 @@ export const DEFAULT_MAX_USER_TRANSCRIPT_TOKENS = 4000;
|
|
|
59
59
|
export const DEFAULT_MAX_TOOL_TRANSCRIPT_TOKENS = 4000;
|
|
60
60
|
export const DENIAL_HISTORY_LIMIT = 12;
|
|
61
61
|
|
|
62
|
+
/** Per-request timeout for classifier completions (fast and detailed stages). */
|
|
63
|
+
export const DEFAULT_CLASSIFIER_TIMEOUT_MS = 20_000;
|
|
64
|
+
|
|
62
65
|
/** Built-in trusted environment. Users extend this with `$defaults`. */
|
|
63
66
|
export const DEFAULT_ENVIRONMENT = [
|
|
64
67
|
"Trusted repo: the repository pi started in and its configured git remotes.",
|
|
@@ -161,7 +164,10 @@ Valid decision/tier combinations:
|
|
|
161
164
|
- block: hard_deny, soft_deny, or none
|
|
162
165
|
If an allow exception or explicit user intent overrides a soft-deny rule, return allow with tier allow or explicit_intent, never soft_deny.`;
|
|
163
166
|
|
|
164
|
-
export const PI_GLOBAL_SETTINGS = [
|
|
167
|
+
export const PI_GLOBAL_SETTINGS = [
|
|
168
|
+
resolve(HOME, ".pi/agent/extensions/pi-automode/config.json"),
|
|
169
|
+
];
|
|
170
|
+
export const PI_LEGACY_GLOBAL_SETTINGS = resolve(HOME, ".pi/agent/automode.json");
|
|
165
171
|
export const PI_PROJECT_LOCAL_SETTINGS = [".pi/automode.local.json"];
|
|
166
172
|
export const PI_PROJECT_SHARED_SETTINGS = [".pi/automode.json"];
|
|
167
173
|
|