Spaces:
Running
Running
File size: 4,418 Bytes
aa89cb5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | import { UserContext } from './context';
export type ToolRisk = 'MEMORY_ONLY' | 'EXTERNAL_RESOURCE' | 'PRIVILEGED';
export interface ToolToken<T> {
readonly id: string;
readonly risk: ToolRisk;
readonly _type?: T;
}
export interface ToolProvider<T> {
readonly token: ToolToken<T>;
create(context: UserContext): T;
}
export class ToolPolicy {
constructor(
private readonly allowed: ReadonlySet<string>,
private readonly allowMemoryOnly = true,
) {}
static standard(): ToolPolicy { return new ToolPolicy(new Set()); }
static denyAll(): ToolPolicy { return new ToolPolicy(new Set(), false); }
static builder(): ToolPolicyBuilder { return new ToolPolicyBuilder(); }
allows<T>(token: ToolToken<T>): boolean {
return (token.risk === 'MEMORY_ONLY' && this.allowMemoryOnly) || this.allowed.has(token.id);
}
}
export class ToolPolicyBuilder {
private readonly allowed = new Set<string>();
allow<T>(token: ToolToken<T>): this { this.allowed.add(token.id); return this; }
build(): ToolPolicy { return new ToolPolicy(new Set(this.allowed)); }
}
export interface Tools {
has<T>(token: ToolToken<T>): boolean;
get<T>(token: ToolToken<T>): T;
descriptors(): readonly ToolToken<unknown>[];
}
class DefaultTools implements Tools {
private readonly providers = new Map<string, ToolProvider<unknown>>();
constructor(
private readonly context: UserContext,
private readonly policy: ToolPolicy,
providers: readonly ToolProvider<unknown>[],
) {
for (const provider of providers) this.providers.set(provider.token.id, provider);
}
has<T>(token: ToolToken<T>): boolean { return this.providers.has(token.id); }
get<T>(token: ToolToken<T>): T {
const provider = this.providers.get(token.id);
if (!provider) throw new Error(`Tool not available: ${token.id}`);
if (!this.policy.allows(token)) throw new Error(`Tool denied by policy: ${token.id}`);
return provider.create(this.context) as T;
}
descriptors(): readonly ToolToken<unknown>[] {
return [...this.providers.values()].map((provider) => provider.token);
}
}
export class ContextTools {
static builder(context: UserContext): ContextToolsBuilder { return new ContextToolsBuilder(context); }
static of(context: UserContext): Tools { return this.builder(context).build(); }
}
export class ContextToolsBuilder {
private selectedPolicy = ToolPolicy.standard();
private readonly providers: ToolProvider<unknown>[] = [];
constructor(private readonly context: UserContext) {}
policy(policy: ToolPolicy): this { this.selectedPolicy = policy; return this; }
provider<T>(provider: ToolProvider<T>): this {
this.providers.push(provider as ToolProvider<unknown>);
return this;
}
build(): Tools { return new DefaultTools(this.context, this.selectedPolicy, this.providers); }
}
export interface ExecutableHttpTool { execute(): Promise<string>; }
export interface HttpIntentPhase {
purpose(intent: string): ExecutableHttpTool;
auditAs(intent: string): ExecutableHttpTool;
}
export interface HttpTool {
get(url: string): HttpIntentPhase;
post(url: string, body: unknown): HttpIntentPhase;
}
export const HTTP_TOOL: ToolToken<HttpTool> = Object.freeze({
id: 'http', risk: 'EXTERNAL_RESOURCE' as const,
});
export class FetchHttpToolProvider implements ToolProvider<HttpTool> {
readonly token = HTTP_TOOL;
constructor(private readonly fetchImpl: typeof fetch = fetch) {}
create(_context: UserContext): HttpTool {
const stage = (method: 'GET' | 'POST', url: string, body?: unknown): HttpIntentPhase => ({
purpose: (intent) => executable(method, url, body, intent),
auditAs: (intent) => executable(method, url, body, intent),
});
const executable = (
method: 'GET' | 'POST', url: string, body: unknown, intent: string,
): ExecutableHttpTool => ({
execute: async () => {
if (!intent.trim()) throw new Error('HTTP tool execution requires non-empty intent');
const response = await this.fetchImpl(url, method === 'GET' ? { method } : {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`HTTP tool failed: ${response.status}`);
return response.text();
},
});
return { get: (url) => stage('GET', url), post: (url, body) => stage('POST', url, body) };
}
}
|