@devicerail/tool-adapter 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/dist/errors.js ADDED
@@ -0,0 +1,39 @@
1
+ export class ToolAdapterError extends Error {
2
+ code;
3
+ constructor(code, message, options) {
4
+ super(message, options);
5
+ this.name = new.target.name;
6
+ this.code = code;
7
+ }
8
+ }
9
+ export class InvalidActionSpaceError extends ToolAdapterError {
10
+ constructor(message, options) {
11
+ super("invalid_action_space", message, options);
12
+ }
13
+ }
14
+ export class UnknownToolError extends ToolAdapterError {
15
+ toolName;
16
+ constructor(toolName) {
17
+ super("unknown_tool", `unknown DeviceRail tool: ${toolName}`);
18
+ this.toolName = toolName;
19
+ }
20
+ }
21
+ export class InvalidToolArgumentsError extends ToolAdapterError {
22
+ toolName;
23
+ constructor(toolName, message, options) {
24
+ super("invalid_tool_arguments", `${toolName}: ${message}`, options);
25
+ this.toolName = toolName;
26
+ }
27
+ }
28
+ export class InvalidToolOptionsError extends ToolAdapterError {
29
+ constructor(message) {
30
+ super("invalid_tool_options", message);
31
+ }
32
+ }
33
+ export class InvalidToolResultError extends ToolAdapterError {
34
+ toolName;
35
+ constructor(toolName, message) {
36
+ super("invalid_tool_result", `${toolName}: ${message}`);
37
+ this.toolName = toolName;
38
+ }
39
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./catalog.js";
2
+ export * from "./errors.js";
3
+ export { actionToolName, OBSERVATION_TOOL_NAME } from "./naming.js";
4
+ export type * from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./catalog.js";
2
+ export * from "./errors.js";
3
+ export { actionToolName, OBSERVATION_TOOL_NAME } from "./naming.js";
package/dist/json.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ type FailureFactory = (message: string) => Error;
2
+ export declare function clonePureJson(value: unknown, failure: FailureFactory): unknown;
3
+ export declare function deepFreezeJson<T>(value: T): T;
4
+ export {};
package/dist/json.js ADDED
@@ -0,0 +1,127 @@
1
+ import { types as utilTypes } from "node:util";
2
+ const MAX_JSON_NODES = 100_000;
3
+ const MAX_JSON_DEPTH = 256;
4
+ function isPlainObject(value) {
5
+ const prototype = Object.getPrototypeOf(value);
6
+ return prototype === Object.prototype || prototype === null;
7
+ }
8
+ export function clonePureJson(value, failure) {
9
+ const seen = new Set();
10
+ const pending = [];
11
+ let nodeCount = 0;
12
+ const cloneNode = (source, path, depth) => {
13
+ nodeCount += 1;
14
+ if (nodeCount > MAX_JSON_NODES) {
15
+ throw failure(`JSON value exceeds ${MAX_JSON_NODES} nodes`);
16
+ }
17
+ if (depth > MAX_JSON_DEPTH) {
18
+ throw failure(`JSON value exceeds ${MAX_JSON_DEPTH} levels`);
19
+ }
20
+ if (source === null || typeof source === "string" || typeof source === "boolean") {
21
+ return source;
22
+ }
23
+ if (typeof source === "number") {
24
+ if (!Number.isFinite(source) || (Number.isInteger(source) && !Number.isSafeInteger(source))) {
25
+ throw failure(`${path} contains a non-finite or unsafe number`);
26
+ }
27
+ return source;
28
+ }
29
+ if (typeof source !== "object") {
30
+ throw failure(`${path} is not a pure JSON value`);
31
+ }
32
+ if (utilTypes.isProxy(source)) {
33
+ throw failure(`${path} must not be a Proxy`);
34
+ }
35
+ if (seen.has(source)) {
36
+ throw failure(`${path} contains a repeated or cyclic object`);
37
+ }
38
+ seen.add(source);
39
+ if (Array.isArray(source)) {
40
+ for (const key of Reflect.ownKeys(source)) {
41
+ if (key === "length") {
42
+ continue;
43
+ }
44
+ if (typeof key !== "string" || !/^(0|[1-9][0-9]*)$/u.test(key)) {
45
+ throw failure(`${path} contains a non-JSON array property`);
46
+ }
47
+ const index = Number(key);
48
+ if (!Number.isSafeInteger(index) || index < 0 || index >= source.length) {
49
+ throw failure(`${path} contains an invalid array index`);
50
+ }
51
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
52
+ if (!descriptor?.enumerable || !("value" in descriptor)) {
53
+ throw failure(`${path}[${key}] is not a plain JSON property`);
54
+ }
55
+ }
56
+ for (let index = 0; index < source.length; index += 1) {
57
+ if (!Object.hasOwn(source, index)) {
58
+ throw failure(`${path} contains a sparse array slot`);
59
+ }
60
+ }
61
+ const target = new Array(source.length);
62
+ pending.push({ depth, kind: "array", path, source, target });
63
+ return target;
64
+ }
65
+ if (!isPlainObject(source)) {
66
+ throw failure(`${path} must use a plain JSON object`);
67
+ }
68
+ const ownKeys = Reflect.ownKeys(source);
69
+ if (ownKeys.some((key) => typeof key !== "string")) {
70
+ throw failure(`${path} contains a symbol property`);
71
+ }
72
+ const descriptors = Object.getOwnPropertyDescriptors(source);
73
+ for (const key of ownKeys) {
74
+ const descriptor = descriptors[key];
75
+ if (!descriptor?.enumerable || !("value" in descriptor)) {
76
+ throw failure(`${path}.${key} is not a plain JSON property`);
77
+ }
78
+ }
79
+ const target = {};
80
+ pending.push({ depth, descriptors, kind: "object", path, source, target });
81
+ return target;
82
+ };
83
+ const root = cloneNode(value, "$", 0);
84
+ while (pending.length > 0) {
85
+ const work = pending.pop();
86
+ if (!work) {
87
+ continue;
88
+ }
89
+ if (work.kind === "array") {
90
+ for (let index = 0; index < work.source.length; index += 1) {
91
+ work.target[index] = cloneNode(work.source[index], `${work.path}[${index}]`, work.depth + 1);
92
+ }
93
+ continue;
94
+ }
95
+ for (const [key, descriptor] of Object.entries(work.descriptors)) {
96
+ const child = cloneNode(descriptor.value, `${work.path}.${key}`, work.depth + 1);
97
+ Object.defineProperty(work.target, key, {
98
+ configurable: true,
99
+ enumerable: true,
100
+ value: child,
101
+ writable: true,
102
+ });
103
+ }
104
+ }
105
+ return root;
106
+ }
107
+ export function deepFreezeJson(value) {
108
+ if (value === null || typeof value !== "object") {
109
+ return value;
110
+ }
111
+ const pending = [value];
112
+ const seen = new Set();
113
+ while (pending.length > 0) {
114
+ const current = pending.pop();
115
+ if (!current || seen.has(current)) {
116
+ continue;
117
+ }
118
+ seen.add(current);
119
+ for (const child of Object.values(current)) {
120
+ if (child !== null && typeof child === "object") {
121
+ pending.push(child);
122
+ }
123
+ }
124
+ Object.freeze(current);
125
+ }
126
+ return value;
127
+ }
@@ -0,0 +1,2 @@
1
+ export declare const OBSERVATION_TOOL_NAME = "devicerail_observe";
2
+ export declare function actionToolName(actionName: string): string;
package/dist/naming.js ADDED
@@ -0,0 +1,21 @@
1
+ import { createHash } from "node:crypto";
2
+ import { InvalidActionSpaceError } from "./errors.js";
3
+ const ACTION_TOOL_PREFIX = "devicerail_action_";
4
+ const MAX_TOOL_NAME_LENGTH = 64;
5
+ export const OBSERVATION_TOOL_NAME = "devicerail_observe";
6
+ export function actionToolName(actionName) {
7
+ if (typeof actionName !== "string" || actionName.trim().length === 0) {
8
+ throw new InvalidActionSpaceError("action names must be non-empty strings");
9
+ }
10
+ const raw = `${ACTION_TOOL_PREFIX}raw_${actionName}`;
11
+ if (/^[A-Za-z0-9_-]+$/u.test(actionName) && raw.length <= MAX_TOOL_NAME_LENGTH) {
12
+ return raw;
13
+ }
14
+ const encoded = Buffer.from(actionName, "utf8").toString("base64url");
15
+ const base64 = `${ACTION_TOOL_PREFIX}b64_${encoded}`;
16
+ if (base64.length <= MAX_TOOL_NAME_LENGTH) {
17
+ return base64;
18
+ }
19
+ const digest = createHash("sha256").update(actionName, "utf8").digest("hex").slice(0, 32);
20
+ return `${ACTION_TOOL_PREFIX}sha256_${digest}`;
21
+ }
@@ -0,0 +1,3 @@
1
+ import type { ActionResult, Observation } from "@devicerail/protocol";
2
+ export declare function validateObservationResult(toolName: string, value: unknown): Observation;
3
+ export declare function validateActionResult(toolName: string, actionCallId: string, value: unknown, protection?: "protected"): ActionResult;
package/dist/result.js ADDED
@@ -0,0 +1,163 @@
1
+ import { ProtocolViolationError, validateRpcResult } from "@devicerail/client";
2
+ import { InvalidToolResultError } from "./errors.js";
3
+ import { clonePureJson, deepFreezeJson } from "./json.js";
4
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
5
+ const NIL_UUID = "00000000-0000-0000-0000-000000000000";
6
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/iu;
7
+ const MAX_U32 = 4_294_967_295;
8
+ function isRecord(value) {
9
+ return value !== null && typeof value === "object" && !Array.isArray(value);
10
+ }
11
+ function fail(toolName, message) {
12
+ throw new InvalidToolResultError(toolName, message);
13
+ }
14
+ function requiredSafeInteger(toolName, value, name, minimum, maximum = Number.MAX_SAFE_INTEGER) {
15
+ if (typeof value !== "number" ||
16
+ !Number.isSafeInteger(value) ||
17
+ value < minimum ||
18
+ value > maximum) {
19
+ fail(toolName, `${name} must be a safe integer between ${minimum} and ${maximum}`);
20
+ }
21
+ return value;
22
+ }
23
+ function validateAsset(toolName, value, name) {
24
+ if (!isRecord(value)) {
25
+ fail(toolName, `${name} must be an evidence object`);
26
+ }
27
+ if (typeof value.id !== "string" || value.id.trim().length === 0) {
28
+ fail(toolName, `${name}.id must be a non-empty string`);
29
+ }
30
+ if (typeof value.mediaType !== "string" ||
31
+ !value.mediaType.includes("/") ||
32
+ /\s/u.test(value.mediaType)) {
33
+ fail(toolName, `${name}.mediaType must be a valid media type`);
34
+ }
35
+ if (typeof value.uri !== "string" || value.uri.trim().length === 0) {
36
+ fail(toolName, `${name}.uri must be a non-empty string`);
37
+ }
38
+ if (value.sha256 !== undefined &&
39
+ value.sha256 !== null &&
40
+ (typeof value.sha256 !== "string" || !SHA256_PATTERN.test(value.sha256))) {
41
+ fail(toolName, `${name}.sha256 must be 64 hexadecimal characters when present`);
42
+ }
43
+ return value;
44
+ }
45
+ function validateObservation(toolName, value, name) {
46
+ if (!isRecord(value)) {
47
+ fail(toolName, `${name} must be an observation object`);
48
+ }
49
+ if (typeof value.id !== "string" ||
50
+ !UUID_PATTERN.test(value.id) ||
51
+ value.id.toLowerCase() === NIL_UUID) {
52
+ fail(toolName, `${name}.id must be a non-nil UUID`);
53
+ }
54
+ if (typeof value.deviceId !== "string" || value.deviceId.trim().length === 0) {
55
+ fail(toolName, `${name}.deviceId must be a non-empty string`);
56
+ }
57
+ requiredSafeInteger(toolName, value.capturedAtMs, `${name}.capturedAtMs`, 1);
58
+ if (!isRecord(value.viewport)) {
59
+ fail(toolName, `${name}.viewport must be an object`);
60
+ }
61
+ requiredSafeInteger(toolName, value.viewport.width, `${name}.viewport.width`, 1, MAX_U32);
62
+ requiredSafeInteger(toolName, value.viewport.height, `${name}.viewport.height`, 1, MAX_U32);
63
+ if (typeof value.viewport.scaleFactor !== "number" ||
64
+ !Number.isFinite(value.viewport.scaleFactor) ||
65
+ value.viewport.scaleFactor <= 0) {
66
+ fail(toolName, `${name}.viewport.scaleFactor must be a positive finite number`);
67
+ }
68
+ if (value.metadata !== undefined && !isRecord(value.metadata)) {
69
+ fail(toolName, `${name}.metadata must be an object when present`);
70
+ }
71
+ const hasScreenshot = value.screenshot !== undefined && value.screenshot !== null;
72
+ if (hasScreenshot) {
73
+ validateAsset(toolName, value.screenshot, `${name}.screenshot`);
74
+ }
75
+ const omission = value.screenshotOmission;
76
+ if (omission !== undefined &&
77
+ omission !== "policy" &&
78
+ omission !== "protectedAction") {
79
+ fail(toolName, `${name}.screenshotOmission must be policy or protectedAction when present`);
80
+ }
81
+ if (hasScreenshot && omission !== undefined) {
82
+ fail(toolName, `${name} must not contain both a screenshot and screenshotOmission`);
83
+ }
84
+ return {
85
+ hasScreenshot,
86
+ omission,
87
+ value: value,
88
+ };
89
+ }
90
+ function pureResult(toolName, method, value) {
91
+ const cloned = deepFreezeJson(clonePureJson(value, (message) => new InvalidToolResultError(toolName, `result ${message}`)));
92
+ try {
93
+ validateRpcResult(method, cloned);
94
+ }
95
+ catch (cause) {
96
+ if (cause instanceof ProtocolViolationError) {
97
+ throw new InvalidToolResultError(toolName, cause.message);
98
+ }
99
+ throw cause;
100
+ }
101
+ return cloned;
102
+ }
103
+ export function validateObservationResult(toolName, value) {
104
+ const cloned = pureResult(toolName, "device.observe", value);
105
+ return validateObservation(toolName, cloned, "observation").value;
106
+ }
107
+ export function validateActionResult(toolName, actionCallId, value, protection) {
108
+ const cloned = pureResult(toolName, "device.execute", value);
109
+ if (!isRecord(cloned)) {
110
+ fail(toolName, "device.execute must return an ActionResult object");
111
+ }
112
+ if (cloned.callId !== actionCallId) {
113
+ fail(toolName, "device.execute returned a result for a different Action call");
114
+ }
115
+ const startedAtMs = requiredSafeInteger(toolName, cloned.startedAtMs, "action.startedAtMs", 1);
116
+ const finishedAtMs = requiredSafeInteger(toolName, cloned.finishedAtMs, "action.finishedAtMs", 1);
117
+ if (finishedAtMs < startedAtMs) {
118
+ fail(toolName, "action.finishedAtMs must not precede action.startedAtMs");
119
+ }
120
+ if (!Object.hasOwn(cloned, "output")) {
121
+ fail(toolName, "action.output is required");
122
+ }
123
+ const before = cloned.before === undefined || cloned.before === null
124
+ ? undefined
125
+ : validateObservation(toolName, cloned.before, "action.before");
126
+ if (cloned.after === undefined || cloned.after === null) {
127
+ fail(toolName, "action.after observation is required");
128
+ }
129
+ const after = validateObservation(toolName, cloned.after, "action.after");
130
+ if (before !== undefined && before.value.deviceId !== after.value.deviceId) {
131
+ fail(toolName, "action.before and action.after must identify the same device");
132
+ }
133
+ if (protection === "protected") {
134
+ if (before === undefined) {
135
+ fail(toolName, "protected action.before observation is required");
136
+ }
137
+ const observations = [before, after];
138
+ if (observations.some((observation) => observation.omission !== "protectedAction")) {
139
+ fail(toolName, "protected action observations must explicitly omit screenshots as protectedAction");
140
+ }
141
+ }
142
+ if (!Array.isArray(cloned.evidence)) {
143
+ fail(toolName, "action.evidence must be an array");
144
+ }
145
+ if (protection === "protected" && cloned.evidence.length !== 0) {
146
+ fail(toolName, "protected action.evidence must be empty");
147
+ }
148
+ if (cloned.evidence.length === 0) {
149
+ const observations = before === undefined ? [after] : [before, after];
150
+ if (observations.some((observation) => observation.omission === undefined)) {
151
+ fail(toolName, "action.evidence may be empty only when every observation explicitly omits its screenshot");
152
+ }
153
+ }
154
+ const evidenceIds = new Set();
155
+ cloned.evidence.forEach((asset, index) => {
156
+ const validated = validateAsset(toolName, asset, `action.evidence[${index}]`);
157
+ if (evidenceIds.has(validated.id)) {
158
+ fail(toolName, `action.evidence contains duplicate id ${validated.id}`);
159
+ }
160
+ evidenceIds.add(validated.id);
161
+ });
162
+ return cloned;
163
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolInputSchema } from "./types.js";
2
+ export declare function validateToolInputSchema(actionName: string, value: unknown): ToolInputSchema;
package/dist/schema.js ADDED
@@ -0,0 +1,240 @@
1
+ import { InvalidActionSpaceError } from "./errors.js";
2
+ import { clonePureJson, deepFreezeJson } from "./json.js";
3
+ const JSON_TYPES = new Set([
4
+ "array",
5
+ "boolean",
6
+ "integer",
7
+ "null",
8
+ "number",
9
+ "object",
10
+ "string",
11
+ ]);
12
+ function isRecord(value) {
13
+ return value !== null && typeof value === "object" && !Array.isArray(value);
14
+ }
15
+ function fail(actionName, path, message) {
16
+ throw new InvalidActionSpaceError(`inputSchema for ${actionName} ${path}: ${message}`);
17
+ }
18
+ function canonicalJson(value) {
19
+ if (value === null || typeof value !== "object") {
20
+ return JSON.stringify(value);
21
+ }
22
+ if (Array.isArray(value)) {
23
+ return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
24
+ }
25
+ const record = value;
26
+ return `{${Object.keys(record)
27
+ .sort()
28
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
29
+ .join(",")}}`;
30
+ }
31
+ function stringArray(actionName, path, value) {
32
+ if (!Array.isArray(value) ||
33
+ value.some((item) => typeof item !== "string") ||
34
+ new Set(value).size !== value.length) {
35
+ fail(actionName, path, "must contain unique strings");
36
+ }
37
+ return value;
38
+ }
39
+ function schemaAt(actionName, path, value, pending) {
40
+ if (typeof value !== "boolean" && !isRecord(value)) {
41
+ fail(actionName, path, "must be a boolean or object schema");
42
+ }
43
+ pending.push({ path, schema: value });
44
+ }
45
+ function schemaMapAt(actionName, path, value, pending) {
46
+ if (!isRecord(value)) {
47
+ fail(actionName, path, "must be an object");
48
+ }
49
+ for (const [key, child] of Object.entries(value)) {
50
+ schemaAt(actionName, `${path}.${key}`, child, pending);
51
+ }
52
+ }
53
+ function schemaArrayAt(actionName, path, value, pending) {
54
+ if (!Array.isArray(value)) {
55
+ fail(actionName, path, "must be an array of schemas");
56
+ }
57
+ value.forEach((child, index) => schemaAt(actionName, `${path}[${index}]`, child, pending));
58
+ }
59
+ function nonNegativeInteger(actionName, path, value) {
60
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
61
+ fail(actionName, path, "must be a non-negative safe integer");
62
+ }
63
+ }
64
+ function numberKeyword(actionName, path, value, positive = false) {
65
+ if (typeof value !== "number" || !Number.isFinite(value) || (positive && value <= 0)) {
66
+ fail(actionName, path, positive ? "must be a positive finite number" : "must be a finite number");
67
+ }
68
+ }
69
+ function validateSchemaNode(actionName, task, pending) {
70
+ if (typeof task.schema === "boolean") {
71
+ return;
72
+ }
73
+ const schema = task.schema;
74
+ for (const keyword of [
75
+ "$schema",
76
+ "$id",
77
+ "$anchor",
78
+ "$dynamicAnchor",
79
+ "$ref",
80
+ "$dynamicRef",
81
+ "$recursiveRef",
82
+ ]) {
83
+ if (schema[keyword] !== undefined && typeof schema[keyword] !== "string") {
84
+ fail(actionName, `${task.path}.${keyword}`, "must be a string");
85
+ }
86
+ }
87
+ if (schema.$recursiveAnchor !== undefined && typeof schema.$recursiveAnchor !== "boolean") {
88
+ fail(actionName, `${task.path}.$recursiveAnchor`, "must be a boolean");
89
+ }
90
+ if (schema.type !== undefined) {
91
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
92
+ if (types.length === 0 ||
93
+ types.some((type) => typeof type !== "string" || !JSON_TYPES.has(type)) ||
94
+ new Set(types).size !== types.length) {
95
+ fail(actionName, `${task.path}.type`, "must be a JSON Schema type or unique type array");
96
+ }
97
+ }
98
+ if (schema.required !== undefined) {
99
+ stringArray(actionName, `${task.path}.required`, schema.required);
100
+ }
101
+ if (schema.enum !== undefined) {
102
+ if (!Array.isArray(schema.enum) || schema.enum.length === 0) {
103
+ fail(actionName, `${task.path}.enum`, "must be a non-empty array");
104
+ }
105
+ const values = new Set(schema.enum.map((value) => canonicalJson(value)));
106
+ if (values.size !== schema.enum.length) {
107
+ fail(actionName, `${task.path}.enum`, "must contain unique JSON values");
108
+ }
109
+ }
110
+ if (schema.examples !== undefined && !Array.isArray(schema.examples)) {
111
+ fail(actionName, `${task.path}.examples`, "must be an array");
112
+ }
113
+ for (const keyword of [
114
+ "$comment",
115
+ "title",
116
+ "description",
117
+ "format",
118
+ "contentEncoding",
119
+ "contentMediaType",
120
+ "pattern",
121
+ ]) {
122
+ if (schema[keyword] !== undefined && typeof schema[keyword] !== "string") {
123
+ fail(actionName, `${task.path}.${keyword}`, "must be a string");
124
+ }
125
+ }
126
+ for (const keyword of [
127
+ "readOnly",
128
+ "writeOnly",
129
+ "deprecated",
130
+ "uniqueItems",
131
+ ]) {
132
+ if (schema[keyword] !== undefined && typeof schema[keyword] !== "boolean") {
133
+ fail(actionName, `${task.path}.${keyword}`, "must be a boolean");
134
+ }
135
+ }
136
+ for (const keyword of ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"]) {
137
+ if (schema[keyword] !== undefined) {
138
+ numberKeyword(actionName, `${task.path}.${keyword}`, schema[keyword]);
139
+ }
140
+ }
141
+ if (schema.multipleOf !== undefined) {
142
+ numberKeyword(actionName, `${task.path}.multipleOf`, schema.multipleOf, true);
143
+ }
144
+ for (const keyword of [
145
+ "minLength",
146
+ "maxLength",
147
+ "minItems",
148
+ "maxItems",
149
+ "minContains",
150
+ "maxContains",
151
+ "minProperties",
152
+ "maxProperties",
153
+ ]) {
154
+ if (schema[keyword] !== undefined) {
155
+ nonNegativeInteger(actionName, `${task.path}.${keyword}`, schema[keyword]);
156
+ }
157
+ }
158
+ for (const keyword of [
159
+ "$defs",
160
+ "definitions",
161
+ "properties",
162
+ "patternProperties",
163
+ "dependentSchemas",
164
+ ]) {
165
+ if (schema[keyword] !== undefined) {
166
+ schemaMapAt(actionName, `${task.path}.${keyword}`, schema[keyword], pending);
167
+ }
168
+ }
169
+ for (const keyword of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
170
+ if (schema[keyword] !== undefined) {
171
+ schemaArrayAt(actionName, `${task.path}.${keyword}`, schema[keyword], pending);
172
+ }
173
+ }
174
+ for (const keyword of [
175
+ "additionalItems",
176
+ "additionalProperties",
177
+ "unevaluatedProperties",
178
+ "propertyNames",
179
+ "contains",
180
+ "unevaluatedItems",
181
+ "not",
182
+ "if",
183
+ "then",
184
+ "else",
185
+ "contentSchema",
186
+ ]) {
187
+ if (schema[keyword] !== undefined) {
188
+ schemaAt(actionName, `${task.path}.${keyword}`, schema[keyword], pending);
189
+ }
190
+ }
191
+ if (schema.items !== undefined) {
192
+ if (Array.isArray(schema.items)) {
193
+ schemaArrayAt(actionName, `${task.path}.items`, schema.items, pending);
194
+ }
195
+ else {
196
+ schemaAt(actionName, `${task.path}.items`, schema.items, pending);
197
+ }
198
+ }
199
+ if (schema.dependencies !== undefined) {
200
+ if (!isRecord(schema.dependencies)) {
201
+ fail(actionName, `${task.path}.dependencies`, "must be an object");
202
+ }
203
+ for (const [key, dependency] of Object.entries(schema.dependencies)) {
204
+ if (Array.isArray(dependency)) {
205
+ stringArray(actionName, `${task.path}.dependencies.${key}`, dependency);
206
+ }
207
+ else {
208
+ schemaAt(actionName, `${task.path}.dependencies.${key}`, dependency, pending);
209
+ }
210
+ }
211
+ }
212
+ if (schema.dependentRequired !== undefined) {
213
+ if (!isRecord(schema.dependentRequired)) {
214
+ fail(actionName, `${task.path}.dependentRequired`, "must be an object");
215
+ }
216
+ for (const [key, required] of Object.entries(schema.dependentRequired)) {
217
+ stringArray(actionName, `${task.path}.dependentRequired.${key}`, required);
218
+ }
219
+ }
220
+ if (schema.$vocabulary !== undefined) {
221
+ if (!isRecord(schema.$vocabulary) ||
222
+ Object.values(schema.$vocabulary).some((required) => typeof required !== "boolean")) {
223
+ fail(actionName, `${task.path}.$vocabulary`, "must map vocabulary URIs to booleans");
224
+ }
225
+ }
226
+ }
227
+ export function validateToolInputSchema(actionName, value) {
228
+ const cloned = clonePureJson(value, (message) => new InvalidActionSpaceError(`inputSchema for ${actionName}: ${message}`));
229
+ if (!isRecord(cloned) || cloned.type !== "object") {
230
+ throw new InvalidActionSpaceError(`inputSchema for ${actionName} must be a JSON object with type: object`);
231
+ }
232
+ const pending = [{ path: "$", schema: cloned }];
233
+ while (pending.length > 0) {
234
+ const task = pending.pop();
235
+ if (task) {
236
+ validateSchemaNode(actionName, task, pending);
237
+ }
238
+ }
239
+ return deepFreezeJson(cloned);
240
+ }