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) };
  }
}