@try-works/dsh-tool-mode 0.5.44 → 0.5.46

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.
@@ -0,0 +1,291 @@
1
+ /**
2
+ * The `model-runtime` recipe: parsing, schema files and the provider factory binding
3
+ * (docs/runtime/PHASE2.md).
4
+ *
5
+ * WHY THE HOST OWNS THE PARSER. The recipe decides what the host will register and which
6
+ * module it will import, so it is a TRUST BOUNDARY. Validating it with code imported from
7
+ * the extension would let the extension define what counts as valid; importing a dedicated
8
+ * runtime-recipes package would put a Phase-2 runtime package in the host's dependency
9
+ * graph, which AT-25 and ARCH-02 exist to prevent. The rules are therefore structural and
10
+ * live here: data only, no engine, no model, no vendor SDK.
11
+ *
12
+ * WHAT THE HOST DOES DELEGATE — and the whole point of the recipe — is the ENGINE. The
13
+ * factory is resolved with `createRequire` anchored at the EXTENSION's own package.json, so
14
+ * a provider package that the extension installed is found, and one that only the host
15
+ * happens to have is not treated as available. The host declares no runtime; the package
16
+ * ships one and names it.
17
+ *
18
+ * @module @try-works/dsh-tool-mode/model-runtime-recipe
19
+ */
20
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
21
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
22
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
23
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
24
+ });
25
+ }
26
+ return path;
27
+ };
28
+ import { createRequire } from 'node:module';
29
+ import { readFileSync } from 'node:fs';
30
+ import { isAbsolute, join, resolve, sep } from 'node:path';
31
+ import { pathToFileURL } from 'node:url';
32
+ const TOP_LEVEL_KEYS = new Set(['kind', 'schemaVersion', 'provider', 'model', 'processor', 'limits', 'tool']);
33
+ const PROVIDER_KEYS = new Set(['package', 'export', 'accelerators']);
34
+ const MODEL_KEYS = new Set(['assetSet', 'fileName', 'format']);
35
+ const PROCESSOR_KEYS = new Set(['id']);
36
+ const LIMITS_KEYS = new Set(['maxInputBytes', 'timeoutMs']);
37
+ const TOOL_KEYS = new Set(['name', 'description', 'inputSchema', 'outputSchema']);
38
+ const FORMATS = new Set(['onnx', 'tflite', 'gguf']);
39
+ const ACCELERATORS = new Set(['cpu', 'dml', 'webgpu']);
40
+ /** `namespace/processor-version`, lower-case, as documented in PHASE2. */
41
+ const PROCESSOR_ID = /^[a-z0-9]+(?:\/[a-z0-9-]+)*$/;
42
+ /** Whether a value is a non-array object. */
43
+ function isRecord(value) {
44
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
45
+ }
46
+ /** Refuse with a code and a reason. */
47
+ function refuse(code, detail) {
48
+ return { ok: false, code, detail };
49
+ }
50
+ /**
51
+ * Whether an unvalidated recipe claims to be a `model-runtime` recipe.
52
+ *
53
+ * The host branches on this BEFORE any harness behaviour: a harness recipe (or one with no
54
+ * `kind`) keeps the existing path untouched.
55
+ * @param raw - the recipe as read from disk.
56
+ * @returns whether this is the model-runtime lane's business.
57
+ */
58
+ export function isModelRuntimeRecipe(raw) {
59
+ return isRecord(raw) && raw['kind'] === 'model-runtime';
60
+ }
61
+ /** Refuse any key a section does not define, naming it. */
62
+ function unknownKey(section, record, allowed) {
63
+ for (const key of Object.keys(record)) {
64
+ if (!allowed.has(key))
65
+ return refuse('unknown-key', `${section} declares the unknown key "${key}"`);
66
+ }
67
+ return undefined;
68
+ }
69
+ /** A non-empty trimmed string, or undefined. */
70
+ function nonEmptyString(value) {
71
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
72
+ }
73
+ /**
74
+ * Whether a value is a package-relative schema path that cannot leave the package.
75
+ * @param value - the declared path.
76
+ * @returns whether it is safe to resolve against the install directory.
77
+ */
78
+ function isContainedRelativePath(value) {
79
+ if (typeof value !== 'string' || !value.startsWith('./') || value.includes('\\'))
80
+ return false;
81
+ if (isAbsolute(value))
82
+ return false;
83
+ const segments = value.split('/').filter(segment => segment.length > 0);
84
+ return !segments.includes('..');
85
+ }
86
+ /**
87
+ * Parse and validate a `model-runtime` recipe (PHASE2 rules).
88
+ * @param raw - the recipe as read from disk.
89
+ * @returns the typed recipe or a named refusal.
90
+ */
91
+ export function parseModelRuntimeRecipe(raw) {
92
+ if (!isModelRuntimeRecipe(raw)) {
93
+ return refuse('not-model-runtime', 'the recipe does not declare kind "model-runtime", so the harness path applies');
94
+ }
95
+ if (!isRecord(raw))
96
+ return refuse('not-model-runtime', 'the recipe is not an object');
97
+ const top = unknownKey('the recipe', raw, TOP_LEVEL_KEYS);
98
+ if (top !== undefined)
99
+ return top;
100
+ if (raw['schemaVersion'] !== 1) {
101
+ return refuse('bad-schema-version', `schemaVersion must be 1; this recipe declares ${JSON.stringify(raw['schemaVersion'])}`);
102
+ }
103
+ const provider = raw['provider'];
104
+ if (!isRecord(provider))
105
+ return refuse('bad-provider', 'provider must be an object');
106
+ const providerUnknown = unknownKey('provider', provider, PROVIDER_KEYS);
107
+ if (providerUnknown !== undefined)
108
+ return providerUnknown;
109
+ const packageName = nonEmptyString(provider['package']);
110
+ if (packageName === undefined)
111
+ return refuse('bad-provider', 'provider.package must be a non-empty string');
112
+ const exportName = nonEmptyString(provider['export']);
113
+ if (exportName === undefined)
114
+ return refuse('bad-provider', 'provider.export must be a non-empty string');
115
+ const rawAccelerators = provider['accelerators'];
116
+ let accelerators;
117
+ if (rawAccelerators !== undefined) {
118
+ if (!Array.isArray(rawAccelerators) || rawAccelerators.length === 0) {
119
+ return refuse('bad-provider', 'provider.accelerators must be a non-empty array when declared');
120
+ }
121
+ const declared = [];
122
+ for (const accelerator of rawAccelerators) {
123
+ if (typeof accelerator !== 'string' || !ACCELERATORS.has(accelerator)) {
124
+ return refuse('bad-provider', `provider.accelerators admits cpu, dml and webgpu; got ${JSON.stringify(accelerator)}`);
125
+ }
126
+ declared.push(accelerator);
127
+ }
128
+ accelerators = declared;
129
+ }
130
+ const model = raw['model'];
131
+ if (!isRecord(model))
132
+ return refuse('bad-model', 'model must be an object');
133
+ const modelUnknown = unknownKey('model', model, MODEL_KEYS);
134
+ if (modelUnknown !== undefined)
135
+ return modelUnknown;
136
+ const assetSet = nonEmptyString(model['assetSet']);
137
+ if (assetSet === undefined)
138
+ return refuse('bad-model', 'model.assetSet must be a non-empty string');
139
+ const fileName = nonEmptyString(model['fileName']);
140
+ if (fileName === undefined)
141
+ return refuse('bad-model', 'model.fileName must be a non-empty string');
142
+ if (fileName.includes('/') || fileName.includes('\\') || fileName === '.' || fileName === '..') {
143
+ return refuse('bad-model', `model.fileName must be a bare file name; got ${JSON.stringify(fileName)}`);
144
+ }
145
+ const format = model['format'];
146
+ if (typeof format !== 'string' || !FORMATS.has(format)) {
147
+ return refuse('bad-model', `model.format admits onnx, tflite and gguf; got ${JSON.stringify(format)}`);
148
+ }
149
+ const processor = raw['processor'];
150
+ if (!isRecord(processor))
151
+ return refuse('bad-processor', 'processor must be an object');
152
+ const processorUnknown = unknownKey('processor', processor, PROCESSOR_KEYS);
153
+ if (processorUnknown !== undefined)
154
+ return processorUnknown;
155
+ const processorId = nonEmptyString(processor['id']);
156
+ if (processorId === undefined)
157
+ return refuse('bad-processor', 'processor.id must be a non-empty string');
158
+ if (!PROCESSOR_ID.test(processorId)) {
159
+ return refuse('bad-processor', `processor.id must match ${PROCESSOR_ID.source}; got ${JSON.stringify(processorId)}`);
160
+ }
161
+ const limits = raw['limits'];
162
+ if (!isRecord(limits))
163
+ return refuse('bad-limits', 'limits must be an object');
164
+ const limitsUnknown = unknownKey('limits', limits, LIMITS_KEYS);
165
+ if (limitsUnknown !== undefined)
166
+ return limitsUnknown;
167
+ for (const key of ['maxInputBytes', 'timeoutMs']) {
168
+ const value = limits[key];
169
+ if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
170
+ return refuse('bad-limits', `limits.${key} must be a positive integer; got ${JSON.stringify(value)}`);
171
+ }
172
+ }
173
+ const tool = raw['tool'];
174
+ if (!isRecord(tool))
175
+ return refuse('bad-tool', 'tool must be an object');
176
+ const toolUnknown = unknownKey('tool', tool, TOOL_KEYS);
177
+ if (toolUnknown !== undefined)
178
+ return toolUnknown;
179
+ const toolName = nonEmptyString(tool['name']);
180
+ if (toolName === undefined)
181
+ return refuse('bad-tool', 'tool.name must be a non-empty string');
182
+ const description = nonEmptyString(tool['description']);
183
+ if (description === undefined)
184
+ return refuse('bad-tool', 'tool.description must be a non-empty string');
185
+ for (const key of ['inputSchema', 'outputSchema']) {
186
+ if (!isContainedRelativePath(tool[key])) {
187
+ return refuse('bad-tool', `tool.${key} must be a package-relative "./..." path that stays inside the package; got ${JSON.stringify(tool[key])}`);
188
+ }
189
+ }
190
+ return {
191
+ ok: true,
192
+ recipe: {
193
+ kind: 'model-runtime',
194
+ schemaVersion: 1,
195
+ provider: { package: packageName, export: exportName, ...(accelerators === undefined ? {} : { accelerators }) },
196
+ model: { assetSet, fileName, format: format },
197
+ processor: { id: processorId },
198
+ limits: { maxInputBytes: limits['maxInputBytes'], timeoutMs: limits['timeoutMs'] },
199
+ tool: {
200
+ name: toolName,
201
+ description,
202
+ inputSchema: tool['inputSchema'],
203
+ outputSchema: tool['outputSchema'],
204
+ },
205
+ },
206
+ };
207
+ }
208
+ /**
209
+ * Read one package-relative schema file, refusing anything that could leave the package.
210
+ * @param installPath - the installed package directory.
211
+ * @param relative - the recipe's `./...` path.
212
+ * @returns the parsed schema or a named refusal.
213
+ */
214
+ export function readModelRuntimeSchema(installPath, relative) {
215
+ if (!isContainedRelativePath(relative)) {
216
+ return refuse('bad-tool', `${JSON.stringify(relative)} is not a package-relative schema path`);
217
+ }
218
+ const root = resolve(installPath);
219
+ const absolute = resolve(root, relative);
220
+ if (absolute !== root && !absolute.startsWith(root + sep)) {
221
+ return refuse('bad-tool', `${JSON.stringify(relative)} resolves outside the package`);
222
+ }
223
+ let text;
224
+ try {
225
+ text = readFileSync(absolute, 'utf8');
226
+ }
227
+ catch (error) {
228
+ return refuse('schema-unreadable', `the tool schema ${JSON.stringify(relative)} could not be read from ${absolute}: ${error instanceof Error ? error.message : String(error)}`);
229
+ }
230
+ let parsed;
231
+ try {
232
+ parsed = JSON.parse(text);
233
+ }
234
+ catch (error) {
235
+ return refuse('schema-unreadable', `the tool schema ${JSON.stringify(relative)} is not JSON: ${error instanceof Error ? error.message : String(error)}`);
236
+ }
237
+ if (!isRecord(parsed)) {
238
+ return refuse('schema-unreadable', `the tool schema ${JSON.stringify(relative)} must be a JSON object`);
239
+ }
240
+ return { ok: true, schema: parsed };
241
+ }
242
+ /**
243
+ * Resolve the recipe's provider module from the EXTENSION's own package, without importing it.
244
+ *
245
+ * Resolution is synchronous, which is what lets `registerDeclarative` — a synchronous host
246
+ * hook — refuse a package that cannot be resolved AT REGISTRATION, instead of registering a
247
+ * tool that cannot run. The import itself stays deferred to first call, so activation remains
248
+ * lightweight (EXT-19).
249
+ * @param options - the install directory and the parsed recipe.
250
+ * @returns the resolved module path or a named refusal.
251
+ */
252
+ export function resolveProviderModule(options) {
253
+ const anchor = join(options.installPath, 'package.json');
254
+ const require = createRequire(anchor);
255
+ const specifier = options.recipe.provider.package;
256
+ try {
257
+ return { ok: true, resolved: require.resolve(specifier) };
258
+ }
259
+ catch (error) {
260
+ return refuse('factory-unresolved', `the provider package ${JSON.stringify(specifier)} cannot be resolved from the extension package directory (${anchor}): `
261
+ + `${error instanceof Error ? error.message : String(error)}. The host declares no engine of its own (AT-25)`);
262
+ }
263
+ }
264
+ /**
265
+ * Import the provider factory the recipe names, resolving from the EXTENSION's own package.
266
+ *
267
+ * The anchor is the extension's `package.json`, not the host's module graph: a package that
268
+ * declares an engine dependency resolves it from its own install, and the host never
269
+ * carries one (AT-25, ARCH-02).
270
+ * @param options - the install directory and the parsed recipe.
271
+ * @returns the factory or a named refusal.
272
+ */
273
+ export async function importProviderFactory(options) {
274
+ const resolvedModule = resolveProviderModule(options);
275
+ if (!resolvedModule.ok)
276
+ return resolvedModule;
277
+ const resolved = resolvedModule.resolved;
278
+ let namespace;
279
+ try {
280
+ namespace = await import(__rewriteRelativeImportExtension(pathToFileURL(resolved).href));
281
+ }
282
+ catch (error) {
283
+ return refuse('factory-import-failed', `the provider module ${resolved} failed to import: ${error instanceof Error ? error.message : String(error)}`);
284
+ }
285
+ const candidate = namespace[options.recipe.provider.export];
286
+ if (typeof candidate !== 'function') {
287
+ return refuse('factory-not-function', `the provider module ${resolved} does not export a function named ${JSON.stringify(options.recipe.provider.export)}`);
288
+ }
289
+ return { ok: true, factory: candidate };
290
+ }
291
+ //# sourceMappingURL=model-runtime-recipe.js.map
@@ -2,7 +2,7 @@
2
2
  "name": "@try-works/tool-mode-contracts",
