@inixiative/foundry-core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/capability.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  // a human (or policy hook) approves.
16
16
  // ---------------------------------------------------------------------------
17
17
 
18
- import { ActionQueue, type ActionResolution } from "./action-prompt";
18
+ import type { ActionQueue, ActionResolution } from './action-prompt';
19
19
 
20
20
  // ---------------------------------------------------------------------------
21
21
  // Built-in capabilities
@@ -27,34 +27,34 @@ import { ActionQueue, type ActionResolution } from "./action-prompt";
27
27
  */
28
28
  export type BuiltinCapability =
29
29
  // File system
30
- | "file:read"
31
- | "file:write"
32
- | "file:delete"
30
+ | 'file:read'
31
+ | 'file:write'
32
+ | 'file:delete'
33
33
  // Network
34
- | "net:fetch"
35
- | "net:api"
34
+ | 'net:fetch'
35
+ | 'net:api'
36
36
  // LLM
37
- | "llm:call"
38
- | "llm:expensive"
37
+ | 'llm:call'
38
+ | 'llm:expensive'
39
39
  // Data stores
40
- | "data:read"
41
- | "data:write"
42
- | "data:delete"
40
+ | 'data:read'
41
+ | 'data:write'
42
+ | 'data:delete'
43
43
  // Execution
44
- | "exec:shell"
45
- | "exec:process"
44
+ | 'exec:shell'
45
+ | 'exec:process'
46
46
  // Browser
47
- | "browser:navigate"
48
- | "browser:interact"
49
- | "browser:execute"
50
- | "browser:screenshot"
47
+ | 'browser:navigate'
48
+ | 'browser:interact'
49
+ | 'browser:execute'
50
+ | 'browser:screenshot'
51
51
  // Meta
52
- | "prompt:auto-resolve";
52
+ | 'prompt:auto-resolve';
53
53
 
54
54
  /** Any string is a valid capability. Builtins are just conventions. */
55
55
  export type Capability = BuiltinCapability | (string & {});
56
56
 
57
- export type PermissionLevel = "allow" | "prompt" | "deny";
57
+ export type PermissionLevel = 'allow' | 'prompt' | 'deny';
58
58
 
59
59
  // ---------------------------------------------------------------------------
60
60
  // PermissionPolicy
@@ -71,59 +71,42 @@ export interface PermissionPolicy {
71
71
  protectedPaths?: string[];
72
72
  }
73
73
 
74
- /** Convenience: everything allowed, no prompts. */
75
- export const UNATTENDED_POLICY: PermissionPolicy = {
76
- defaults: "allow",
77
- capabilities: {},
78
- };
79
-
80
- /** Convenience: prompt for writes/deletes/exec/expensive, allow reads. */
81
- export const SUPERVISED_POLICY: PermissionPolicy = {
82
- defaults: "prompt",
83
- capabilities: {
84
- "file:read": "allow",
85
- "data:read": "allow",
86
- "net:fetch": "allow",
87
- "llm:call": "allow",
88
- },
89
- };
90
-
91
74
  /** Convenience: deny dangerous ops, allow reads and cheap LLM. */
92
75
  export const RESTRICTED_POLICY: PermissionPolicy = {
93
- defaults: "deny",
76
+ defaults: 'deny',
94
77
  capabilities: {
95
- "file:read": "allow",
96
- "data:read": "allow",
97
- "net:fetch": "allow",
98
- "llm:call": "allow",
99
- "llm:expensive": "deny",
100
- "exec:shell": "deny",
101
- "exec:process": "deny",
102
- "file:delete": "deny",
103
- "data:delete": "deny",
104
- "browser:navigate": "deny",
105
- "browser:interact": "deny",
106
- "browser:execute": "deny",
107
- "browser:screenshot": "deny",
78
+ 'file:read': 'allow',
79
+ 'data:read': 'allow',
80
+ 'net:fetch': 'allow',
81
+ 'llm:call': 'allow',
82
+ 'llm:expensive': 'deny',
83
+ 'exec:shell': 'deny',
84
+ 'exec:process': 'deny',
85
+ 'file:delete': 'deny',
86
+ 'data:delete': 'deny',
87
+ 'browser:navigate': 'deny',
88
+ 'browser:interact': 'deny',
89
+ 'browser:execute': 'deny',
90
+ 'browser:screenshot': 'deny',
108
91
  },
109
92
  };
110
93
 
111
94
  /** Convenience: browser-capable agent — allows navigation and interaction, prompts for JS execution. */
