Batch 1 — tool-call-parser.ts: replaces xml-parser.ts with a port of
Unsloth's tool_call_parser.py. Adds balanced-brace JSON scanner,
single-param fast path, hasToolSignal/stripToolMarkup/parseToolCallsFromText
exports, and stream-finalization stripping at all three final-write sites
(error-handler, finalizeCompletion, executeToolPhase). Anthropic <invoke>
shape preserved. 75+12 tests.
Batch 2 — web/html-to-md.ts: parse5 tree-walking HTML-to-Markdown converter
ported from Unsloth's _html_to_md.py. Replaces web_fetch's regex stripHtml
with structured markdown output (headings, links, lists, tables, code blocks,
blockquotes, entity decoding). 29 tests.
Batch 3 — llama-args-validator.ts: port of llama_server_args.py deny-list
validator. Wired into AGENTS.md frontmatter parser — llama_extra_args field
validated at load time, rejects managed flags (model identity, networking,
auth/TLS, server UI). No runtime consumer yet (llama-swap boundary). 76 tests.
All three files carry SPDX-License-Identifier: AGPL-3.0-only headers.
LICENSE flipped to AGPL-3.0-only in prior commit (a938cf1).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
161 lines
4.9 KiB
TypeScript
161 lines
4.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
validateExtraArgs,
|
|
isManagedFlag,
|
|
stripShadowingFlags,
|
|
} from '../inference/llama-args-validator.js';
|
|
import { parseAgentsMd } from '../agents.js';
|
|
|
|
describe('validateExtraArgs', () => {
|
|
describe('deny list — each alias rejected', () => {
|
|
const denied = [
|
|
'-m', '--model',
|
|
'-mu', '--model-url',
|
|
'-dr', '--docker-repo',
|
|
'-hf', '-hfr', '--hf-repo',
|
|
'-hff', '--hf-file',
|
|
'-hfv', '-hfrv', '--hf-repo-v',
|
|
'-hffv', '--hf-file-v',
|
|
'-hft', '--hf-token',
|
|
'-mm', '--mmproj',
|
|
'-mmu', '--mmproj-url',
|
|
'--host', '--port', '--path', '--api-prefix', '--reuse-port',
|
|
'--api-key', '--api-key-file',
|
|
'--ssl-key-file', '--ssl-cert-file',
|
|
'--webui', '--no-webui', '--ui', '--no-ui',
|
|
'--ui-config', '--ui-config-file',
|
|
'--ui-mcp-proxy', '--no-ui-mcp-proxy',
|
|
'--models-dir', '--models-preset', '--models-max',
|
|
'--models-autoload', '--no-models-autoload',
|
|
];
|
|
for (const flag of denied) {
|
|
it(`rejects ${flag}`, () => {
|
|
expect(() => validateExtraArgs([flag])).toThrow(/managed/);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('safe flags accepted', () => {
|
|
const safe = [
|
|
'-c', '--ctx-size', '-ngl', '--gpu-layers',
|
|
'--top-k', '--cache-type-k', '--jinja', '--no-jinja',
|
|
'--spec-draft-n-max', '-fa', '--flash-attn',
|
|
'-t', '--threads', '-np', '--parallel',
|
|
];
|
|
for (const flag of safe) {
|
|
it(`accepts ${flag}`, () => {
|
|
expect(() => validateExtraArgs([flag])).not.toThrow();
|
|
expect(validateExtraArgs([flag])).toEqual([flag]);
|
|
});
|
|
}
|
|
});
|
|
|
|
it('handles --flag=value shape (denies the flag part)', () => {
|
|
expect(() => validateExtraArgs(['--model=evil.gguf'])).toThrow(/managed/);
|
|
});
|
|
|
|
it('handles --flag=value shape (accepts safe flag)', () => {
|
|
expect(validateExtraArgs(['--ctx-size=4096'])).toEqual(['--ctx-size=4096']);
|
|
});
|
|
|
|
it('returns empty array for undefined input', () => {
|
|
expect(validateExtraArgs(undefined)).toEqual([]);
|
|
});
|
|
|
|
it('returns empty array for empty input', () => {
|
|
expect(validateExtraArgs([])).toEqual([]);
|
|
});
|
|
|
|
it('treats negative numbers as values, not flags', () => {
|
|
expect(validateExtraArgs(['--seed', '-1'])).toEqual(['--seed', '-1']);
|
|
});
|
|
});
|
|
|
|
describe('isManagedFlag', () => {
|
|
it('returns true for denied flags', () => {
|
|
expect(isManagedFlag('--model')).toBe(true);
|
|
expect(isManagedFlag('-m')).toBe(true);
|
|
expect(isManagedFlag('--api-key')).toBe(true);
|
|
expect(isManagedFlag('--port')).toBe(true);
|
|
});
|
|
|
|
it('returns false for safe flags', () => {
|
|
expect(isManagedFlag('-c')).toBe(false);
|
|
expect(isManagedFlag('--ctx-size')).toBe(false);
|
|
expect(isManagedFlag('--top-k')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('stripShadowingFlags', () => {
|
|
it('strips auto -c when user supplies -c', () => {
|
|
const result = stripShadowingFlags(['-c', '4096', '--top-k', '40']);
|
|
expect(result).toEqual(['--top-k', '40']);
|
|
});
|
|
|
|
it('retains both when no overlap', () => {
|
|
const result = stripShadowingFlags(['--top-k', '40', '--top-p', '0.95']);
|
|
expect(result).toEqual(['--top-k', '40', '--top-p', '0.95']);
|
|
});
|
|
|
|
it('strips --ctx-size=value form', () => {
|
|
const result = stripShadowingFlags(['--ctx-size=4096']);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('strips boolean --jinja flag (no value consumed)', () => {
|
|
const result = stripShadowingFlags(['--jinja', '--top-k', '40']);
|
|
expect(result).toEqual(['--top-k', '40']);
|
|
});
|
|
|
|
it('respects stripContext=false to keep context flags', () => {
|
|
const result = stripShadowingFlags(['-c', '4096'], { stripContext: false });
|
|
expect(result).toEqual(['-c', '4096']);
|
|
});
|
|
|
|
it('strips cache flags by default', () => {
|
|
const result = stripShadowingFlags(['--cache-type-k', 'q8_0']);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('strips spec flags by default', () => {
|
|
const result = stripShadowingFlags(['--spec-draft-n-max', '16']);
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('AGENTS.md frontmatter validation', () => {
|
|
it('rejects agent with managed flag in llama_extra_args', () => {
|
|
const md = `## Evil Agent
|
|
---
|
|
llama_extra_args: ["--model", "evil.gguf"]
|
|
---
|
|
You are evil.`;
|
|
const { agents, errors } = parseAgentsMd(md);
|
|
expect(agents).toHaveLength(0);
|
|
expect(errors).toHaveLength(1);
|
|
expect(errors[0]!.reason).toContain('managed');
|
|
});
|
|
|
|
it('accepts agent with safe llama_extra_args', () => {
|
|
const md = `## Good Agent
|
|
---
|
|
llama_extra_args: ["--top-k", "20"]
|
|
---
|
|
You are good.`;
|
|
const { agents, errors } = parseAgentsMd(md);
|
|
expect(errors).toHaveLength(0);
|
|
expect(agents).toHaveLength(1);
|
|
expect(agents[0]!.llama_extra_args).toEqual(['--top-k', '20']);
|
|
});
|
|
|
|
it('agent without llama_extra_args has null field', () => {
|
|
const md = `## Simple Agent
|
|
---
|
|
temperature: 0.5
|
|
---
|
|
You are simple.`;
|
|
const { agents } = parseAgentsMd(md);
|
|
expect(agents[0]!.llama_extra_args).toBeNull();
|
|
});
|
|
});
|