@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.
Files changed (55) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +86 -1
  3. package/dist/define.d.ts +138 -0
  4. package/dist/define.d.ts.map +1 -0
  5. package/dist/define.js +318 -0
  6. package/dist/define.js.map +1 -0
  7. package/dist/errors.d.ts +113 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +4 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/http.d.ts +2 -0
  12. package/dist/http.d.ts.map +1 -0
  13. package/dist/http.js +213 -0
  14. package/dist/http.js.map +1 -0
  15. package/dist/index.d.ts +15 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +14 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/invoke.d.ts +27 -0
  20. package/dist/invoke.d.ts.map +1 -0
  21. package/dist/invoke.js +127 -0
  22. package/dist/invoke.js.map +1 -0
  23. package/dist/manifest.d.ts +23 -0
  24. package/dist/manifest.d.ts.map +1 -0
  25. package/dist/manifest.js +37 -0
  26. package/dist/manifest.js.map +1 -0
  27. package/dist/precondition.d.ts +25 -0
  28. package/dist/precondition.d.ts.map +1 -0
  29. package/dist/precondition.js +34 -0
  30. package/dist/precondition.js.map +1 -0
  31. package/dist/registry.d.ts +87 -0
  32. package/dist/registry.d.ts.map +1 -0
  33. package/dist/registry.js +171 -0
  34. package/dist/registry.js.map +1 -0
  35. package/dist/spec-registry.d.ts +48 -0
  36. package/dist/spec-registry.d.ts.map +1 -0
  37. package/dist/spec-registry.js +37 -0
  38. package/dist/spec-registry.js.map +1 -0
  39. package/dist/tool.schema.json +297 -0
  40. package/dist/types.d.ts +508 -0
  41. package/dist/types.d.ts.map +1 -0
  42. package/dist/types.js +14 -0
  43. package/dist/types.js.map +1 -0
  44. package/package.json +64 -4
  45. package/src/define.ts +537 -0
  46. package/src/errors.ts +139 -0
  47. package/src/http.ts +268 -0
  48. package/src/index.ts +68 -0
  49. package/src/invoke.ts +165 -0
  50. package/src/manifest.ts +40 -0
  51. package/src/precondition.ts +40 -0
  52. package/src/registry.ts +262 -0
  53. package/src/spec-registry.ts +78 -0
  54. package/src/tool.schema.json +297 -0
  55. package/src/types.ts +538 -0
@@ -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
+ }