pi-sage 0.2.4 → 0.2.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/.pi/extensions/sage/index.ts +133 -13
- package/.pi/extensions/sage/settings.ts +28 -10
- package/README.md +2 -2
- package/docs/SAGE_SPEC.md +15 -15
- package/docs/coding-standards.md +1 -1
- package/docs/testing-standards.md +1 -1
- package/package.json +1 -1
|
@@ -39,6 +39,7 @@ const ROLE_HINT_ENV_KEYS = ["TASKPLANE_ROLE", "TASKPLANE_AGENT_ROLE", "PI_AGENT_
|
|
|
39
39
|
|
|
40
40
|
const REASONING_LEVELS: ReasoningLevel[] = ["minimal", "low", "medium", "high", "xhigh"];
|
|
41
41
|
const TOOL_PROFILES: ToolProfile[] = ["none", "read-only-lite", "git-review-readonly", "custom-read-only"];
|
|
42
|
+
const MODEL_INHERIT_VALUE = "inherit";
|
|
42
43
|
|
|
43
44
|
const SAGE_GUIDANCE = [
|
|
44
45
|
"You may call `sage_consult` for high-complexity reasoning, ambiguous debugging, risky architectural decisions, or explicit second-opinion requests.",
|
|
@@ -371,15 +372,18 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
371
372
|
let scope: SettingsScope = loaded.source === "global" ? "global" : "project";
|
|
372
373
|
|
|
373
374
|
while (true) {
|
|
374
|
-
const action = await ctx
|
|
375
|
+
const action = await selectPaged(ctx, "Sage settings", [
|
|
375
376
|
`Enabled: ${onOff(draft.enabled)}`,
|
|
376
377
|
`Autonomous mode: ${onOff(draft.autonomousEnabled)}`,
|
|
378
|
+
`Explicit requests bypass soft limits: ${onOff(draft.explicitRequestAlwaysAllowed)}`,
|
|
377
379
|
`Model: ${draft.model}`,
|
|
378
380
|
`Reasoning level: ${draft.reasoningLevel}`,
|
|
379
381
|
`Timeout ms: ${draft.timeoutMs}`,
|
|
380
382
|
`Max calls/turn: ${draft.maxCallsPerTurn}`,
|
|
381
383
|
`Max calls/session: ${draft.maxCallsPerSession}`,
|
|
382
384
|
`Cooldown turns: ${draft.cooldownTurnsBetweenAutoCalls}`,
|
|
385
|
+
`Max question chars: ${draft.maxQuestionChars}`,
|
|
386
|
+
`Max context chars: ${draft.maxContextChars}`,
|
|
383
387
|
`Tool profile: ${draft.toolPolicy.profile}`,
|
|
384
388
|
`Custom tools: ${(draft.toolPolicy.customAllowedTools ?? []).join(",") || "(none)"}`,
|
|
385
389
|
`Max tool calls: ${draft.toolPolicy.maxToolCalls ?? 10}`,
|
|
@@ -419,8 +423,12 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
419
423
|
draft = { ...draft, autonomousEnabled: !draft.autonomousEnabled };
|
|
420
424
|
continue;
|
|
421
425
|
}
|
|
426
|
+
if (action.startsWith("Explicit requests bypass soft limits:")) {
|
|
427
|
+
draft = { ...draft, explicitRequestAlwaysAllowed: !draft.explicitRequestAlwaysAllowed };
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
422
430
|
if (action.startsWith("Model:")) {
|
|
423
|
-
const selected = await pickModel(ctx);
|
|
431
|
+
const selected = await pickModel(ctx, draft.model);
|
|
424
432
|
if (selected) draft = { ...draft, model: selected };
|
|
425
433
|
continue;
|
|
426
434
|
}
|
|
@@ -447,6 +455,14 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
447
455
|
draft = await setNumberSetting(ctx, draft, "cooldownTurnsBetweenAutoCalls", "Cooldown turns", 0);
|
|
448
456
|
continue;
|
|
449
457
|
}
|
|
458
|
+
if (action.startsWith("Max question chars:")) {
|
|
459
|
+
draft = await setNumberSetting(ctx, draft, "maxQuestionChars", "Max question chars", 256);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (action.startsWith("Max context chars:")) {
|
|
463
|
+
draft = await setNumberSetting(ctx, draft, "maxContextChars", "Max context chars", 512);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
450
466
|
if (action.startsWith("Tool profile:")) {
|
|
451
467
|
const selected = await ctx.ui.select("Tool profile", [...TOOL_PROFILES]);
|
|
452
468
|
if (selected && TOOL_PROFILES.includes(selected as ToolProfile)) {
|
|
@@ -521,23 +537,63 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
521
537
|
}
|
|
522
538
|
}
|
|
523
539
|
|
|
524
|
-
async function pickModel(ctx: ExtensionContext): Promise<string | undefined> {
|
|
540
|
+
async function pickModel(ctx: ExtensionContext, currentModel: string): Promise<string | undefined> {
|
|
525
541
|
const available = ctx.modelRegistry.getAvailable();
|
|
526
542
|
if (available.length === 0) {
|
|
527
543
|
ctx.ui.notify("No available models found", "warning");
|
|
528
544
|
return undefined;
|
|
529
545
|
}
|
|
530
546
|
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
const right = `${b.provider}/${b.id}`.toLowerCase();
|
|
534
|
-
return left.localeCompare(right);
|
|
535
|
-
});
|
|
547
|
+
const currentLower = currentModel.trim().toLowerCase();
|
|
548
|
+
const providers = [...new Set(available.map((model) => model.provider))].sort((a, b) => a.localeCompare(b));
|
|
536
549
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
550
|
+
while (true) {
|
|
551
|
+
const providerOptions = [
|
|
552
|
+
`${MODEL_INHERIT_VALUE} (use current session model)`,
|
|
553
|
+
...providers.map((provider) => {
|
|
554
|
+
const count = available.filter((model) => model.provider === provider).length;
|
|
555
|
+
return `${provider} (${count})`;
|
|
556
|
+
})
|
|
557
|
+
];
|
|
558
|
+
|
|
559
|
+
const providerChoice = await selectPaged(ctx, "Choose Sage model provider", providerOptions);
|
|
560
|
+
if (!providerChoice) return undefined;
|
|
561
|
+
|
|
562
|
+
if (providerChoice.startsWith(MODEL_INHERIT_VALUE)) {
|
|
563
|
+
return MODEL_INHERIT_VALUE;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const provider = providerChoice.replace(/\s*\(\d+\)$/, "");
|
|
567
|
+
const providerModels = available
|
|
568
|
+
.filter((model) => model.provider === provider)
|
|
569
|
+
.sort((a, b) => {
|
|
570
|
+
const aComposite = `${a.provider}/${a.id}`.toLowerCase();
|
|
571
|
+
const bComposite = `${b.provider}/${b.id}`.toLowerCase();
|
|
572
|
+
const aCurrent = aComposite === currentLower;
|
|
573
|
+
const bCurrent = bComposite === currentLower;
|
|
574
|
+
if (aCurrent && !bCurrent) return -1;
|
|
575
|
+
if (!aCurrent && bCurrent) return 1;
|
|
576
|
+
return a.id.localeCompare(b.id);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const modelOptionMap = new Map<string, string>();
|
|
580
|
+
const modelOptions = ["← Back to providers"];
|
|
581
|
+
|
|
582
|
+
for (const model of providerModels) {
|
|
583
|
+
const composite = `${model.provider}/${model.id}`;
|
|
584
|
+
const isCurrent = composite.toLowerCase() === currentLower;
|
|
585
|
+
const option = `${model.id}${isCurrent ? " ✓ current" : ""}`;
|
|
586
|
+
modelOptions.push(option);
|
|
587
|
+
modelOptionMap.set(option, composite);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const modelChoice = await selectPaged(ctx, `Choose Sage model (${provider})`, modelOptions);
|
|
591
|
+
if (!modelChoice) return undefined;
|
|
592
|
+
if (modelChoice === "← Back to providers") continue;
|
|
593
|
+
|
|
594
|
+
const resolved = modelOptionMap.get(modelChoice);
|
|
595
|
+
if (resolved) return resolved;
|
|
596
|
+
}
|
|
541
597
|
}
|
|
542
598
|
|
|
543
599
|
function resolveModelSpec(
|
|
@@ -549,6 +605,22 @@ function resolveModelSpec(
|
|
|
549
605
|
return { modelArg: undefined, reason: "no available models" };
|
|
550
606
|
}
|
|
551
607
|
|
|
608
|
+
if (configuredModel.trim().toLowerCase() === MODEL_INHERIT_VALUE) {
|
|
609
|
+
if (currentModel) {
|
|
610
|
+
const present = availableModels.find(
|
|
611
|
+
(model) => model.provider === currentModel.provider && model.id === currentModel.id
|
|
612
|
+
);
|
|
613
|
+
if (present) {
|
|
614
|
+
return { modelArg: `${present.provider}/${present.id}`, reason: "inherit current model" };
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const firstAvailable = availableModels.at(0);
|
|
619
|
+
return firstAvailable
|
|
620
|
+
? { modelArg: `${firstAvailable.provider}/${firstAvailable.id}`, reason: "inherit fallback first available" }
|
|
621
|
+
: { modelArg: undefined, reason: "no available models" };
|
|
622
|
+
}
|
|
623
|
+
|
|
552
624
|
const exactConfigured = availableModels.find((model) => `${model.provider}/${model.id}` === configuredModel);
|
|
553
625
|
if (exactConfigured) {
|
|
554
626
|
return { modelArg: `${exactConfigured.provider}/${exactConfigured.id}`, reason: "configured exact match" };
|
|
@@ -590,6 +662,48 @@ function resolveModelSpec(
|
|
|
590
662
|
return { modelArg: `${first.provider}/${first.id}`, reason: "first available fallback" };
|
|
591
663
|
}
|
|
592
664
|
|
|
665
|
+
type HasUIContext = { ui: ExtensionContext["ui"] };
|
|
666
|
+
|
|
667
|
+
async function selectPaged(
|
|
668
|
+
ctx: HasUIContext,
|
|
669
|
+
title: string,
|
|
670
|
+
options: string[],
|
|
671
|
+
pageSize = 10
|
|
672
|
+
): Promise<string | undefined> {
|
|
673
|
+
if (options.length <= pageSize) {
|
|
674
|
+
return await ctx.ui.select(title, options);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
let page = 0;
|
|
678
|
+
const totalPages = Math.max(1, Math.ceil(options.length / pageSize));
|
|
679
|
+
const PREV = "← Previous page";
|
|
680
|
+
const NEXT = "→ Next page";
|
|
681
|
+
|
|
682
|
+
while (true) {
|
|
683
|
+
const start = page * pageSize;
|
|
684
|
+
const end = Math.min(start + pageSize, options.length);
|
|
685
|
+
const pageOptions = options.slice(start, end);
|
|
686
|
+
|
|
687
|
+
if (page > 0) pageOptions.push(PREV);
|
|
688
|
+
if (page < totalPages - 1) pageOptions.push(NEXT);
|
|
689
|
+
|
|
690
|
+
const selected = await ctx.ui.select(`${title} (${start + 1}-${end} of ${options.length})`, pageOptions);
|
|
691
|
+
if (!selected) return undefined;
|
|
692
|
+
|
|
693
|
+
if (selected === PREV && page > 0) {
|
|
694
|
+
page -= 1;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (selected === NEXT && page < totalPages - 1) {
|
|
699
|
+
page += 1;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return selected;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
593
707
|
function onOff(value: boolean): string {
|
|
594
708
|
return value ? "on" : "off";
|
|
595
709
|
}
|
|
@@ -597,7 +711,13 @@ function onOff(value: boolean): string {
|
|
|
597
711
|
async function setNumberSetting(
|
|
598
712
|
ctx: ExtensionContext,
|
|
599
713
|
settings: SageSettings,
|
|
600
|
-
key:
|
|
714
|
+
key:
|
|
715
|
+
| "timeoutMs"
|
|
716
|
+
| "maxCallsPerTurn"
|
|
717
|
+
| "maxCallsPerSession"
|
|
718
|
+
| "cooldownTurnsBetweenAutoCalls"
|
|
719
|
+
| "maxQuestionChars"
|
|
720
|
+
| "maxContextChars",
|
|
601
721
|
title: string,
|
|
602
722
|
min: number
|
|
603
723
|
): Promise<SageSettings> {
|
|
@@ -46,18 +46,18 @@ export const DEFAULT_SAGE_SETTINGS: SageSettings = {
|
|
|
46
46
|
autonomousEnabled: true,
|
|
47
47
|
explicitRequestAlwaysAllowed: true,
|
|
48
48
|
invocationScope: "interactive-primary-only",
|
|
49
|
-
model: "
|
|
49
|
+
model: "inherit",
|
|
50
50
|
reasoningLevel: "high",
|
|
51
|
-
timeoutMs:
|
|
51
|
+
timeoutMs: 720000,
|
|
52
52
|
maxCallsPerTurn: 1,
|
|
53
|
-
maxCallsPerSession:
|
|
53
|
+
maxCallsPerSession: 100000,
|
|
54
54
|
cooldownTurnsBetweenAutoCalls: 1,
|
|
55
|
-
maxQuestionChars:
|
|
56
|
-
maxContextChars:
|
|
55
|
+
maxQuestionChars: 12000,
|
|
56
|
+
maxContextChars: 64000,
|
|
57
57
|
toolPolicy: {
|
|
58
|
-
profile: "
|
|
59
|
-
maxToolCalls:
|
|
60
|
-
maxFilesRead:
|
|
58
|
+
profile: "git-review-readonly",
|
|
59
|
+
maxToolCalls: 250,
|
|
60
|
+
maxFilesRead: 100,
|
|
61
61
|
maxBytesPerFile: 200 * 1024,
|
|
62
62
|
maxTotalBytesRead: 1024 * 1024,
|
|
63
63
|
sensitivePathDenylist: DEFAULT_DENYLIST
|
|
@@ -82,6 +82,14 @@ function normalizeNumber(value: unknown): number | undefined {
|
|
|
82
82
|
return value;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function normalizeModelSetting(value: unknown): string | undefined {
|
|
86
|
+
if (typeof value !== "string") return undefined;
|
|
87
|
+
const trimmed = value.trim();
|
|
88
|
+
if (!trimmed) return undefined;
|
|
89
|
+
if (trimmed.toLowerCase() === "inherit-current") return "inherit";
|
|
90
|
+
return trimmed;
|
|
91
|
+
}
|
|
92
|
+
|
|
85
93
|
export function mergeSettings(override: Partial<SageSettings> | undefined): SageSettings {
|
|
86
94
|
const merged: SageSettings = {
|
|
87
95
|
...DEFAULT_SAGE_SETTINGS,
|
|
@@ -92,6 +100,8 @@ export function mergeSettings(override: Partial<SageSettings> | undefined): Sage
|
|
|
92
100
|
}
|
|
93
101
|
};
|
|
94
102
|
|
|
103
|
+
merged.model = normalizeModelSetting(merged.model) ?? DEFAULT_SAGE_SETTINGS.model;
|
|
104
|
+
|
|
95
105
|
merged.timeoutMs = Math.max(1000, Math.floor(merged.timeoutMs));
|
|
96
106
|
merged.maxCallsPerTurn = Math.max(1, Math.floor(merged.maxCallsPerTurn));
|
|
97
107
|
merged.maxCallsPerSession = Math.max(1, Math.floor(merged.maxCallsPerSession));
|
|
@@ -145,7 +155,7 @@ function parseSettingsRaw(content: string): Partial<SageSettings> | undefined {
|
|
|
145
155
|
typeof parsed.explicitRequestAlwaysAllowed === "boolean" ? parsed.explicitRequestAlwaysAllowed : undefined,
|
|
146
156
|
invocationScope:
|
|
147
157
|
parsed.invocationScope === "interactive-primary-only" ? "interactive-primary-only" : undefined,
|
|
148
|
-
model:
|
|
158
|
+
model: normalizeModelSetting(parsed.model),
|
|
149
159
|
reasoningLevel:
|
|
150
160
|
parsed.reasoningLevel === "minimal" ||
|
|
151
161
|
parsed.reasoningLevel === "low" ||
|
|
@@ -194,7 +204,15 @@ export function loadSettings(cwd: string): LoadedSettings {
|
|
|
194
204
|
|
|
195
205
|
export function saveSettings(path: string, settings: SageSettings): void {
|
|
196
206
|
mkdirSync(dirname(path), { recursive: true });
|
|
197
|
-
|
|
207
|
+
|
|
208
|
+
const persisted: Omit<SageSettings, "invocationScope"> & { invocationScope?: SageSettings["invocationScope"] } = {
|
|
209
|
+
...settings
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// invocationScope is currently fixed by policy and not user-configurable in UI.
|
|
213
|
+
delete persisted.invocationScope;
|
|
214
|
+
|
|
215
|
+
writeFileSync(path, `${JSON.stringify(persisted, null, 2)}\n`, "utf8");
|
|
198
216
|
}
|
|
199
217
|
|
|
200
218
|
export function getSettingsPathForScope(cwd: string, scope: SettingsScope): string {
|
package/README.md
CHANGED
|
@@ -44,8 +44,8 @@ Then in Pi run:
|
|
|
44
44
|
|
|
45
45
|
## Tool profiles
|
|
46
46
|
|
|
47
|
-
- `read-only-lite
|
|
48
|
-
- `git-review-readonly
|
|
47
|
+
- `read-only-lite`: `ls, glob, grep, read`
|
|
48
|
+
- `git-review-readonly` (default): adds restricted `bash` for allowlisted **read-only git** commands
|
|
49
49
|
- `none`
|
|
50
50
|
- `custom-read-only`
|
|
51
51
|
|
package/docs/SAGE_SPEC.md
CHANGED
|
@@ -72,7 +72,7 @@ This spec targets a **final architecture** (not MVP): Sage is implemented as a *
|
|
|
72
72
|
- `pi --mode json -p --no-session --model <configured-model> ...`
|
|
73
73
|
- Provides dedicated Sage system prompt.
|
|
74
74
|
- Enforces **single-shot execution** per call (one request → one response).
|
|
75
|
-
- Enforces advisory tool policy (default `
|
|
75
|
+
- Enforces advisory tool policy (default `git-review-readonly`: `ls,glob,grep,read,bash` with strict allowlisted read-only git commands).
|
|
76
76
|
- Supports stricter `none` mode, optional `git-review-readonly` mode, and constrained custom read-only lists.
|
|
77
77
|
- Explicitly disables `sage_consult` inside Sage subprocess context to prevent subagent recursion.
|
|
78
78
|
|
|
@@ -188,15 +188,15 @@ Primary agent guidance to append:
|
|
|
188
188
|
## 8.2 Soft limits (defaults)
|
|
189
189
|
Soft limits are policy/budget controls and are bypassable for explicit user requests (`force=true`).
|
|
190
190
|
- `maxCallsPerTurn: 1`
|
|
191
|
-
- `maxCallsPerSession:
|
|
191
|
+
- `maxCallsPerSession: 100000`
|
|
192
192
|
- `cooldownTurnsBetweenAutoCalls: 1`
|
|
193
|
-
- `maxQuestionChars:
|
|
194
|
-
- `maxContextChars:
|
|
193
|
+
- `maxQuestionChars: 12000`
|
|
194
|
+
- `maxContextChars: 64000`
|
|
195
195
|
|
|
196
196
|
## 8.3 Hard safety limits (non-bypassable)
|
|
197
197
|
- Disallowed caller context (not top-level interactive primary session).
|
|
198
198
|
- Model unavailable or no API key.
|
|
199
|
-
- Timeout exceeded (`timeoutMs`, default `
|
|
199
|
+
- Timeout exceeded (`timeoutMs`, default `720000`).
|
|
200
200
|
- Absolute cost cap exceeded (`maxEstimatedCostPerSession`, optional).
|
|
201
201
|
|
|
202
202
|
## 8.4 Explicit user request behavior
|
|
@@ -294,11 +294,11 @@ Interactive command to configure Sage runtime behavior.
|
|
|
294
294
|
8. **Cooldown between autonomous calls**
|
|
295
295
|
9. **Tool profile for Sage subagent**:
|
|
296
296
|
- none (strict advisor mode)
|
|
297
|
-
- read-only-lite (`ls,glob,grep,read`)
|
|
298
|
-
- git-review-readonly (`ls,glob,grep,read,bash`) with strict allowlisted git read commands only
|
|
297
|
+
- read-only-lite (`ls,glob,grep,read`)
|
|
298
|
+
- git-review-readonly (`ls,glob,grep,read,bash`) with strict allowlisted git read commands only **default**
|
|
299
299
|
- custom read-only list (must exclude mutating/execution tools)
|
|
300
|
-
10. **Per-call max tool calls** (default:
|
|
301
|
-
11. **Per-call max files read** (default:
|
|
300
|
+
10. **Per-call max tool calls** (default: 250)
|
|
301
|
+
11. **Per-call max files read** (default: 100)
|
|
302
302
|
12. **Per-file max bytes** (default: 200KB)
|
|
303
303
|
13. **Per-call max total bytes read** (default: 1MB)
|
|
304
304
|
14. **Sensitive path denylist** (default on; configurable additions)
|
|
@@ -320,7 +320,7 @@ Interactive command to configure Sage runtime behavior.
|
|
|
320
320
|
4. Never pass secrets intentionally unless present in existing prompt/context.
|
|
321
321
|
|
|
322
322
|
### 11.1 Tool-access policy (advisor model)
|
|
323
|
-
1. Default Sage tool profile is `
|
|
323
|
+
1. Default Sage tool profile is `git-review-readonly`: `ls,glob,grep,read,bash` with strict read-only git command allowlist.
|
|
324
324
|
2. Allow `none` profile for stricter environments.
|
|
325
325
|
3. Custom profile must be read-only; mutating/execution tools are disallowed.
|
|
326
326
|
4. Explicitly disallow: `edit`, `write`, git-mutating tools, orchestration-control tools, and network/http tools.
|
|
@@ -330,8 +330,8 @@ Interactive command to configure Sage runtime behavior.
|
|
|
330
330
|
1. Restrict reads to workspace/project roots.
|
|
331
331
|
2. Denylist sensitive paths by default (e.g., `.env*`, `*.pem`, `*.key`, `id_rsa*`, credential stores).
|
|
332
332
|
3. Enforce caps per Sage call:
|
|
333
|
-
- max tool calls: `
|
|
334
|
-
- max files read: `
|
|
333
|
+
- max tool calls: `250`
|
|
334
|
+
- max files read: `100`
|
|
335
335
|
- max bytes per file: `200KB`
|
|
336
336
|
- max total bytes read: `1MB`
|
|
337
337
|
4. Cap `grep`/`glob` result counts to avoid context explosion.
|
|
@@ -379,8 +379,8 @@ On tool-policy/data-policy block:
|
|
|
379
379
|
4. User phrase “get a second opinion” in an eligible interactive primary session causes at least one Sage consultation in same task flow.
|
|
380
380
|
5. Explicit user-requested Sage consultation can bypass soft limits, but still respects hard safety limits and caller-scope restrictions.
|
|
381
381
|
6. No `/sage` command exists.
|
|
382
|
-
7. `/sage-settings`
|
|
383
|
-
8. Default Sage tool profile is `
|
|
382
|
+
7. `/sage-settings` supports model configuration interactively (including `inherit`) and persists selected value.
|
|
383
|
+
8. Default Sage tool profile is `git-review-readonly` (`ls,glob,grep,read,bash` with strict allowlisted read-only git commands), with `none`, `read-only-lite`, and constrained custom read-only options available.
|
|
384
384
|
9. Sage tool profile blocks mutating/execution/network/orchestration tools (including `edit`, `write`, and `sage_consult`), except allowlisted git-read bash commands in `git-review-readonly`.
|
|
385
385
|
10. Sage enforces per-call guardrails (max tool calls/files/bytes and capped `grep`/`glob` output).
|
|
386
386
|
11. Sage calls obey hard safety limits (caller context, model/auth availability, timeout, optional absolute cost cap).
|
|
@@ -399,7 +399,7 @@ On tool-policy/data-policy block:
|
|
|
399
399
|
5. Start with model interpretation for explicit-request detection; add phrase-detection hook only if testing reveals reliability issues.
|
|
400
400
|
6. Sage subagents cannot invoke other Sage subagents (no recursion).
|
|
401
401
|
7. Sage is advisory-only and should not perform implementation actions.
|
|
402
|
-
8. Default Sage tool profile is `
|
|
402
|
+
8. Default Sage tool profile is `git-review-readonly` (`ls,glob,grep,read,bash`) with strict guardrails; `bash` is restricted to allowlisted read-only git commands.
|
|
403
403
|
|
|
404
404
|
---
|
|
405
405
|
|
package/docs/coding-standards.md
CHANGED
|
@@ -74,7 +74,7 @@ Every implementation must preserve these behaviors:
|
|
|
74
74
|
|
|
75
75
|
## 7) Security and Access Controls
|
|
76
76
|
|
|
77
|
-
1. Enforce advisory tool policy by default (`
|
|
77
|
+
1. Enforce advisory tool policy by default (`git-review-readonly`: `ls,glob,grep,read,bash` with strict read-only git command allowlist).
|
|
78
78
|
2. Disallow mutating/execution/network/orchestration tools in Sage.
|
|
79
79
|
3. Restrict filesystem reads to workspace/project roots.
|
|
80
80
|
4. Apply sensitive-path denylist by default (`.env*`, `*.pem`, `*.key`, etc.).
|
|
@@ -125,7 +125,7 @@ Required E2E checks:
|
|
|
125
125
|
- [ ] no recursion
|
|
126
126
|
|
|
127
127
|
### Tool/data policy
|
|
128
|
-
- [ ] default profile is `
|
|
128
|
+
- [ ] default profile is `git-review-readonly`
|
|
129
129
|
- [ ] disallowed tools blocked (`edit`, `write`, `bash`, `sage_consult`, etc.)
|
|
130
130
|
- [ ] denylisted paths blocked
|
|
131
131
|
- [ ] volume caps enforced
|