112
95
  export const BROWSER_POLICY: PermissionPolicy = {
113
- defaults: "prompt",
96
+ defaults: 'prompt',
114
97
  capabilities: {
115
- "file:read": "allow",
116
- "data:read": "allow",
117
- "net:fetch": "allow",
118
- "llm:call": "allow",
119
- "browser:navigate": "allow",
120
- "browser:interact": "allow",
121
- "browser:execute": "prompt",
122
- "browser:screenshot": "allow",
123
- "exec:shell": "deny",
124
- "exec:process": "deny",
125
- "file:delete": "deny",
126
- "data:delete": "deny",
98
+ 'file:read': 'allow',
99
+ 'data:read': 'allow',
100
+ 'net:fetch': 'allow',
101
+ 'llm:call': 'allow',
102
+ 'browser:navigate': 'allow',
103
+ 'browser:interact': 'allow',
104
+ 'browser:execute': 'prompt',
105
+ 'browser:screenshot': 'allow',
106
+ 'exec:shell': 'deny',
107
+ 'exec:process': 'deny',
108
+ 'file:delete': 'deny',
109
+ 'data:delete': 'deny',
127
110
  },
128
111
  };
129
112
 
@@ -136,7 +119,7 @@ export class CapabilityDeniedError extends Error {
136
119
  readonly level: PermissionLevel;
137
120
  constructor(capability: string, level: PermissionLevel) {
138
121
  super(`Capability "${capability}" is ${level}`);
139
- this.name = "CapabilityDeniedError";
122
+ this.name = 'CapabilityDeniedError';
140
123
  this.capability = capability;
141
124
  this.level = level;
142
125
  }
@@ -159,9 +142,11 @@ export class CapabilityGate {
159
142
  this._queue = queue;
160
143
  }
161
144
 
162
- get policy(): PermissionPolicy { return this._policy; }
145
+ get policy(): PermissionPolicy {
146
+ return this._policy;
147
+ }
163
148
 
164
- /** Update policy at runtime (e.g. operator toggles supervised mode). */
149
+ /** Update policy at runtime. */
165
150
  setPolicy(policy: PermissionPolicy): void {
166
151
  this._policy = policy;
167
152
  }
@@ -173,18 +158,18 @@ export class CapabilityGate {
173
158
  async check(capability: Capability, ctx: GateContext): Promise<ActionResolution> {
174
159
  const level = this._resolve(capability, ctx);
175
160
 
176
- if (level === "allow") {
177
- return { by: "policy", action: "approved", timestamp: Date.now() };
161
+ if (level === 'allow') {
162
+ return { by: 'policy', action: 'approved', timestamp: Date.now() };
178
163
  }
179
164
 
180
- if (level === "deny") {
181
- return { by: "policy", action: "rejected", timestamp: Date.now() };
165
+ if (level === 'deny') {
166
+ return { by: 'policy', action: 'rejected', timestamp: Date.now() };
182
167
  }
183
168
 
184
169
  // level === "prompt"
185
170
  const message = this._buildMessage(capability, ctx);
186
171
  return this._queue.prompt({
187
- kind: "approval",
172
+ kind: 'approval',
188
173
  message,
189
174
  agentId: ctx.agentId,
190
175
  threadId: ctx.threadId,
@@ -200,8 +185,8 @@ export class CapabilityGate {
200
185
  */
201
186
  async require(capability: Capability, ctx: GateContext): Promise<void> {
202
187
  const resolution = await this.check(capability, ctx);
203
- if (resolution.action !== "approved") {
204
- throw new CapabilityDeniedError(capability, "deny");
188
+ if (resolution.action !== 'approved') {
189
+ throw new CapabilityDeniedError(capability, 'deny');
205
190
  }
206
191
  }
207
192
 
@@ -221,17 +206,25 @@ export class CapabilityGate {
221
206
  if (explicit) return explicit;
222
207
 
223
208
  // Cost threshold check for LLM calls
224
- if (capability === "llm:call" && this._policy.costThreshold != null && ctx?.meta?.estimatedCost != null) {
209
+ if (
210
+ capability === 'llm:call' &&
211
+ this._policy.costThreshold != null &&
212
+ ctx?.meta?.estimatedCost != null
213
+ ) {
225
214
  if ((ctx.meta.estimatedCost as number) > this._policy.costThreshold) {
226
- return "prompt";
215
+ return 'prompt';
227
216
  }
228
217
  }
229
218
 
230
219
  // Protected paths check for file ops
231
- if ((capability === "file:write" || capability === "file:delete") && this._policy.protectedPaths?.length && ctx?.meta?.path) {
220
+ if (
221
+ (capability === 'file:write' || capability === 'file:delete') &&
222
+ this._policy.protectedPaths?.length &&
223
+ ctx?.meta?.path
224
+ ) {
232
225
  const path = ctx.meta.path as string;
233
226
  if (this._policy.protectedPaths.some((p) => path.includes(p))) {
234
- return "prompt";
227
+ return 'prompt';
235
228
  }
236
229
  }
237
230
 
@@ -239,14 +232,14 @@ export class CapabilityGate {
239
232
  }
240
233
 
241
234
  private _buildMessage(capability: Capability, ctx: GateContext): string {
242
- const detail = ctx.detail ? ` — ${ctx.detail}` : "";
235
+ const detail = ctx.detail ? ` — ${ctx.detail}` : '';
243
236
  return `Agent "${ctx.agentId}" requires capability "${capability}"${detail}`;
244
237
  }
245
238
 
246
- private _urgencyFor(capability: Capability): "low" | "normal" | "high" | "critical" {
247
- if (capability === "exec:shell" || capability === "exec:process") return "high";
248
- if (capability.endsWith(":delete")) return "high";
249
- if (capability === "llm:expensive") return "normal";
250
- return "normal";
239
+ private _urgencyFor(capability: Capability): 'low' | 'normal' | 'high' | 'critical' {
240
+ if (capability === 'exec:shell' || capability === 'exec:process') return 'high';
241
+ if (capability.endsWith(':delete')) return 'high';
242
+ if (capability === 'llm:expensive') return 'normal';
243
+ return 'normal';
251
244
  }
252
245
  }
@@ -0,0 +1,46 @@
1
+ import type { AgentConfig } from './base-agent';
2
+ import type { Classification } from './classifier';
3
+ import type { ContextStack } from './context-stack';
4
+ import { Decider, type DeciderConfig, type Decision } from './decider';
5
+ import type { ExecuteMeta } from './executor';
6
+
7
+ /** The raw message plus the classification already computed upstream. */
8
+ export interface ClarifyPayload {
9
+ readonly message: string;
10
+ readonly classification: Classification;
11
+ }
12
+
13
+ /** needed=true: the request is underspecified and `questions` says what is missing. */
14
+ export interface ClarificationResult {
15
+ readonly needed: boolean;
16
+ readonly questions?: string[];
17
+ readonly reasoning?: string;
18
+ }
19
+
20
+ export type ClarifyHandler = (
21
+ context: string,
22
+ payload: ClarifyPayload,
23
+ meta?: ExecuteMeta,
24
+ ) => Promise<Decision<ClarificationResult>>;
25
+
26
+ export interface ClarifierConfig extends AgentConfig {
27
+ handler: ClarifyHandler;
28
+ }
29
+
30
+ /**
31
+ * A Clarifier is a Decider that checks whether a request is complete enough
32
+ * to execute. Its context should hold only a compact completeness schema
33
+ * (category -> required slots); pair it with a fast model.
34
+ *
35
+ * Sits between "route" and "execute". When needed=true the harness
36
+ * short-circuits and returns the questions instead of dispatching execution.
37
+ */
38
+ export class Clarifier extends Decider<ClarifyPayload, ClarificationResult> {
39
+ constructor(config: ClarifierConfig) {
40
+ super(config as DeciderConfig<ClarifyPayload, ClarificationResult>);
41
+ }
42
+
43
+ override withStack(stack: ContextStack): Clarifier {
44
+ return new Clarifier({ ...this.agentConfig(stack), handler: this._handler });
45
+ }
46
+ }
package/src/classifier.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { Decider, type DeciderConfig, type Decision } from "./decider";
2
- import type { AgentConfig } from "./base-agent";
3
- import type { ContextStack } from "./context-stack";
4
- import type { ExecuteMeta } from "./executor";
1
+ import type { AgentConfig } from './base-agent';
2
+ import type { ContextStack } from './context-stack';
3
+ import { Decider, type DeciderConfig, type Decision } from './decider';
4
+ import type { ExecuteMeta } from './executor';
5
5
 
6
6
  /**
7
7
  * A classification — "what is this thing?"
@@ -15,7 +15,7 @@ export interface Classification {
15
15
  export type ClassifyHandler<TPayload> = (
16
16
  context: string,
17
17
  payload: TPayload,
18
- meta?: ExecuteMeta
18
+ meta?: ExecuteMeta,
19
19
  ) => Promise<Decision<Classification>>;
20
20
 
21
21
  export interface ClassifierConfig<TPayload = unknown> extends AgentConfig {
@@ -29,10 +29,7 @@ export interface ClassifierConfig<TPayload = unknown> extends AgentConfig {
29
29
  * The Classifier has the full taxonomy loaded in its context layers.
30
30
  * The caller sends a payload and gets back a category — not the taxonomy.
31
31
  */
32
- export class Classifier<TPayload = unknown> extends Decider<
33
- TPayload,
34
- Classification
35
- > {
32
+ export class Classifier<TPayload = unknown> extends Decider<TPayload, Classification> {
36
33
  constructor(config: ClassifierConfig<TPayload>) {
37
34
  super(config as DeciderConfig<TPayload, Classification>);
38
35
  }