pi-sage 0.2.3 → 0.2.5
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 +92 -13
- package/.pi/extensions/sage/policy.ts +8 -2
- package/.pi/extensions/sage/settings.ts +28 -10
- package/AGENTS.md +1 -1
- package/README.md +2 -2
- package/docs/SAGE_SPEC.md +29 -26
- 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.",
|
|
@@ -335,6 +336,7 @@ function buildCallerContext(lastInputSource: InputSource | undefined, hasUI: boo
|
|
|
335
336
|
const isRpcSource = lastInputSource === "rpc";
|
|
336
337
|
const isSubagent = process.env.PI_SAGE_SUBAGENT === "1";
|
|
337
338
|
const interactive = hasUI && lastInputSource === "interactive";
|
|
339
|
+
const isInteractiveSupervisor = interactive && roleHint === "supervisor";
|
|
338
340
|
|
|
339
341
|
return {
|
|
340
342
|
session: {
|
|
@@ -343,7 +345,7 @@ function buildCallerContext(lastInputSource: InputSource | undefined, hasUI: boo
|
|
|
343
345
|
agent: {
|
|
344
346
|
role: roleHint ?? "primary",
|
|
345
347
|
isSubagent,
|
|
346
|
-
isRpcOrchestrated: isRpcSource || Boolean(roleHint && roleHint !== "primary")
|
|
348
|
+
isRpcOrchestrated: isRpcSource || Boolean(roleHint && roleHint !== "primary" && !isInteractiveSupervisor)
|
|
347
349
|
},
|
|
348
350
|
runtime: {
|
|
349
351
|
mode: process.env.CI ? "ci" : hasUI ? (isRpcSource ? "rpc" : "interactive") : "non-interactive"
|
|
@@ -373,12 +375,15 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
373
375
|
const action = await ctx.ui.select("Sage settings", [
|
|
374
376
|
`Enabled: ${onOff(draft.enabled)}`,
|
|
375
377
|
`Autonomous mode: ${onOff(draft.autonomousEnabled)}`,
|
|
378
|
+
`Explicit requests bypass soft limits: ${onOff(draft.explicitRequestAlwaysAllowed)}`,
|
|
376
379
|
`Model: ${draft.model}`,
|
|
377
380
|
`Reasoning level: ${draft.reasoningLevel}`,
|
|
378
381
|
`Timeout ms: ${draft.timeoutMs}`,
|
|
379
382
|
`Max calls/turn: ${draft.maxCallsPerTurn}`,
|
|
380
383
|
`Max calls/session: ${draft.maxCallsPerSession}`,
|
|
381
384
|
`Cooldown turns: ${draft.cooldownTurnsBetweenAutoCalls}`,
|
|
385
|
+
`Max question chars: ${draft.maxQuestionChars}`,
|
|
386
|
+
`Max context chars: ${draft.maxContextChars}`,
|
|
382
387
|
`Tool profile: ${draft.toolPolicy.profile}`,
|
|
383
388
|
`Custom tools: ${(draft.toolPolicy.customAllowedTools ?? []).join(",") || "(none)"}`,
|
|
384
389
|
`Max tool calls: ${draft.toolPolicy.maxToolCalls ?? 10}`,
|
|
@@ -418,8 +423,12 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
418
423
|
draft = { ...draft, autonomousEnabled: !draft.autonomousEnabled };
|
|
419
424
|
continue;
|
|
420
425
|
}
|
|
426
|
+
if (action.startsWith("Explicit requests bypass soft limits:")) {
|
|
427
|
+
draft = { ...draft, explicitRequestAlwaysAllowed: !draft.explicitRequestAlwaysAllowed };
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
421
430
|
if (action.startsWith("Model:")) {
|
|
422
|
-
const selected = await pickModel(ctx);
|
|
431
|
+
const selected = await pickModel(ctx, draft.model);
|
|
423
432
|
if (selected) draft = { ...draft, model: selected };
|
|
424
433
|
continue;
|
|
425
434
|
}
|
|
@@ -446,6 +455,14 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
446
455
|
draft = await setNumberSetting(ctx, draft, "cooldownTurnsBetweenAutoCalls", "Cooldown turns", 0);
|
|
447
456
|
continue;
|
|
448
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
|
+
}
|
|
449
466
|
if (action.startsWith("Tool profile:")) {
|
|
450
467
|
const selected = await ctx.ui.select("Tool profile", [...TOOL_PROFILES]);
|
|
451
468
|
if (selected && TOOL_PROFILES.includes(selected as ToolProfile)) {
|
|
@@ -520,23 +537,63 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
520
537
|
}
|
|
521
538
|
}
|
|
522
539
|
|
|
523
|
-
async function pickModel(ctx: ExtensionContext): Promise<string | undefined> {
|
|
540
|
+
async function pickModel(ctx: ExtensionContext, currentModel: string): Promise<string | undefined> {
|
|
524
541
|
const available = ctx.modelRegistry.getAvailable();
|
|
525
542
|
if (available.length === 0) {
|
|
526
543
|
ctx.ui.notify("No available models found", "warning");
|
|
527
544
|
return undefined;
|
|
528
545
|
}
|
|
529
546
|
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
547
|
+
const currentLower = currentModel.trim().toLowerCase();
|
|
548
|
+
const providers = [...new Set(available.map((model) => model.provider))].sort((a, b) => a.localeCompare(b));
|
|
549
|
+
|
|
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 ctx.ui.select("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
|
+
});
|
|
535
578
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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 ctx.ui.select(`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
|
+
}
|
|
540
597
|
}
|
|
541
598
|
|
|
542
599
|
function resolveModelSpec(
|
|
@@ -548,6 +605,22 @@ function resolveModelSpec(
|
|
|
548
605
|
return { modelArg: undefined, reason: "no available models" };
|
|
549
606
|
}
|
|
550
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
|
+
|
|
551
624
|
const exactConfigured = availableModels.find((model) => `${model.provider}/${model.id}` === configuredModel);
|
|
552
625
|
if (exactConfigured) {
|
|
553
626
|
return { modelArg: `${exactConfigured.provider}/${exactConfigured.id}`, reason: "configured exact match" };
|
|
@@ -596,7 +669,13 @@ function onOff(value: boolean): string {
|
|
|
596
669
|
async function setNumberSetting(
|
|
597
670
|
ctx: ExtensionContext,
|
|
598
671
|
settings: SageSettings,
|
|
599
|
-
key:
|
|
672
|
+
key:
|
|
673
|
+
| "timeoutMs"
|
|
674
|
+
| "maxCallsPerTurn"
|
|
675
|
+
| "maxCallsPerSession"
|
|
676
|
+
| "cooldownTurnsBetweenAutoCalls"
|
|
677
|
+
| "maxQuestionChars"
|
|
678
|
+
| "maxContextChars",
|
|
600
679
|
title: string,
|
|
601
680
|
min: number
|
|
602
681
|
): Promise<SageSettings> {
|
|
@@ -22,11 +22,17 @@ export function isEligibleCaller(ctx: CallerContext | null | undefined): CallerD
|
|
|
22
22
|
return { ok: false, blockCode: "subagent", reason: "Sage disabled for subagents" };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
const interactiveSupervisor =
|
|
26
|
+
ctx.agent.role === "supervisor" && ctx.session.interactive === true && ctx.runtime.mode === "interactive";
|
|
27
|
+
|
|
28
|
+
if (ctx.agent.role !== "primary" && !interactiveSupervisor) {
|
|
26
29
|
return { ok: false, blockCode: "ineligible-caller", reason: "Only primary agent may invoke Sage" };
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
return {
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
reason: interactiveSupervisor ? "eligible (interactive supervisor)" : "eligible"
|
|
35
|
+
};
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
export function isHardCostCapExceeded(settings: SageSettings, budgetState: SageBudgetState): boolean {
|
|
@@ -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/AGENTS.md
CHANGED
|
@@ -11,7 +11,7 @@ Sage should improve decision quality for complex tasks while preserving strict s
|
|
|
11
11
|
## 2) Product Invariants (Do Not Violate)
|
|
12
12
|
|
|
13
13
|
1. Sage invocation is restricted to **interactive top-level primary** sessions.
|
|
14
|
-
2. RPC-orchestrated roles
|
|
14
|
+
2. RPC-orchestrated roles cannot invoke Sage. Exception: an interactive top-level `supervisor` context may invoke Sage when treated as the user-facing primary session.
|
|
15
15
|
3. Sage is **single-shot** per call.
|
|
16
16
|
4. Sage cannot recursively invoke Sage.
|
|
17
17
|
5. Sage is **advisory-only** (analysis/recommendations, not implementation execution).
|
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
|
|
|
@@ -182,21 +182,21 @@ Primary agent guidance to append:
|
|
|
182
182
|
- `autonomousEnabled: boolean` (applies only in eligible interactive primary sessions; default: true there)
|
|
183
183
|
- `explicitRequestAlwaysAllowed: boolean` (default: true)
|
|
184
184
|
- `invocationScope: "interactive-primary-only"` (default and recommended)
|
|
185
|
-
- `sage_consult` is callable only from
|
|
186
|
-
- `sage_consult` is blocked for non-interactive/CI contexts and RPC-orchestrated
|
|
185
|
+
- `sage_consult` is callable only from top-level interactive user-facing sessions.
|
|
186
|
+
- `sage_consult` is blocked for non-interactive/CI contexts and RPC-orchestrated roles. Exception: interactive top-level `supervisor` may be treated as primary.
|
|
187
187
|
|
|
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
|
|
@@ -213,7 +213,7 @@ Implement a deterministic `isEligibleCaller(context)` check before any policy/bu
|
|
|
213
213
|
|
|
214
214
|
Required conditions (all must be true):
|
|
215
215
|
1. `context.session.interactive === true`
|
|
216
|
-
2. `context.agent.role === "primary"`
|
|
216
|
+
2. `context.agent.role === "primary"` OR (`context.agent.role === "supervisor"` in interactive top-level context)
|
|
217
217
|
3. `context.agent.isSubagent !== true`
|
|
218
218
|
4. `context.agent.isRpcOrchestrated !== true`
|
|
219
219
|
5. `context.runtime.mode !== "ci"`
|
|
@@ -241,20 +241,23 @@ function isEligibleCaller(ctx: CallerContext): { ok: boolean; blockCode?: string
|
|
|
241
241
|
if (ctx.agent.isSubagent === true) {
|
|
242
242
|
return { ok: false, blockCode: "subagent", reason: "Sage disabled for subagents" };
|
|
243
243
|
}
|
|
244
|
-
|
|
244
|
+
const interactiveSupervisor =
|
|
245
|
+
ctx.agent.role === "supervisor" && ctx.session.interactive === true && ctx.runtime.mode === "interactive";
|
|
246
|
+
if (ctx.agent.role !== "primary" && !interactiveSupervisor) {
|
|
245
247
|
return { ok: false, blockCode: "ineligible-caller", reason: "Only primary agent may invoke Sage" };
|
|
246
248
|
}
|
|
247
|
-
return { ok: true, reason: "eligible" };
|
|
249
|
+
return { ok: true, reason: interactiveSupervisor ? "eligible (interactive supervisor)" : "eligible" };
|
|
248
250
|
}
|
|
249
251
|
```
|
|
250
252
|
|
|
251
253
|
## 8.7 RPC role mapping guidance (orchestration frameworks)
|
|
252
|
-
For orchestrated multi-agent frameworks, map caller role metadata so these roles are
|
|
253
|
-
- `supervisor`
|
|
254
|
+
For orchestrated multi-agent frameworks, map caller role metadata so these roles are ineligible by default:
|
|
254
255
|
- `worker`
|
|
255
256
|
- `reviewer`
|
|
256
257
|
- `merger`
|
|
257
258
|
|
|
259
|
+
`supervisor` is ineligible unless it is the interactive top-level user-facing session.
|
|
260
|
+
|
|
258
261
|
If runtime only provides a session name/string, derive role via conservative matching and deny on ambiguity.
|
|
259
262
|
|
|
260
263
|
---
|
|
@@ -291,11 +294,11 @@ Interactive command to configure Sage runtime behavior.
|
|
|
291
294
|
8. **Cooldown between autonomous calls**
|
|
292
295
|
9. **Tool profile for Sage subagent**:
|
|
293
296
|
- none (strict advisor mode)
|
|
294
|
-
- read-only-lite (`ls,glob,grep,read`)
|
|
295
|
-
- 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**
|
|
296
299
|
- custom read-only list (must exclude mutating/execution tools)
|
|
297
|
-
10. **Per-call max tool calls** (default:
|
|
298
|
-
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)
|
|
299
302
|
12. **Per-file max bytes** (default: 200KB)
|
|
300
303
|
13. **Per-call max total bytes read** (default: 1MB)
|
|
301
304
|
14. **Sensitive path denylist** (default on; configurable additions)
|
|
@@ -317,7 +320,7 @@ Interactive command to configure Sage runtime behavior.
|
|
|
317
320
|
4. Never pass secrets intentionally unless present in existing prompt/context.
|
|
318
321
|
|
|
319
322
|
### 11.1 Tool-access policy (advisor model)
|
|
320
|
-
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.
|
|
321
324
|
2. Allow `none` profile for stricter environments.
|
|
322
325
|
3. Custom profile must be read-only; mutating/execution tools are disallowed.
|
|
323
326
|
4. Explicitly disallow: `edit`, `write`, git-mutating tools, orchestration-control tools, and network/http tools.
|
|
@@ -327,8 +330,8 @@ Interactive command to configure Sage runtime behavior.
|
|
|
327
330
|
1. Restrict reads to workspace/project roots.
|
|
328
331
|
2. Denylist sensitive paths by default (e.g., `.env*`, `*.pem`, `*.key`, `id_rsa*`, credential stores).
|
|
329
332
|
3. Enforce caps per Sage call:
|
|
330
|
-
- max tool calls: `
|
|
331
|
-
- max files read: `
|
|
333
|
+
- max tool calls: `250`
|
|
334
|
+
- max files read: `100`
|
|
332
335
|
- max bytes per file: `200KB`
|
|
333
336
|
- max total bytes read: `1MB`
|
|
334
337
|
4. Cap `grep`/`glob` result counts to avoid context explosion.
|
|
@@ -372,12 +375,12 @@ On tool-policy/data-policy block:
|
|
|
372
375
|
|
|
373
376
|
1. Primary agent autonomously invokes Sage at least once in a complex debug scenario without user explicitly asking (interactive top-level primary session).
|
|
374
377
|
2. In CI/non-interactive mode, Sage invocation is blocked (non-bypassable caller-scope gate).
|
|
375
|
-
3. RPC-orchestrated agents
|
|
378
|
+
3. RPC-orchestrated agents cannot invoke `sage_consult` (interactive top-level `supervisor` exception).
|
|
376
379
|
4. User phrase “get a second opinion” in an eligible interactive primary session causes at least one Sage consultation in same task flow.
|
|
377
380
|
5. Explicit user-requested Sage consultation can bypass soft limits, but still respects hard safety limits and caller-scope restrictions.
|
|
378
381
|
6. No `/sage` command exists.
|
|
379
|
-
7. `/sage-settings`
|
|
380
|
-
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.
|
|
381
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`.
|
|
382
385
|
10. Sage enforces per-call guardrails (max tool calls/files/bytes and capped `grep`/`glob` output).
|
|
383
386
|
11. Sage calls obey hard safety limits (caller context, model/auth availability, timeout, optional absolute cost cap).
|
|
@@ -396,7 +399,7 @@ On tool-policy/data-policy block:
|
|
|
396
399
|
5. Start with model interpretation for explicit-request detection; add phrase-detection hook only if testing reveals reliability issues.
|
|
397
400
|
6. Sage subagents cannot invoke other Sage subagents (no recursion).
|
|
398
401
|
7. Sage is advisory-only and should not perform implementation actions.
|
|
399
|
-
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.
|
|
400
403
|
|
|
401
404
|
---
|
|
402
405
|
|
|
@@ -410,7 +413,7 @@ On tool-policy/data-policy block:
|
|
|
410
413
|
6. Add system-prompt guidance injection for autonomous invocation.
|
|
411
414
|
7. Add budget gates and policy telemetry.
|
|
412
415
|
8. Add rendering polish and acceptance tests.
|
|
413
|
-
9. Add a caller-context + tool-policy test matrix (interactive primary allowed; CI blocked;
|
|
416
|
+
9. Add a caller-context + tool-policy test matrix (interactive primary allowed; interactive supervisor allowed; CI blocked; rpc-orchestrated roles blocked; subagent blocked; unknown context blocked; disallowed tool attempts blocked).
|
|
414
417
|
|
|
415
418
|
---
|
|
416
419
|
|
|
@@ -470,12 +473,12 @@ On tool-policy/data-policy block:
|
|
|
470
473
|
- [ ] Show effective scope/source (project/global/default) and save target.
|
|
471
474
|
|
|
472
475
|
### 17.6 Test checklist
|
|
473
|
-
- [ ] Unit: `isEligibleCaller` allows
|
|
476
|
+
- [ ] Unit: `isEligibleCaller` allows interactive primary (and interactive supervisor exception); blocks CI, non-interactive, subagent, rpc-role, unknown context.
|
|
474
477
|
- [ ] Unit: `resolveToolPolicy` defaults to `read-only-lite` and strips/blocks disallowed custom tools.
|
|
475
478
|
- [ ] Unit: `isPathAllowed` blocks denylisted paths and non-workspace paths.
|
|
476
479
|
- [ ] Unit: `checkVolumeCaps` trips correctly on call/file/byte overages.
|
|
477
480
|
- [ ] Integration: explicit user request with `force=true` bypasses soft limits but not caller gate.
|
|
478
|
-
- [ ] Integration: RPC roles
|
|
481
|
+
- [ ] Integration: RPC-orchestrated roles receive structured blocked result (interactive supervisor exception).
|
|
479
482
|
- [ ] Integration: Sage subprocess cannot call `sage_consult` (recursion test).
|
|
480
483
|
- [ ] Integration: tool usage metadata is present in successful responses.
|
|
481
484
|
- [ ] Integration: blocked results include `blockCode` + human-readable reason.
|
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
|