@kindgi/tools 0.0.0-bootstrap.0 → 0.1.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/LICENSE +201 -0
- package/README.md +86 -1
- package/dist/define.d.ts +138 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +318 -0
- package/dist/define.js.map +1 -0
- package/dist/errors.d.ts +113 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +213 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/invoke.d.ts +27 -0
- package/dist/invoke.d.ts.map +1 -0
- package/dist/invoke.js +127 -0
- package/dist/invoke.js.map +1 -0
- package/dist/manifest.d.ts +23 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +37 -0
- package/dist/manifest.js.map +1 -0
- package/dist/precondition.d.ts +25 -0
- package/dist/precondition.d.ts.map +1 -0
- package/dist/precondition.js +34 -0
- package/dist/precondition.js.map +1 -0
- package/dist/registry.d.ts +87 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +171 -0
- package/dist/registry.js.map +1 -0
- package/dist/spec-registry.d.ts +48 -0
- package/dist/spec-registry.d.ts.map +1 -0
- package/dist/spec-registry.js +37 -0
- package/dist/spec-registry.js.map +1 -0
- package/dist/tool.schema.json +297 -0
- package/dist/types.d.ts +508 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -4
- package/src/define.ts +537 -0
- package/src/errors.ts +139 -0
- package/src/http.ts +268 -0
- package/src/index.ts +68 -0
- package/src/invoke.ts +165 -0
- package/src/manifest.ts +40 -0
- package/src/precondition.ts +40 -0
- package/src/registry.ts +262 -0
- package/src/spec-registry.ts +78 -0
- package/src/tool.schema.json +297 -0
- package/src/types.ts +538 -0
package/src/registry.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import semver from 'semver';
|
|
5
|
+
|
|
6
|
+
import type { Result, TenantId, ToolId } from '@kindgi/types';
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
DuplicateToolVersionError,
|
|
10
|
+
InvalidVersionRangeError,
|
|
11
|
+
ToolError,
|
|
12
|
+
ToolNotFoundError,
|
|
13
|
+
ToolVersionNotFoundError,
|
|
14
|
+
ToolVersionUnresolvableError,
|
|
15
|
+
} from './errors.js';
|
|
16
|
+
import type { AnyTool } from './types.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* In-process registry mapping (tool id, version) → tool. Multi-version:
|
|
20
|
+
* a single id may carry multiple compiled implementations at different
|
|
21
|
+
* exact semver strings; agent bindings reference tools by (id, range)
|
|
22
|
+
* and dispatch resolves at run start via `resolve(id, range)` (npm-
|
|
23
|
+
* compatible semver grammar, backed by `semver.maxSatisfying`).
|
|
24
|
+
*
|
|
25
|
+
* Not tenant-scoped. Multi-tenant deployments hold one registry per
|
|
26
|
+
* tenant or encode tenancy into tool ids. Cross-tenant tool sharing goes
|
|
27
|
+
* through the policy engine, not through registry design.
|
|
28
|
+
*/
|
|
29
|
+
export interface ToolRegistry {
|
|
30
|
+
/**
|
|
31
|
+
* Register a tool. Fails with `duplicate-tool-version` if the same
|
|
32
|
+
* `(id, version)` pair is already registered. Distinct versions of the
|
|
33
|
+
* same id may coexist — that's the whole point of the versioned model.
|
|
34
|
+
*/
|
|
35
|
+
register(tool: AnyTool): Result<void, ToolError>;
|
|
36
|
+
/**
|
|
37
|
+
* Return the latest active version for an id. "Latest" is defined by
|
|
38
|
+
* `semver.compare` — the highest exact version present. For callers
|
|
39
|
+
* that have no version range to resolve. Prefer `resolve(id, range)`
|
|
40
|
+
* when a range is available.
|
|
41
|
+
*/
|
|
42
|
+
get(id: ToolId): Result<AnyTool, ToolError>;
|
|
43
|
+
/** Return a specific `(id, version)` pair. */
|
|
44
|
+
getVersion(id: ToolId, version: string): Result<AnyTool, ToolError>;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a semver range against the versions registered under `id`.
|
|
47
|
+
* Uses `semver.maxSatisfying` (npm grammar). Distinct failure modes:
|
|
48
|
+
* - `tool-not-found` → id has no versions at all
|
|
49
|
+
* - `invalid-version-range` → range string is grammatically broken
|
|
50
|
+
* - `tool-version-unresolvable` → id is present but no version satisfies
|
|
51
|
+
*/
|
|
52
|
+
resolve(id: ToolId, range: string): Result<ToolResolution, ToolError>;
|
|
53
|
+
has(id: ToolId): boolean;
|
|
54
|
+
hasVersion(id: ToolId, version: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Return every registered tool version, sorted by id then by version
|
|
57
|
+
* descending. Callers that want a per-id latest-only view use
|
|
58
|
+
* `.get(id)` per id.
|
|
59
|
+
*/
|
|
60
|
+
list(): readonly AnyTool[];
|
|
61
|
+
/** Return every id with at least one registered version. */
|
|
62
|
+
ids(): readonly ToolId[];
|
|
63
|
+
/** Return every version registered under `id`, sorted descending. */
|
|
64
|
+
versions(id: ToolId): readonly string[];
|
|
65
|
+
/**
|
|
66
|
+
* The registry for one tenant: lookups on the returned registry
|
|
67
|
+
* (`get`, `getVersion`, `resolve`, `list`, …) answer for `tenantId`
|
|
68
|
+
* only. Multi-tenant implementations load that tenant's tools (e.g.
|
|
69
|
+
* from a persistent store, cached) and return a view bound to it —
|
|
70
|
+
* never shared per-call state, so concurrent turns of different
|
|
71
|
+
* tenants cannot see each other's tools. A single-tenant registry
|
|
72
|
+
* (like `createToolRegistry`) returns itself. Callers resolve a
|
|
73
|
+
* turn's tools through the returned registry.
|
|
74
|
+
*/
|
|
75
|
+
forTenant(tenantId: TenantId): Promise<ToolRegistry>;
|
|
76
|
+
/**
|
|
77
|
+
* Optional cache invalidation. Storage-backed implementations call
|
|
78
|
+
* this from the write path (`POST /v1/tools`, unregister, reinstate)
|
|
79
|
+
* so the next `forTenant(tenantId)` re-reads from the persistent
|
|
80
|
+
* store. Without it, tools published after a tenant was loaded stay
|
|
81
|
+
* invisible to that tenant's cached view. In-memory implementations
|
|
82
|
+
* leave it undefined — there is no cache to invalidate.
|
|
83
|
+
*/
|
|
84
|
+
readonly invalidate?: (tenantId: TenantId) => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The outcome of a successful `resolve(id, range)` lookup. */
|
|
88
|
+
export interface ToolResolution {
|
|
89
|
+
readonly tool: AnyTool;
|
|
90
|
+
/**
|
|
91
|
+
* The exact version picked by `maxSatisfying`. Callers capture this in
|
|
92
|
+
* provenance + telemetry so a replay can pin against the same version.
|
|
93
|
+
*/
|
|
94
|
+
readonly resolvedVersion: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create an empty tool registry. Seeds are registered in order; the seed
|
|
99
|
+
* throws on duplicate `(id, version)` so accidental clobbers of a
|
|
100
|
+
* running tool fail loud at boot.
|
|
101
|
+
*/
|
|
102
|
+
export function createToolRegistry(seed: readonly AnyTool[] = []): ToolRegistry {
|
|
103
|
+
// id → version → tool. Nested map so we can iterate versions per id
|
|
104
|
+
// cheaply and cache a sorted-version list.
|
|
105
|
+
const tools = new Map<ToolId, Map<string, AnyTool>>();
|
|
106
|
+
|
|
107
|
+
function versionsDesc(id: ToolId): string[] {
|
|
108
|
+
const versions = tools.get(id);
|
|
109
|
+
if (versions === undefined) return [];
|
|
110
|
+
return [...versions.keys()].sort((a, b) => semver.rcompare(a, b));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const registry: ToolRegistry = {
|
|
114
|
+
register(tool: AnyTool): Result<void, ToolError> {
|
|
115
|
+
let versions = tools.get(tool.id);
|
|
116
|
+
if (versions === undefined) {
|
|
117
|
+
versions = new Map();
|
|
118
|
+
tools.set(tool.id, versions);
|
|
119
|
+
}
|
|
120
|
+
if (versions.has(tool.version)) {
|
|
121
|
+
const dup: DuplicateToolVersionError = {
|
|
122
|
+
code: 'duplicate-tool-version',
|
|
123
|
+
message: `Tool "${tool.id}@${tool.version}" is already registered`,
|
|
124
|
+
id: tool.id,
|
|
125
|
+
version: tool.version,
|
|
126
|
+
};
|
|
127
|
+
return { kind: 'err', error: dup };
|
|
128
|
+
}
|
|
129
|
+
versions.set(tool.version, tool);
|
|
130
|
+
return { kind: 'ok', value: undefined };
|
|
131
|
+
},
|
|
132
|
+
get(id: ToolId): Result<AnyTool, ToolError> {
|
|
133
|
+
const desc = versionsDesc(id);
|
|
134
|
+
if (desc.length === 0) {
|
|
135
|
+
const err: ToolNotFoundError = {
|
|
136
|
+
code: 'tool-not-found',
|
|
137
|
+
message: `No tool registered with id "${id}"`,
|
|
138
|
+
id,
|
|
139
|
+
};
|
|
140
|
+
return { kind: 'err', error: err };
|
|
141
|
+
}
|
|
142
|
+
// desc is sorted desc — desc[0] is highest.
|
|
143
|
+
const latest = tools.get(id)?.get(desc[0]!);
|
|
144
|
+
if (latest === undefined) {
|
|
145
|
+
const err: ToolNotFoundError = {
|
|
146
|
+
code: 'tool-not-found',
|
|
147
|
+
message: `No tool registered with id "${id}"`,
|
|
148
|
+
id,
|
|
149
|
+
};
|
|
150
|
+
return { kind: 'err', error: err };
|
|
151
|
+
}
|
|
152
|
+
return { kind: 'ok', value: latest };
|
|
153
|
+
},
|
|
154
|
+
getVersion(id: ToolId, version: string): Result<AnyTool, ToolError> {
|
|
155
|
+
const found = tools.get(id)?.get(version);
|
|
156
|
+
if (found === undefined) {
|
|
157
|
+
if (tools.get(id) === undefined) {
|
|
158
|
+
const err: ToolNotFoundError = {
|
|
159
|
+
code: 'tool-not-found',
|
|
160
|
+
message: `No tool registered with id "${id}"`,
|
|
161
|
+
id,
|
|
162
|
+
};
|
|
163
|
+
return { kind: 'err', error: err };
|
|
164
|
+
}
|
|
165
|
+
const err: ToolVersionNotFoundError = {
|
|
166
|
+
code: 'tool-version-not-found',
|
|
167
|
+
message: `Tool "${id}" has no version "${version}" registered`,
|
|
168
|
+
id,
|
|
169
|
+
version,
|
|
170
|
+
};
|
|
171
|
+
return { kind: 'err', error: err };
|
|
172
|
+
}
|
|
173
|
+
return { kind: 'ok', value: found };
|
|
174
|
+
},
|
|
175
|
+
resolve(id: ToolId, range: string): Result<ToolResolution, ToolError> {
|
|
176
|
+
const versions = tools.get(id);
|
|
177
|
+
if (versions === undefined || versions.size === 0) {
|
|
178
|
+
const err: ToolNotFoundError = {
|
|
179
|
+
code: 'tool-not-found',
|
|
180
|
+
message: `No tool registered with id "${id}"`,
|
|
181
|
+
id,
|
|
182
|
+
};
|
|
183
|
+
return { kind: 'err', error: err };
|
|
184
|
+
}
|
|
185
|
+
if (semver.validRange(range) === null) {
|
|
186
|
+
const err: InvalidVersionRangeError = {
|
|
187
|
+
code: 'invalid-version-range',
|
|
188
|
+
message: `Version range "${range}" for tool "${id}" is not a valid semver range`,
|
|
189
|
+
id,
|
|
190
|
+
range,
|
|
191
|
+
};
|
|
192
|
+
return { kind: 'err', error: err };
|
|
193
|
+
}
|
|
194
|
+
const available = [...versions.keys()];
|
|
195
|
+
const picked = semver.maxSatisfying(available, range);
|
|
196
|
+
if (picked === null) {
|
|
197
|
+
const err: ToolVersionUnresolvableError = {
|
|
198
|
+
code: 'tool-version-unresolvable',
|
|
199
|
+
message: `Tool "${id}" has no version satisfying "${range}" (available: ${available
|
|
200
|
+
.sort((a, b) => semver.rcompare(a, b))
|
|
201
|
+
.join(', ')})`,
|
|
202
|
+
id,
|
|
203
|
+
range,
|
|
204
|
+
availableVersions: available.sort((a, b) => semver.rcompare(a, b)),
|
|
205
|
+
};
|
|
206
|
+
return { kind: 'err', error: err };
|
|
207
|
+
}
|
|
208
|
+
const tool = versions.get(picked);
|
|
209
|
+
// maxSatisfying returned a version we just enumerated — guaranteed
|
|
210
|
+
// to be in the map. The `if (tool === undefined)` branch is a
|
|
211
|
+
// defensive no-op for the type system.
|
|
212
|
+
if (tool === undefined) {
|
|
213
|
+
const err: ToolVersionNotFoundError = {
|
|
214
|
+
code: 'tool-version-not-found',
|
|
215
|
+
message: `Tool "${id}" resolved to "${picked}" but the version is missing (internal)`,
|
|
216
|
+
id,
|
|
217
|
+
version: picked,
|
|
218
|
+
};
|
|
219
|
+
return { kind: 'err', error: err };
|
|
220
|
+
}
|
|
221
|
+
return { kind: 'ok', value: { tool, resolvedVersion: picked } };
|
|
222
|
+
},
|
|
223
|
+
has(id: ToolId): boolean {
|
|
224
|
+
const versions = tools.get(id);
|
|
225
|
+
return versions !== undefined && versions.size > 0;
|
|
226
|
+
},
|
|
227
|
+
hasVersion(id: ToolId, version: string): boolean {
|
|
228
|
+
return tools.get(id)?.has(version) ?? false;
|
|
229
|
+
},
|
|
230
|
+
list(): readonly AnyTool[] {
|
|
231
|
+
const out: AnyTool[] = [];
|
|
232
|
+
const ids = [...tools.keys()].sort();
|
|
233
|
+
for (const id of ids) {
|
|
234
|
+
for (const v of versionsDesc(id)) {
|
|
235
|
+
const tool = tools.get(id)?.get(v);
|
|
236
|
+
if (tool !== undefined) out.push(tool);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
},
|
|
241
|
+
ids(): readonly ToolId[] {
|
|
242
|
+
const out: ToolId[] = [];
|
|
243
|
+
for (const [id, versions] of tools) {
|
|
244
|
+
if (versions.size > 0) out.push(id);
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
},
|
|
248
|
+
versions(id: ToolId): readonly string[] {
|
|
249
|
+
return versionsDesc(id);
|
|
250
|
+
},
|
|
251
|
+
async forTenant(): Promise<ToolRegistry> {
|
|
252
|
+
// Single-tenant: the same registry serves every tenant.
|
|
253
|
+
return registry;
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
for (const tool of seed) {
|
|
258
|
+
const r = registry.register(tool);
|
|
259
|
+
if (r.kind === 'err') throw new Error(`seed failed: ${r.error.message}`);
|
|
260
|
+
}
|
|
261
|
+
return registry;
|
|
262
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { ToolContext, ToolSpec } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A synthesizer takes a declarative `ToolSpec` variant and returns
|
|
8
|
+
* the runtime handler that satisfies its semantics. Framework core
|
|
9
|
+
* ships the `'http'` synthesizer (see `./http.ts` for the impl).
|
|
10
|
+
* Third-party kinds (SQL, MCP, webhook, ...) register their own via
|
|
11
|
+
* `registerToolSpecSynthesizer(kind, fn)` at module load.
|
|
12
|
+
*
|
|
13
|
+
* The synthesizer receives the fully-validated spec (author-time
|
|
14
|
+
* validation has already run) and returns a handler with the
|
|
15
|
+
* standard `(input, ctx) => Promise<output>` signature. Any
|
|
16
|
+
* per-invoke correctness bits (URL substitution, abort chaining,
|
|
17
|
+
* secret resolution) live inside the handler the synthesizer
|
|
18
|
+
* returns.
|
|
19
|
+
*/
|
|
20
|
+
/** What the runtime gives a synthesized handler. */
|
|
21
|
+
export interface ToolSpecSynthesizerOptions {
|
|
22
|
+
/**
|
|
23
|
+
* The fetch for requests to the hosts a spec names. The runtime passes
|
|
24
|
+
* one that refuses the hosts its deployment forbids
|
|
25
|
+
* (`KINDGI_TENANT_HOST_ACCESS`: the cloud metadata endpoints and the
|
|
26
|
+
* server's own host). Absent: the global `fetch`.
|
|
27
|
+
*/
|
|
28
|
+
readonly fetch?: typeof fetch;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ToolSpecSynthesizer<TSpec extends ToolSpec = ToolSpec> = (
|
|
32
|
+
spec: TSpec,
|
|
33
|
+
toolId: string,
|
|
34
|
+
options?: ToolSpecSynthesizerOptions,
|
|
35
|
+
) => (input: unknown, ctx: ToolContext) => Promise<unknown>;
|
|
36
|
+
|
|
37
|
+
// Registry keyed by the spec's `kind` discriminant. Populated by
|
|
38
|
+
// side-effect imports of first-party synthesizer packages (`./http.js`
|
|
39
|
+
// registers `'http'`) or third-party equivalents.
|
|
40
|
+
const registry = new Map<string, ToolSpecSynthesizer>();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register a synthesizer for a given `spec.kind`. First-party kinds
|
|
44
|
+
* register at framework load; third-party kinds register when their
|
|
45
|
+
* package's module runs. Overwriting an existing kind throws — kind
|
|
46
|
+
* ownership must be explicit to avoid silent shadowing.
|
|
47
|
+
*/
|
|
48
|
+
export function registerToolSpecSynthesizer<TSpec extends ToolSpec>(
|
|
49
|
+
kind: TSpec['kind'],
|
|
50
|
+
synthesizer: ToolSpecSynthesizer<TSpec>,
|
|
51
|
+
): void {
|
|
52
|
+
if (registry.has(kind)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`ToolSpec synthesizer for kind "${kind}" is already registered. Multiple registrations for the same kind indicate shadowing — unregister first if replacement is intended.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
registry.set(kind, synthesizer as ToolSpecSynthesizer);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Look up the synthesizer for a `ToolSpec`. Returns `undefined` when
|
|
62
|
+
* no synthesizer is registered for the spec's `kind` — callers
|
|
63
|
+
* (typically `defineTool`) surface this as a clear "no synthesizer
|
|
64
|
+
* for kind X" error at define time.
|
|
65
|
+
*/
|
|
66
|
+
export function getToolSpecSynthesizer(kind: string): ToolSpecSynthesizer | undefined {
|
|
67
|
+
return registry.get(kind);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Test-only helper. Removes a registered synthesizer so tests can
|
|
72
|
+
* verify the "no synthesizer registered" error path without process
|
|
73
|
+
* teardown. Not exported from the package barrel — call it via
|
|
74
|
+
* direct import in tests only.
|
|
75
|
+
*/
|
|
76
|
+
export function _unregisterToolSpecSynthesizer(kind: string): void {
|
|
77
|
+
registry.delete(kind);
|
|
78
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://kindgi.com/schemas/v1/tool.schema.json",
|
|
4
|
+
"$comment": "schema-version: 1.1.0",
|
|
5
|
+
"title": "Tool",
|
|
6
|
+
"description": "A tool definition. Authored once; the runtime picks transport (native in-process for tools built into the runtime, MCP over the wire for cross-process / cross-org / third-party tools). Every tool declares its typed I/O, its context needs (typed DI), and its side-effects — enabling policy enforcement and parallel scheduling by the kernel.",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"required": ["id", "description", "version", "input", "output"],
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"properties": {
|
|
11
|
+
"id": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"minLength": 1,
|
|
14
|
+
"description": "Unique tool identifier. Convention: '<pack>.<tool>' (e.g. 'acme.verify-citation') or a bare id for tools built into Kindgi."
|
|
15
|
+
},
|
|
16
|
+
"description": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"minLength": 1,
|
|
19
|
+
"description": "Human- and LLM-readable description of what this tool does. Load-bearing: this is what the LLM sees when deciding to call the tool."
|
|
20
|
+
},
|
|
21
|
+
"version": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+$"
|
|
24
|
+
},
|
|
25
|
+
"input": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"description": "JSON Schema for the tool input. MUST validate under Draft 2020-12."
|
|
28
|
+
},
|
|
29
|
+
"output": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"description": "JSON Schema for the tool output. MUST validate under Draft 2020-12."
|
|
32
|
+
},
|
|
33
|
+
"needs": {
|
|
34
|
+
"type": "array",
|
|
35
|
+
"items": {
|
|
36
|
+
"$ref": "#/$defs/Need"
|
|
37
|
+
},
|
|
38
|
+
"description": "Typed context requirements — named slots (e.g. `tenant`, `run`, `blob`, `memory:facts`) the runtime injects via `ToolContext`. Declarative: not checked when a flow is loaded. `needsSpec` is the typed, discriminated form."
|
|
39
|
+
},
|
|
40
|
+
"effects": {
|
|
41
|
+
"type": "array",
|
|
42
|
+
"items": {
|
|
43
|
+
"$ref": "#/$defs/Effect"
|
|
44
|
+
},
|
|
45
|
+
"description": "Declared side-effects. Enables policy enforcement (can this tool run in this tenant?), safe parallelization (are two calls conflicting?), and causal provenance."
|
|
46
|
+
},
|
|
47
|
+
"transport": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"enum": ["auto", "native", "mcp"],
|
|
50
|
+
"default": "auto",
|
|
51
|
+
"description": "Transport hint. 'auto' (default) lets the runtime pick native when possible, MCP otherwise. 'native' requires in-process execution. 'mcp' requires MCP execution. Deployment-time concern; authoring is unchanged."
|
|
52
|
+
},
|
|
53
|
+
"mcpEndpoint": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "If transport = 'mcp' (or resolved to MCP by 'auto'), the MCP endpoint URI. Format: 'mcp+stdio://<command>', 'mcp+http://<url>', 'mcp+sse://<url>'."
|
|
56
|
+
},
|
|
57
|
+
"metadata": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"additionalProperties": true,
|
|
60
|
+
"description": "Free-form annotations (author, docs URL, tags). Not interpreted by the kernel."
|
|
61
|
+
},
|
|
62
|
+
"sandbox": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"enum": ["none", "context-isolated", "strict"],
|
|
65
|
+
"description": "Isolation posture the runtime enforces around the handler. Optional additive field."
|
|
66
|
+
},
|
|
67
|
+
"mutating": {
|
|
68
|
+
"type": "boolean",
|
|
69
|
+
"description": "Semantic marker: `true` when this tool causes observable side effects (writes state, calls external APIs with mutations, sends messages). Read-only tools set `false`. Absent defaults to `true`. Consumed by the HITL default classifier — a read-only tool passes straight through, a mutating tool asks on first use."
|
|
70
|
+
},
|
|
71
|
+
"limits": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"additionalProperties": false,
|
|
74
|
+
"description": "Sandbox-enforced resource caps at dispatch. Optional additive field.",
|
|
75
|
+
"properties": {
|
|
76
|
+
"memMB": { "type": "integer", "minimum": 1 },
|
|
77
|
+
"cpuMs": { "type": "integer", "minimum": 1 }
|
|
78
|
+
},
|
|
79
|
+
"required": ["memMB", "cpuMs"]
|
|
80
|
+
},
|
|
81
|
+
"network": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"description": "Network egress policy. Optional additive field. Discriminated on `kind`.",
|
|
84
|
+
"oneOf": [
|
|
85
|
+
{
|
|
86
|
+
"type": "object",
|
|
87
|
+
"additionalProperties": false,
|
|
88
|
+
"properties": { "kind": { "type": "string", "enum": ["none", "unrestricted"] } },
|
|
89
|
+
"required": ["kind"]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"type": "object",
|
|
93
|
+
"additionalProperties": false,
|
|
94
|
+
"properties": {
|
|
95
|
+
"kind": { "type": "string", "const": "allowlist" },
|
|
96
|
+
"hosts": { "type": "array", "items": { "type": "string", "minLength": 1 } }
|
|
97
|
+
},
|
|
98
|
+
"required": ["kind", "hosts"]
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
"needsSpec": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"additionalProperties": false,
|
|
105
|
+
"description": "Typed discriminated needs (env / secrets / config / capabilities / bindings). Optional additive field.",
|
|
106
|
+
"properties": {
|
|
107
|
+
"env": { "type": "object", "additionalProperties": { "type": "object" } },
|
|
108
|
+
"secrets": { "type": "object", "additionalProperties": { "type": "object" } },
|
|
109
|
+
"config": { "type": "object", "additionalProperties": { "type": "object" } },
|
|
110
|
+
"capabilities": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
111
|
+
"bindings": { "type": "array", "items": { "type": "string", "minLength": 1 } }
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"codeArtifactRef": {
|
|
115
|
+
"description": "Handler-artifact pointer. Discriminated on `kind`: `oci` is the deploy-pipeline shape (image + module path + artifactVersion); `filesystem` is the local-development shape — absolute host path at the pack's on-disk handler file. Production servers SHOULD reject `filesystem`.",
|
|
116
|
+
"oneOf": [
|
|
117
|
+
{
|
|
118
|
+
"type": "object",
|
|
119
|
+
"additionalProperties": false,
|
|
120
|
+
"properties": {
|
|
121
|
+
"kind": { "const": "oci" },
|
|
122
|
+
"imageRef": { "type": "string", "minLength": 1 },
|
|
123
|
+
"modulePath": { "type": "string", "minLength": 1 },
|
|
124
|
+
"artifactVersion": { "type": "string", "minLength": 1 }
|
|
125
|
+
},
|
|
126
|
+
"required": ["kind", "imageRef", "modulePath", "artifactVersion"]
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"type": "object",
|
|
130
|
+
"additionalProperties": false,
|
|
131
|
+
"properties": {
|
|
132
|
+
"kind": { "const": "filesystem" },
|
|
133
|
+
"modulePath": { "type": "string", "minLength": 1 }
|
|
134
|
+
},
|
|
135
|
+
"required": ["kind", "modulePath"]
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
"spec": {
|
|
140
|
+
"description": "Declarative handler spec. Discriminated on `kind`; the framework's synthesizer registry maps kind → runtime handler. Mutually exclusive with an imperative handler at author time.",
|
|
141
|
+
"oneOf": [{ "$ref": "#/$defs/HttpToolSpec" }]
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"$defs": {
|
|
145
|
+
"Need": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"required": ["name"],
|
|
148
|
+
"additionalProperties": false,
|
|
149
|
+
"properties": {
|
|
150
|
+
"name": {
|
|
151
|
+
"type": "string",
|
|
152
|
+
"minLength": 1,
|
|
153
|
+
"description": "The named context slot (e.g. 'tenant', 'db', 'run', 'provenance', 'memory:facts', 'blob')."
|
|
154
|
+
},
|
|
155
|
+
"optional": {
|
|
156
|
+
"type": "boolean",
|
|
157
|
+
"default": false
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"Effect": {
|
|
162
|
+
"type": "object",
|
|
163
|
+
"required": ["kind"],
|
|
164
|
+
"additionalProperties": false,
|
|
165
|
+
"properties": {
|
|
166
|
+
"kind": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"enum": [
|
|
169
|
+
"reads",
|
|
170
|
+
"writes",
|
|
171
|
+
"deletes",
|
|
172
|
+
"network",
|
|
173
|
+
"spawns-run",
|
|
174
|
+
"emits-event",
|
|
175
|
+
"external-side-effect",
|
|
176
|
+
"sensitive-data-egress"
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
"resource": {
|
|
180
|
+
"type": "string",
|
|
181
|
+
"description": "Resource identifier (e.g. 'memory:facts', 'blob:*', 'external:api.example.com'). Used by the policy engine to authorize the effect."
|
|
182
|
+
},
|
|
183
|
+
"notes": {
|
|
184
|
+
"type": "string"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"ToolSecretRef": {
|
|
189
|
+
"type": "object",
|
|
190
|
+
"additionalProperties": false,
|
|
191
|
+
"required": ["envName", "name"],
|
|
192
|
+
"properties": {
|
|
193
|
+
"envName": { "type": "string", "minLength": 1 },
|
|
194
|
+
"name": { "type": "string", "minLength": 1 }
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"HttpHeaderSpec": {
|
|
198
|
+
"type": "object",
|
|
199
|
+
"additionalProperties": false,
|
|
200
|
+
"required": ["name", "value"],
|
|
201
|
+
"properties": {
|
|
202
|
+
"name": { "type": "string", "minLength": 1 },
|
|
203
|
+
"value": { "type": "string" }
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"HttpAuthSpec": {
|
|
207
|
+
"oneOf": [
|
|
208
|
+
{
|
|
209
|
+
"type": "object",
|
|
210
|
+
"additionalProperties": false,
|
|
211
|
+
"required": ["kind", "secretRef"],
|
|
212
|
+
"properties": {
|
|
213
|
+
"kind": { "const": "bearer" },
|
|
214
|
+
"secretRef": { "$ref": "#/$defs/ToolSecretRef" }
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"type": "object",
|
|
219
|
+
"additionalProperties": false,
|
|
220
|
+
"required": ["kind", "headerName", "secretRef"],
|
|
221
|
+
"properties": {
|
|
222
|
+
"kind": { "const": "header" },
|
|
223
|
+
"headerName": { "type": "string", "minLength": 1 },
|
|
224
|
+
"secretRef": { "$ref": "#/$defs/ToolSecretRef" }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
},
|
|
229
|
+
"HttpRequestBodySpec": {
|
|
230
|
+
"oneOf": [
|
|
231
|
+
{
|
|
232
|
+
"type": "object",
|
|
233
|
+
"additionalProperties": false,
|
|
234
|
+
"required": ["kind"],
|
|
235
|
+
"properties": { "kind": { "const": "json-input" } }
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"type": "object",
|
|
239
|
+
"additionalProperties": false,
|
|
240
|
+
"required": ["kind"],
|
|
241
|
+
"properties": { "kind": { "const": "input-passthrough" } }
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"type": "object",
|
|
245
|
+
"additionalProperties": false,
|
|
246
|
+
"required": ["kind", "template"],
|
|
247
|
+
"properties": {
|
|
248
|
+
"kind": { "const": "text" },
|
|
249
|
+
"template": { "type": "string" }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
},
|
|
254
|
+
"HttpToolSpec": {
|
|
255
|
+
"type": "object",
|
|
256
|
+
"description": "Declarative HTTP-invocation spec. Attached to `ToolManifest.spec` under the discriminant `kind: 'http'`. The runtime `Tool.handler` is synthesized by the 'http' spec synthesizer to perform URL-template substitution, secret-ref resolution via `ToolContext.resolveSecret`, and the outbound fetch. All fields serialize cleanly to JSON.",
|
|
257
|
+
"additionalProperties": false,
|
|
258
|
+
"required": ["kind", "method", "urlTemplate"],
|
|
259
|
+
"properties": {
|
|
260
|
+
"kind": { "const": "http" },
|
|
261
|
+
"method": {
|
|
262
|
+
"type": "string",
|
|
263
|
+
"enum": ["GET", "POST", "PUT", "PATCH", "DELETE"]
|
|
264
|
+
},
|
|
265
|
+
"urlTemplate": {
|
|
266
|
+
"type": "string",
|
|
267
|
+
"minLength": 1,
|
|
268
|
+
"description": "URL template with `{param}` placeholders substituted from the tool's input at invoke time."
|
|
269
|
+
},
|
|
270
|
+
"headers": {
|
|
271
|
+
"type": "array",
|
|
272
|
+
"items": { "$ref": "#/$defs/HttpHeaderSpec" }
|
|
273
|
+
},
|
|
274
|
+
"authorization": { "$ref": "#/$defs/HttpAuthSpec" },
|
|
275
|
+
"requestBody": { "$ref": "#/$defs/HttpRequestBodySpec" },
|
|
276
|
+
"timeoutMs": {
|
|
277
|
+
"type": "integer",
|
|
278
|
+
"minimum": 1,
|
|
279
|
+
"description": "Wall-clock timeout in ms. Default 30000."
|
|
280
|
+
},
|
|
281
|
+
"parseJson": {
|
|
282
|
+
"type": "boolean",
|
|
283
|
+
"description": "When true (default), the response body is parsed as JSON before returning to the invoker."
|
|
284
|
+
},
|
|
285
|
+
"successStatus": {
|
|
286
|
+
"type": "object",
|
|
287
|
+
"additionalProperties": false,
|
|
288
|
+
"required": ["min", "max"],
|
|
289
|
+
"properties": {
|
|
290
|
+
"min": { "type": "integer", "minimum": 100, "maximum": 599 },
|
|
291
|
+
"max": { "type": "integer", "minimum": 100, "maximum": 599 }
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|