3
3
  "version": "0.1.1",
4
4
  "description": "Model-free standard tool contracts for Tool Mode: identities, schemas, semantics and runtime validation",
5
- "license": "MIT",
5
+ "license": "Apache-2.0",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
7
7
  "repository": {
8
8
  "type": "git",
@@ -2,7 +2,7 @@
2
2
  "name": "@try-works/tool-mode-extension-sdk",
3
3
  "version": "0.1.5",
4
4
  "description": "Author-facing SDK for Tool Mode extensions: TMX manifest schema, validation, lifecycle state and management errors",
5
- "license": "MIT",
5
+ "license": "Apache-2.0",
6
6
  "publishConfig": {
7
7
  "access": "public",
8
8
  "registry": "https://registry.npmjs.org/"
@@ -2,7 +2,7 @@
2
2
  "name": "@try-works/tool-mode-recipes",
3
3
  "version": "0.1.0",
4
4
  "description": "Declarative harness-model recipe format, a provider-neutral LLM broker and the built-in text_title and describe_image recipes",
5
- "license": "MIT",
5
+ "license": "Apache-2.0",
6
6
  "publishConfig": {
7
7
  "access": "public",
8
8
  "registry": "https://registry.npmjs.org/"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@try-works/dsh-tool-mode",
3
- "version": "0.5.44",
3
+ "version": "0.5.46",
4
4
  "description": "Install Desert Ant Labs on-device models as DSH tools and skills, with per-platform capability gating",
5
5
  "keywords": [
6
6
  "dsh",
@@ -10,7 +10,7 @@
10
10
  "desert-ant-labs",
11
11
  "models-as-tools"
12
12
  ],
13
- "license": "MIT",
13
+ "license": "Apache-2.0",
14
14
  "publishConfig": {
15
15
  "access": "public",
16
16
  "registry": "https://registry.npmjs.org/"