feat: confucius-aligned agent stack - #34
Conversation
Made-with: Cursor
|
Hey @iotserver24! 👋 I'll go through the changes and help you out with an automated review! 🔍 Starting the review now... |
There was a problem hiding this comment.
Pull request overview
This PR adds a “Confucius/Cooper-aligned” agent stack to XibeCode, focusing on cost controls, security hardening, hierarchical execution (plan-first), session memory, context pruning, and a self-correction loop for run-pr.
Changes:
- Add economy mode configuration + CLI flags, plus multi-model routing (planning vs execution) and optional plan-first behavior.
- Add session memory persistence and lightweight context pruning to reduce repetition and token usage.
- Add security utilities for path traversal and URL validation; introduce synthesized session-scoped tools and
run-prself-correction retries; expand docs + CI.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/safety.ts | Adds sanitizePath / sanitizeUrl for path traversal + SSRF reduction. |
| src/utils/config.ts | Adds economy/context-pruning/multi-model config fields and getters. |
| src/index.ts | Exposes new CLI flags for cost mode, plan-first, and mindset mode. |
| src/core/tools.ts | Adds synthesized tools, path/URL sanitization usage, and fetch URL hardening. |
| src/core/session-memory.ts | Implements persisted session memory (attempts + learnings + summary injection). |
| src/core/modes.ts | Categorizes synthesize_tool for mode permissioning. |
| src/core/context-pruner.ts | Adds keyword-based context pruning with optional code-graph augmentation. |
| src/core/agent.ts | Adds reasoning tiers, plan-first flow, mindset switching, session summary injection, and model routing. |
| src/commands/run.ts | Wires economy mode, session memory, and context pruning into run. |
| src/commands/run-pr.ts | Wires economy mode/session memory/context pruning and adds a self-correction retry loop on test failures. |
| src/commands/config.ts | Adds config setters for cost mode and economy model. |
| src/commands/chat.ts | Adds economy-mode model selection for chat. |
| SECURITY.md | Documents new security posture and features. |
| README.md | Documents platform/device support and economy-mode CLI usage. |
| DOCS.md | Adds economy mode docs, session memory, context pruning, and platform/device matrix. |
| .github/workflows/ci.yml | Adds ARM runner to matrix and a security audit step. |
Comments suppressed due to low confidence (1)
src/core/tools.ts:2857
fetch_urlsanitizes the initial URL, butfetch()will follow redirects by default. A public URL could 30x-redirect to a blocked/private destination (SSRF) after passingsanitizeUrl. Consider settingredirect: 'manual'(orerror) and/or re-validating the finalresponse.urlafter redirects before reading the body.
const urlResult = sanitizeUrl(url.trim());
if (!urlResult.ok) {
return { error: true, success: false, message: urlResult.message };
}
try {
const response = await fetch(urlResult.url, {
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,text/plain,application/json',
},
signal: AbortSignal.timeout(15000),
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| export function sanitizePath(workingDir: string, filePath: string): { ok: true; path: string } | { ok: false; message: string } { | ||
| const normalized = path.normalize(filePath).replace(/^(\.\.(\/|\\))+/, ''); | ||
| const resolved = path.resolve(workingDir, normalized); | ||
| const relative = path.relative(workingDir, resolved); | ||
| if (relative.startsWith('..') || path.isAbsolute(relative)) { | ||
| return { ok: false, message: 'Path escapes working directory and is not allowed' }; | ||
| } | ||
| return { ok: true, path: resolved }; |
| /** Max files to suggest from context pruning; 0 means disabled. */ | ||
| getMaxContextFiles(): number { | ||
| const v = this.get('maxContextFiles'); | ||
| return v !== undefined && v !== null ? Number(v) : 40; |
| try { | ||
| if (this.dynamicTools.has(toolName)) { | ||
| return this.runDynamicTool(toolName, p); | ||
| } | ||
| switch (toolName) { |
| const reserved = new Set(['read_file', 'write_file', 'run_command', 'synthesize_tool', 'get_context']); | ||
| if (reserved.has(name)) { | ||
| return { error: true, success: false, message: `Cannot override built-in tool: ${name}` }; | ||
| } | ||
| this.dynamicTools.set(name, { description: description || name, script }); | ||
| return { success: true, message: `Tool "${name}" registered. You can call it with the same name. Execution is sandboxed.` }; |
| export function sanitizePath(workingDir: string, filePath: string): { ok: true; path: string } | { ok: false; message: string } { | ||
| const normalized = path.normalize(filePath).replace(/^(\.\.(\/|\\))+/, ''); | ||
| const resolved = path.resolve(workingDir, normalized); | ||
| const relative = path.relative(workingDir, resolved); | ||
| if (relative.startsWith('..') || path.isAbsolute(relative)) { | ||
| return { ok: false, message: 'Path escapes working directory and is not allowed' }; | ||
| } | ||
| return { ok: true, path: resolved }; | ||
| } | ||
|
|
||
| /** | ||
| * Validate URL for fetch_url to reduce SSRF risk: only http/https, no localhost or private IPs by default. | ||
| */ | ||
| export function sanitizeUrl(url: string, allowLocalhost = false): { ok: true; url: string } | { ok: false; message: string } { | ||
| let parsed: URL; | ||
| try { | ||
| parsed = new URL(url); | ||
| } catch { | ||
| return { ok: false, message: 'Invalid URL' }; | ||
| } | ||
| if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { | ||
| return { ok: false, message: 'Only http and https URLs are allowed' }; | ||
| } | ||
| if (!allowLocalhost) { | ||
| const host = (parsed.hostname || '').toLowerCase(); | ||
| if (host === 'localhost' || host === '127.0.0.1' || host.startsWith('192.168.') || host.startsWith('10.') || host.endsWith('.local')) { | ||
| return { ok: false, message: 'Local or private URLs are not allowed' }; | ||
| } | ||
| } | ||
| return { ok: true, url: parsed.toString() }; |
| export async function pruneContext( | ||
| workingDir: string, | ||
| task: string, | ||
| options: PruneOptions = {} | ||
| ): Promise<string[]> { | ||
| const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES; | ||
| const extensions = options.extensions ?? DEFAULT_EXTENSIONS; | ||
| const useContent = options.useContent ?? false; | ||
| const usePkgStyleContext = options.usePkgStyleContext ?? false; | ||
|
|
||
| const words = taskWords(task); | ||
| const patterns = extensions.map(ext => `**/${ext}`); | ||
| const ignore = IGNORE_DIRS.map(d => `**/${d}/**`); | ||
| const files = words.size > 0 | ||
| ? await glob(patterns, { cwd: workingDir, absolute: false, ignore, onlyFiles: true }) | ||
| : []; | ||
|
|
||
| const scored: { path: string; score: number }[] = []; | ||
|
|
||
| for (const rel of files) { | ||
| let content: string | null = null; | ||
| if (useContent) { | ||
| try { | ||
| const full = path.join(workingDir, rel); | ||
| const buf = await fs.readFile(full, 'utf-8').catch(() => ''); | ||
| content = buf.slice(0, 500); | ||
| } catch { | ||
| // skip content | ||
| } | ||
| } | ||
| const score = scorePathAndContent(rel, content, words); | ||
| scored.push({ path: rel, score }); | ||
| } | ||
|
|
||
| scored.sort((a, b) => b.score - a.score); | ||
|
|
||
| const withScore = scored.filter(s => s.score > 0); | ||
| let top = withScore.length > 0 | ||
| ? withScore.slice(0, maxFiles).map(s => s.path) | ||
| : scored.slice(0, maxFiles).map(s => s.path); |
| export function sanitizeUrl(url: string, allowLocalhost = false): { ok: true; url: string } | { ok: false; message: string } { | ||
| let parsed: URL; | ||
| try { | ||
| parsed = new URL(url); | ||
| } catch { | ||
| return { ok: false, message: 'Invalid URL' }; | ||
| } | ||
| if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { | ||
| return { ok: false, message: 'Only http and https URLs are allowed' }; | ||
| } | ||
| if (!allowLocalhost) { | ||
| const host = (parsed.hostname || '').toLowerCase(); | ||
| if (host === 'localhost' || host === '127.0.0.1' || host.startsWith('192.168.') || host.startsWith('10.') || host.endsWith('.local')) { | ||
| return { ok: false, message: 'Local or private URLs are not allowed' }; | ||
| } | ||
| } | ||
| return { ok: true, url: parsed.toString() }; |
| */ | ||
| private async runDynamicTool(toolName: string, _input: any): Promise<any> { | ||
| const def = this.dynamicTools.get(toolName); | ||
| if (!def) return { error: true, success: false, message: `Dynamic tool "${toolName}" not found` }; |
|
|
||
| // ── Config ─────────────────────────────────────────────────────────────── | ||
| const model = options.model || config.getModel(); | ||
| const costMode = (options.costMode || config.getCostMode()) as 'normal' | 'economy'; |
| const params: any = { | ||
| model: this.config.model, | ||
| model: this.getModelForTier(), | ||
| max_tokens: 8192, | ||
| messages: this.messages, | ||
| system: this.getSystemPrompt(), |
|
@copilot explain whether this makes this ai tool better at coding or not |
|
@iotserver24 I've opened a new pull request, #35, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
Xibe |
|
Hey @iotserver24! 👋 I'll go through the changes and help you out! 🔍 Starting the review now... |
feat: confucius-aligned agent stack — economy mode, session memory, context pruning, self-correction
feat: confucius-aligned agent stack
Implements Confucius/Cooper-aligned architecture: economy mode, security hardening, AX-lite reasoning tiers, session memory, context pruning, meta-tool synthesis, self-correction loop, multi-model routing, CoM-inspired mindsets, and device/ARM/mobile support.