Files
analysis_claude_code/learn-pi-agent/s11_trust_and_execution_boundary/code.ts
2026-06-16 00:10:35 +08:00

282 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// s11: Trust and Execution Boundary — mini Pi 的第 11 版
//
// 对齐 Pi 真实设计trust 控制资源加载;执行边界不内置 permission靠部署层 containerization。
// 词汇边界:本章新增 ProjectTrust / trust / trusted / untrustedcontainerization 三方案README 讲)。
// 关键:移除了教学版的 ExecutionPolicy/ExecutorPi 里没有executeToolCall 回到无 policys05 版本)。
declare const process: {
argv: string[];
exitCode?: number;
};
// ============ s11 新增:项目信任(控制资源加载)============
// 项目可不可信:决定要不要加载它的资料(防恶意 AGENTS.md / 扩展)。
export type ProjectTrust = "trusted" | "untrusted";
// —— 停止原因s04 起)——
export type StopReason = "stop" | "toolUse" | "error";
// —— 消息 ——
export type UserMessage = { role: "user"; content: string };
export type AssistantMessage = { role: "assistant"; content: string; stopReason: StopReason };
export type ToolResultMessage = { role: "toolResult"; toolCallId: string; content: string };
export type AgentMessage = UserMessage | AssistantMessage | ToolResultMessage;
// —— 会话历史s07 起)——
export type SessionEntry = { id: string; parentId: string | null; message: AgentMessage };
export class SessionTree {
private entries = new Map<string, SessionEntry>();
private activeLeafId: string | null = null;
private counter = 0;
append(message: AgentMessage): SessionEntry {
const entry = { id: `e${++this.counter}`, parentId: this.activeLeafId, message };
this.entries.set(entry.id, entry);
this.activeLeafId = entry.id;
return entry;
}
moveTo(entryId: string): void {
if (!this.entries.has(entryId)) throw new Error(`unknown entry: ${entryId}`);
this.activeLeafId = entryId;
}
currentPath(): AgentMessage[] {
const path: AgentMessage[] = [];
let cursor = this.activeLeafId;
while (cursor) {
const entry = this.entries.get(cursor);
if (!entry) break;
path.push(entry.message);
cursor = entry.parentId;
}
return path.reverse();
}
allEntries(): SessionEntry[] { return [...this.entries.values()]; }
}
export type AgentState = { session: SessionTree; model: string };
// —— 工具契约 ——
export type ToolSpec = { name: string; description: string; input: Record<string, string> };
export type ToolHandler = (input: Record<string, string>) => string;
export type ToolCall = { id: string; name: string; input: Record<string, string> };
export type Tool = { spec: ToolSpec; handler: ToolHandler };
export class ToolRegistry {
private tools = new Map<string, Tool>();
register(tool: Tool): void { this.tools.set(tool.spec.name, tool); }
getSpecs(): ToolSpec[] { return [...this.tools.values()].map((tool) => tool.spec); }
count(): number { return this.tools.size; }
run(call: ToolCall): string {
const tool = this.tools.get(call.name);
if (!tool) return `unknown tool: ${call.name}`;
return tool.handler(call.input);
}
}
// —— 上下文资源s08 起s11load 加 trust 参数U1——
export type ContextResource = { kind: "agents" | "skill" | "prompt"; name: string; content: string };
export class ResourceLoader {
constructor(private resources: ContextResource[]) {}
// [U1 升级] 加 trust 参数。untrusted → 不加载任何资料。默认 trusted。
load(trust: ProjectTrust = "trusted"): ContextResource[] {
if (trust === "untrusted") return [];
return this.resources.map((r) => ({ ...r }));
}
}
export function buildSystemPrompt(resources: ContextResource[]): string {
return resources.map((r) => `[${r.kind}:${r.name}]\n${r.content}`).join("\n\n");
}
// —— provider 对外 ——
export type ProviderMessage =
| { role: "user" | "assistant"; content: string }
| { role: "toolResult"; toolCallId: string; content: string };
export type ProviderInput = { systemPrompt: string; messages: ProviderMessage[]; tools: ToolSpec[] };
export type ProviderEvent =
| { type: "message_start" }
| { type: "text_delta"; text: string }
| { type: "tool_call"; call: ToolCall }
| { type: "message_end"; stopReason: StopReason };
export interface Provider { stream(input: ProviderInput): AsyncGenerator<ProviderEvent>; }
export type Output = { log(line: string): void };
export function createConsoleOutput(): Output { return { log: (line) => console.log(line) }; }
// —— s05 起:执行插口(无 policy——Pi 不内置执行权限)——
export type BeforeToolCallResult = { type: "allow" } | { type: "block"; reason: string };
export type ToolHooks = {
beforeToolCall?: (call: ToolCall) => BeforeToolCallResult;
afterToolCall?: (call: ToolCall, result: string) => string;
};
export function executeToolCall(registry: ToolRegistry, hooks: ToolHooks, call: ToolCall): ToolResultMessage {
const before = hooks.beforeToolCall?.(call) ?? { type: "allow" };
if (before.type === "block") {
return { role: "toolResult", toolCallId: call.id, content: `blocked: ${before.reason}` };
}
let result: string;
try { result = registry.run(call); }
catch (error) { result = `error: ${error instanceof Error ? error.message : String(error)}`; }
const finalResult = hooks.afterToolCall?.(call, result) ?? result;
return { role: "toolResult", toolCallId: call.id, content: finalResult };
}
// —— s06 起快照s11createTurnSnapshot 加 trust传给 load——
export type TurnSnapshot = { systemPrompt: string; messages: ProviderMessage[]; tools: ToolSpec[] };
function toProviderMessages(messages: AgentMessage[]): ProviderMessage[] {
return messages.map((message) => {
if (message.role === "toolResult") {
return { role: "toolResult", toolCallId: message.toolCallId, content: message.content };
}
return { role: message.role, content: message.content };
});
}
export function createTurnSnapshot(
state: AgentState, registry: ToolRegistry, loader: ResourceLoader, trust: ProjectTrust = "trusted",
): TurnSnapshot {
return {
systemPrompt: buildSystemPrompt(loader.load(trust)),
messages: toProviderMessages(state.session.currentPath()),
tools: registry.getSpecs(),
};
}
export function buildProviderInputFromSnapshot(snapshot: TurnSnapshot, state: AgentState): ProviderInput {
return {
systemPrompt: snapshot.systemPrompt,
messages: toProviderMessages(state.session.currentPath()),
tools: snapshot.tools,
};
}
export function createInitialState(model = "demo-small"): AgentState { return { session: new SessionTree(), model }; }
export function createUserMessage(content: string): UserMessage { return { role: "user", content }; }
const MAX_TURNS = 8;
export async function runEventedToolLoop(
state: AgentState, provider: Provider, registry: ToolRegistry,
hooks: ToolHooks, snapshot: TurnSnapshot, output: Output,
): Promise<AssistantMessage> {
let turns = 0;
while (true) {
turns += 1;
if (turns > MAX_TURNS) {
const stopped: AssistantMessage = { role: "assistant", content: "(达到最大轮次,停止)", stopReason: "stop" };
state.session.append(stopped);
return stopped;
}
const providerInput = buildProviderInputFromSnapshot(snapshot, state);
let content = "";
let stopReason: StopReason = "stop";
let sawToolCall = false;
for await (const event of provider.stream(providerInput)) {
if (event.type === "message_start") output.log("message_start");
else if (event.type === "text_delta") { output.log(`text_delta: ${event.text}`); content += event.text; }
else if (event.type === "tool_call") {
sawToolCall = true;
output.log(`tool_call: ${event.call.name}`);
const resultMessage = executeToolCall(registry, hooks, event.call);
state.session.append(resultMessage);
output.log(`tool_result: ${resultMessage.content}`);
} else if (event.type === "message_end") { stopReason = event.stopReason; output.log(`message_end: ${stopReason}`); }
}
if (!sawToolCall || stopReason !== "toolUse") {
const assistant: AssistantMessage = { role: "assistant", content, stopReason };
state.session.append(assistant);
return assistant;
}
}
}
// —— s09 起:扩展运行时(累积)——
export type RuntimeEvent = { type: "message"; content: string } | { type: "done" };
type EventHandler<T extends RuntimeEvent["type"]> = (event: Extract<RuntimeEvent, { type: T }>) => void;
export type Command = { name: string; run: () => string };
export type ExtensionAPI = {
on<T extends RuntimeEvent["type"]>(type: T, handler: EventHandler<T>): void;
registerTool(tool: Tool): void;
registerCommand(command: Command): void;
};
export type Extension = (api: ExtensionAPI) => void;
export class ExtensionRuntime {
private commands = new Map<string, Command>();
private handlers: { type: RuntimeEvent["type"]; handler: (event: RuntimeEvent) => void }[] = [];
constructor(private registry: ToolRegistry) {}
createApi(): ExtensionAPI {
return {
on: (type, handler) => { this.handlers.push({ type, handler: handler as (event: RuntimeEvent) => void }); },
registerTool: (tool) => { this.registry.register(tool); },
registerCommand: (command) => { this.commands.set(command.name, command); },
};
}
use(extension: Extension): void { extension(this.createApi()); }
emit(event: RuntimeEvent): void {
for (const { type, handler } of this.handlers) if (type === event.type) handler(event);
}
runCommand(name: string): string {
const command = this.commands.get(name);
if (!command) return `unknown command: ${name}`;
return command.run();
}
}
// —— s10 起:运行方式(累积)——
export function createDemoRuntimeEvents(input: string): RuntimeEvent[] {
return [{ type: "message", content: `收到:${input}` }, { type: "done" }];
}
export type RuntimeMode = { render(events: RuntimeEvent[]): void };
export class PrintMode implements RuntimeMode {
render(events: RuntimeEvent[]): void {
for (const event of events) if (event.type === "message") console.log(event.content);
}
}
export class JsonMode implements RuntimeMode {
render(events: RuntimeEvent[]): void {
for (const event of events) console.log(JSON.stringify(event));
}
}
// ============ 演示脚手架 ============
function readArg(name: string): string | undefined {
const index = process.argv.indexOf(name);
return index >= 0 ? process.argv[index + 1] : undefined;
}
function main(): void {
const output = createConsoleOutput();
const trust: ProjectTrust = readArg("--trust") === "untrusted" ? "untrusted" : "trusted";
const loader = new ResourceLoader([
{ kind: "agents", name: "AGENTS.md", content: "Use concise engineering explanations." },
]);
output.log("s11: Trust and Execution Boundary");
output.log("");
// 加载边界:看 trust。untrusted → 不加载资料(防恶意资源)。
const resources = loader.load(trust);
output.log("[resources]");
if (resources.length === 0) {
output.log("noneuntrusted不加载任何资料");
} else {
for (const resource of resources) {
output.log(resource.name);
}
}
output.log("");
// 执行边界:对齐 Pi——core 不内置 permission靠部署层 containerization。
output.log("[execution boundary]");
output.log("Pi 不在 core 内限制执行权限。执行边界靠部署层 containerization");
output.log("- OpenShell整个 pi 进程跑在策略控制的沙箱");
output.log("- Gondolinpi 留主机,工具执行路由到 Linux 微虚拟机");
output.log("- Plain Docker整个 pi 进程跑在本地容器");
output.log("core 内唯一的执行拦截点是 s05 的 beforeToolCall hook。");
output.log("");
}
try {
main();
} catch (error: unknown) {
console.error(error);
process.exitCode = 1;
}