@particle-academy/prism-harness 0.8.0 → 0.9.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/README.md +85 -15
- package/dist/address-index.d.ts +35 -0
- package/dist/address-index.js +170 -0
- package/dist/address.d.ts +25 -0
- package/dist/address.js +83 -0
- package/dist/harness.d.ts +41 -2
- package/dist/harness.js +99 -4
- package/dist/index.d.ts +11 -5
- package/dist/index.js +4 -1
- package/dist/modes.d.ts +10 -1
- package/dist/modes.js +53 -1
- package/dist/registration.d.ts +6 -0
- package/dist/registration.js +15 -0
- package/dist/runtime.d.ts +21 -15
- package/dist/runtime.js +85 -7
- package/dist/session.d.ts +12 -0
- package/dist/session.js +66 -11
- package/dist/stores/file-lock-identity.d.ts +20 -0
- package/dist/stores/file-lock-identity.js +74 -0
- package/dist/stores/file-lock-windows-identity.d.ts +2 -0
- package/dist/stores/file-lock-windows-identity.js +48 -0
- package/dist/stores/file-owner-lock.d.ts +28 -0
- package/dist/stores/file-owner-lock.js +280 -0
- package/dist/stores/file-session-store.d.ts +13 -6
- package/dist/stores/file-session-store.js +16 -183
- package/dist/stores/memory-session-store.d.ts +2 -0
- package/dist/stores/memory-session-store.js +2 -0
- package/dist/stores/session-store.d.ts +4 -0
- package/dist/subagent-runner.js +10 -3
- package/dist/subagents.d.ts +2 -1
- package/dist/subagents.js +10 -3
- package/dist/task-tools.js +6 -1
- package/dist/thread.d.ts +2 -0
- package/dist/thread.js +6 -0
- package/dist/tools.d.ts +28 -5
- package/dist/tools.js +159 -21
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -1,33 +1,107 @@
|
|
|
1
1
|
import { HarnessError } from './errors.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* Three ways in, because the three answer different questions: a tool that is
|
|
6
|
-
* always the same (`register`), one that needs the session to build
|
|
7
|
-
* (`registerFactory`), and a set discovered at resolve time — an MCP server's
|
|
8
|
-
* catalogue, say (`registerProvider`).
|
|
9
|
-
*/
|
|
2
|
+
import { validateAgent } from './address.js';
|
|
3
|
+
import { RegistrationCollision, registrationOwner } from './registration.js';
|
|
10
4
|
export class ToolRegistry {
|
|
11
5
|
#tools = new Map();
|
|
12
6
|
#factories = new Map();
|
|
13
|
-
#
|
|
14
|
-
|
|
7
|
+
#metadata = new Map();
|
|
8
|
+
#providers = new Map();
|
|
9
|
+
#agents = new Map();
|
|
10
|
+
#staticOwners = new Map();
|
|
11
|
+
#factoryOwners = new Map();
|
|
12
|
+
#refuseDuplicates;
|
|
13
|
+
namespace;
|
|
14
|
+
root;
|
|
15
|
+
constructor(options = null, root) {
|
|
16
|
+
if (options !== null && typeof options !== 'string' && (typeof options !== 'object' || Array.isArray(options)))
|
|
17
|
+
throw new TypeError('Registry options must be an options object, agent string or null');
|
|
18
|
+
this.namespace = typeof options === 'string' ? options : null;
|
|
19
|
+
this.root = root;
|
|
20
|
+
const setting = options !== null && typeof options === 'object' ? options.refuseDuplicates : (root === undefined ? undefined : root.#refuseDuplicates);
|
|
21
|
+
if (setting !== undefined && typeof setting !== 'boolean')
|
|
22
|
+
throw new TypeError('refuseDuplicates must be a boolean');
|
|
23
|
+
this.#refuseDuplicates = setting ?? false;
|
|
24
|
+
validateAgent(this.namespace);
|
|
25
|
+
for (const key of ['namespace', 'root'])
|
|
26
|
+
Object.defineProperty(this, key, { writable: false, configurable: false });
|
|
27
|
+
}
|
|
28
|
+
/** Immutable namespace views share registrations, never a current-agent flag. */
|
|
29
|
+
forAgent(agent) {
|
|
30
|
+
validateAgent(agent);
|
|
31
|
+
const root = this.root ?? this;
|
|
32
|
+
if (agent === null)
|
|
33
|
+
return root;
|
|
34
|
+
let view = root.#agents.get(agent);
|
|
35
|
+
if (!view) {
|
|
36
|
+
view = new ToolRegistry(agent, root);
|
|
37
|
+
root.#agents.set(agent, view);
|
|
38
|
+
}
|
|
39
|
+
return view;
|
|
40
|
+
}
|
|
41
|
+
register(tool, owner) {
|
|
42
|
+
this.#claim(tool.name, owner, 'static');
|
|
15
43
|
this.#tools.set(tool.name, tool);
|
|
44
|
+
this.#staticOwners.set(tool.name, registrationOwner(owner, this.namespace));
|
|
16
45
|
return this;
|
|
17
46
|
}
|
|
18
|
-
registerMany(tools) {
|
|
47
|
+
registerMany(tools, owner) {
|
|
19
48
|
for (const tool of tools)
|
|
20
|
-
this.register(tool);
|
|
49
|
+
this.register(tool, owner);
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
registerFactory(name, factory, owner) {
|
|
53
|
+
this.#claim(name, owner, 'factory');
|
|
54
|
+
this.#factories.set(name, factory);
|
|
55
|
+
this.#factoryOwners.set(name, registrationOwner(owner, this.namespace));
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
registerProvider(provider, owner, name) {
|
|
59
|
+
const label = registrationOwner(owner, this.namespace);
|
|
60
|
+
const id = name ?? (this.namespace === null ? `provider:${this.#providers.size}` : undefined);
|
|
61
|
+
if (!id)
|
|
62
|
+
throw new TypeError('A named provider requires a registration name.');
|
|
63
|
+
const previous = this.#providers.get(id);
|
|
64
|
+
if (this.#strict() && previous)
|
|
65
|
+
throw new RegistrationCollision(this.namespace, id, previous.owner, label);
|
|
66
|
+
this.#providers.set(id, { provider, owner: label });
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
replace(tool, predecessorOwner, owner) {
|
|
70
|
+
this.#claim(tool.name, owner, 'static', predecessorOwner);
|
|
71
|
+
this.#tools.set(tool.name, tool);
|
|
72
|
+
this.#staticOwners.set(tool.name, registrationOwner(owner, this.namespace));
|
|
21
73
|
return this;
|
|
22
74
|
}
|
|
23
|
-
|
|
75
|
+
replaceFactory(name, factory, predecessorOwner, owner) {
|
|
76
|
+
this.#claim(name, owner, 'factory', predecessorOwner);
|
|
24
77
|
this.#factories.set(name, factory);
|
|
78
|
+
this.#factoryOwners.set(name, registrationOwner(owner, this.namespace));
|
|
25
79
|
return this;
|
|
26
80
|
}
|
|
27
|
-
|
|
28
|
-
this.#providers.
|
|
81
|
+
replaceProvider(name, provider, predecessorOwner, owner) {
|
|
82
|
+
const label = registrationOwner(owner, this.namespace), previous = this.#providers.get(name);
|
|
83
|
+
if (!previous || previous.owner !== predecessorOwner)
|
|
84
|
+
throw new Error('Replacement must name the registered predecessor owner.');
|
|
85
|
+
this.#providers.set(name, { provider, owner: label });
|
|
29
86
|
return this;
|
|
30
87
|
}
|
|
88
|
+
#claim(name, owner, source, predecessor) {
|
|
89
|
+
const label = registrationOwner(owner, this.namespace), previous = this.#metadata.get(name);
|
|
90
|
+
if (predecessor !== undefined) {
|
|
91
|
+
if (!previous || previous.owner !== predecessor)
|
|
92
|
+
throw new Error('Replacement must name the registered predecessor owner.');
|
|
93
|
+
}
|
|
94
|
+
else if (this.#strict() && previous)
|
|
95
|
+
throw new RegistrationCollision(this.namespace, name, previous.owner, label);
|
|
96
|
+
if (this.#strict() || predecessor !== undefined) {
|
|
97
|
+
this.#tools.delete(name);
|
|
98
|
+
this.#factories.delete(name);
|
|
99
|
+
this.#staticOwners.delete(name);
|
|
100
|
+
this.#factoryOwners.delete(name);
|
|
101
|
+
}
|
|
102
|
+
this.#metadata.set(name, { owner: label, source });
|
|
103
|
+
}
|
|
104
|
+
#strict() { return this.namespace !== null || this.#refuseDuplicates; }
|
|
31
105
|
/** Every name this registry can produce without a session. */
|
|
32
106
|
names() {
|
|
33
107
|
return [...new Set([...this.#tools.keys(), ...this.#factories.keys()])].sort();
|
|
@@ -41,11 +115,26 @@ export class ToolRegistry {
|
|
|
41
115
|
* run wondering why the model never called it.
|
|
42
116
|
*/
|
|
43
117
|
async resolve(names, session) {
|
|
44
|
-
const
|
|
118
|
+
const detailed = await this.resolveDetailed(names, session);
|
|
119
|
+
return new Map([...detailed].map(([name, entry]) => [name, entry.tool]));
|
|
120
|
+
}
|
|
121
|
+
/** The offer path and inspection path consume the same ownership result. */
|
|
122
|
+
async resolveDetailed(names, session) {
|
|
123
|
+
if (session && session.agent !== this.namespace) {
|
|
124
|
+
if (this.namespace !== null)
|
|
125
|
+
throw new Error('Harness registry/session agent mismatch.');
|
|
126
|
+
return this.forAgent(session.agent).resolveDetailed(names, session);
|
|
127
|
+
}
|
|
128
|
+
const provided = new Map(), metadata = new Map(this.#metadata);
|
|
45
129
|
if (session !== undefined) {
|
|
46
|
-
for (const provider of this.#providers) {
|
|
47
|
-
for (const tool of await provider(session))
|
|
130
|
+
for (const { provider, owner } of this.#providers.values()) {
|
|
131
|
+
for (const tool of await provider(session)) {
|
|
132
|
+
const previous = metadata.get(tool.name);
|
|
133
|
+
if (this.#strict() && previous)
|
|
134
|
+
throw new RegistrationCollision(this.namespace, tool.name, previous.owner, owner);
|
|
48
135
|
provided.set(tool.name, tool);
|
|
136
|
+
metadata.set(tool.name, { owner, source: 'provider' });
|
|
137
|
+
}
|
|
49
138
|
}
|
|
50
139
|
}
|
|
51
140
|
const available = [
|
|
@@ -56,7 +145,7 @@ export class ToolRegistry {
|
|
|
56
145
|
for (const name of selected) {
|
|
57
146
|
const tool = this.#tools.get(name) ?? provided.get(name);
|
|
58
147
|
if (tool !== undefined) {
|
|
59
|
-
resolved.set(name, tool);
|
|
148
|
+
resolved.set(name, { tool, ...(this.#tools.has(name) ? { owner: this.#staticOwners.get(name), source: 'static' } : metadata.get(name)) });
|
|
60
149
|
continue;
|
|
61
150
|
}
|
|
62
151
|
const factory = this.#factories.get(name);
|
|
@@ -66,7 +155,16 @@ export class ToolRegistry {
|
|
|
66
155
|
if (session === undefined) {
|
|
67
156
|
throw HarnessError.toolNotAvailable(`${name} (needs a session to build)`, available);
|
|
68
157
|
}
|
|
69
|
-
|
|
158
|
+
const built = factory(session);
|
|
159
|
+
if (this.#strict() && built.name !== name)
|
|
160
|
+
throw new Error(`Session-bound tool factory [${name}] returned [${built.name}].`);
|
|
161
|
+
if (built.name !== name) {
|
|
162
|
+
const previous = metadata.get(built.name);
|
|
163
|
+
if (this.#strict() && previous)
|
|
164
|
+
throw new RegistrationCollision(this.namespace, built.name, previous.owner, metadata.get(name).owner);
|
|
165
|
+
metadata.set(built.name, metadata.get(name));
|
|
166
|
+
}
|
|
167
|
+
resolved.set(name, { tool: built, owner: this.#factoryOwners.get(name), source: 'factory' });
|
|
70
168
|
}
|
|
71
169
|
return resolved;
|
|
72
170
|
}
|
|
@@ -85,19 +183,54 @@ export class ToolRegistry {
|
|
|
85
183
|
* not to leave in place — both at once.
|
|
86
184
|
*/
|
|
87
185
|
export class ToolAuthorizer {
|
|
186
|
+
namespace;
|
|
187
|
+
root;
|
|
88
188
|
enabled;
|
|
89
189
|
#offer;
|
|
90
190
|
#call;
|
|
91
|
-
|
|
191
|
+
#agents;
|
|
192
|
+
#scoped = new Map();
|
|
193
|
+
constructor(options = {}, namespace = null, root) {
|
|
194
|
+
this.namespace = namespace;
|
|
195
|
+
this.root = root;
|
|
196
|
+
validateAgent(namespace);
|
|
92
197
|
this.enabled = options.enabled ?? false;
|
|
93
198
|
this.#offer = options.offer;
|
|
94
199
|
this.#call = options.call;
|
|
200
|
+
this.#agents = Object.fromEntries(Object.entries(options.agents ?? {}).map(([agent, config]) => {
|
|
201
|
+
validateAgent(agent);
|
|
202
|
+
return [agent, { ...config }];
|
|
203
|
+
}));
|
|
95
204
|
if (!this.enabled && (options.offer !== undefined || options.call !== undefined)) {
|
|
96
205
|
throw HarnessError.policyDefinedButDisabled();
|
|
97
206
|
}
|
|
207
|
+
for (const key of ['enabled', 'namespace', 'root'])
|
|
208
|
+
Object.defineProperty(this, key, { writable: false, configurable: false });
|
|
209
|
+
}
|
|
210
|
+
forAgent(agent) {
|
|
211
|
+
validateAgent(agent);
|
|
212
|
+
const root = this.root ?? this;
|
|
213
|
+
if (agent === null)
|
|
214
|
+
return root;
|
|
215
|
+
let scoped = root.#scoped.get(agent);
|
|
216
|
+
if (!scoped) {
|
|
217
|
+
const overrides = root.#agents[agent] ?? {};
|
|
218
|
+
const options = { enabled: root.enabled, offer: root.#offer, call: root.#call, ...overrides, agents: {} };
|
|
219
|
+
if (typeof options.enabled !== 'boolean' || (options.offer !== undefined && typeof options.offer !== 'function') || (options.call !== undefined && typeof options.call !== 'function'))
|
|
220
|
+
throw new TypeError('Malformed agent tool authorization options.');
|
|
221
|
+
scoped = new ToolAuthorizer(options, agent, root);
|
|
222
|
+
root.#scoped.set(agent, scoped);
|
|
223
|
+
}
|
|
224
|
+
return scoped;
|
|
98
225
|
}
|
|
226
|
+
hasCallPolicy() { return this.#call !== undefined; }
|
|
99
227
|
/** The tools a run may be offered, each wrapped so the call policy is asked again. */
|
|
100
228
|
async allowed(session, tools) {
|
|
229
|
+
if (session.agent !== this.namespace) {
|
|
230
|
+
if (this.namespace !== null)
|
|
231
|
+
throw new Error('Harness authorizer/session agent mismatch.');
|
|
232
|
+
return this.forAgent(session.agent).allowed(session, tools);
|
|
233
|
+
}
|
|
101
234
|
if (!this.enabled) {
|
|
102
235
|
return [...tools.values()];
|
|
103
236
|
}
|
|
@@ -120,6 +253,11 @@ export class ToolAuthorizer {
|
|
|
120
253
|
* be mistaken for enforcement.
|
|
121
254
|
*/
|
|
122
255
|
async allowsCall(session, tool, args) {
|
|
256
|
+
if (session.agent !== this.namespace) {
|
|
257
|
+
if (this.namespace !== null)
|
|
258
|
+
throw new Error('Harness authorizer/session agent mismatch.');
|
|
259
|
+
return this.forAgent(session.agent).allowsCall(session, tool, args);
|
|
260
|
+
}
|
|
123
261
|
if (!this.enabled || this.#call === undefined) {
|
|
124
262
|
return true;
|
|
125
263
|
}
|