@try-works/dsh-model-as-tool 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/bin/dsh-tool-mode-mcp-stdio.mjs +80 -0
- package/catalog/desert-ant-models.json +77 -13
- package/lib/adapters/index.d.ts +19 -6
- package/lib/adapters/index.d.ts.map +1 -1
- package/lib/adapters/index.js +28 -13
- package/lib/adapters/tongue.d.ts +11 -3
- package/lib/adapters/tongue.d.ts.map +1 -1
- package/lib/adapters/tongue.js +12 -4
- package/lib/adapters/types.d.ts +11 -0
- package/lib/adapters/types.d.ts.map +1 -1
- package/lib/adapters/voz.d.ts +7 -1
- package/lib/adapters/voz.d.ts.map +1 -1
- package/lib/adapters/voz.js +16 -4
- package/lib/asset-acquisition.d.ts +137 -0
- package/lib/asset-acquisition.d.ts.map +1 -0
- package/lib/asset-acquisition.js +161 -0
- package/lib/authority.d.ts +153 -0
- package/lib/authority.d.ts.map +1 -0
- package/lib/authority.js +194 -0
- package/lib/bindings.d.ts +115 -0
- package/lib/bindings.d.ts.map +1 -0
- package/lib/bindings.js +239 -0
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +57 -0
- package/lib/contract-payload.d.ts +47 -0
- package/lib/contract-payload.d.ts.map +1 -0
- package/lib/contract-payload.js +137 -0
- package/lib/deployment-coordinator.d.ts +213 -0
- package/lib/deployment-coordinator.d.ts.map +1 -0
- package/lib/deployment-coordinator.js +274 -0
- package/lib/harness-budgets.d.ts +158 -0
- package/lib/harness-budgets.d.ts.map +1 -0
- package/lib/harness-budgets.js +152 -0
- package/lib/index.d.ts +41 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +424 -16
- package/lib/licensing.d.ts +221 -0
- package/lib/licensing.d.ts.map +1 -0
- package/lib/licensing.js +188 -0
- package/lib/mcp/host-binding.d.ts +91 -0
- package/lib/mcp/host-binding.d.ts.map +1 -0
- package/lib/mcp/host-binding.js +133 -0
- package/lib/mcp/http.d.ts +130 -0
- package/lib/mcp/http.d.ts.map +1 -0
- package/lib/mcp/http.js +274 -0
- package/lib/mcp/protocol.d.ts +261 -0
- package/lib/mcp/protocol.d.ts.map +1 -0
- package/lib/mcp/protocol.js +387 -0
- package/lib/mcp/server.d.ts +115 -0
- package/lib/mcp/server.d.ts.map +1 -0
- package/lib/mcp/server.js +200 -0
- package/lib/mcp/stdio.d.ts +80 -0
- package/lib/mcp/stdio.d.ts.map +1 -0
- package/lib/mcp/stdio.js +176 -0
- package/lib/mcp/subscriptions.d.ts +135 -0
- package/lib/mcp/subscriptions.d.ts.map +1 -0
- package/lib/mcp/subscriptions.js +207 -0
- package/lib/provider-bridge.d.ts +69 -0
- package/lib/provider-bridge.d.ts.map +1 -0
- package/lib/provider-bridge.js +218 -0
- package/lib/publication.d.ts +160 -0
- package/lib/publication.d.ts.map +1 -0
- package/lib/publication.js +173 -0
- package/lib/receipt.d.ts +208 -0
- package/lib/receipt.d.ts.map +1 -0
- package/lib/receipt.js +150 -0
- package/lib/registrar.d.ts +13 -0
- package/lib/registrar.d.ts.map +1 -1
- package/lib/registrar.js +20 -3
- package/lib/resource-governance.d.ts +204 -0
- package/lib/resource-governance.d.ts.map +1 -0
- package/lib/resource-governance.js +213 -0
- package/lib/server-lifecycle.d.ts +175 -0
- package/lib/server-lifecycle.d.ts.map +1 -0
- package/lib/server-lifecycle.js +269 -0
- package/lib/types.d.ts +21 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +9 -2
package/lib/authority.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caller authority: the grant a tool invocation runs under (SEC-03, SEC-06).
|
|
3
|
+
*
|
|
4
|
+
* The governing rule is BASE-05: an external request cannot supply or forge these
|
|
5
|
+
* authorities. Everything here is constructed by the HOST from operator
|
|
6
|
+
* configuration, never from a request body, a `clientInfo.name`, a requested tool
|
|
7
|
+
* list, or a path a caller supplied. A caller's arguments are then checked
|
|
8
|
+
* AGAINST the grant; they never expand it.
|
|
9
|
+
*
|
|
10
|
+
* Path handling is deliberately paranoid. SEC-06 requires resolving Windows drive
|
|
11
|
+
* letters, separators, Unicode, junctions and symlinks safely, rejecting escapes,
|
|
12
|
+
* unsupported UNC/device paths and unauthorized roots, and refusing when
|
|
13
|
+
* authorization cannot be established. The resolver therefore returns the
|
|
14
|
+
* CANONICAL path for the caller to open, rather than validating a string and
|
|
15
|
+
* letting a later open re-resolve it — that gap is the classic check-then-open
|
|
16
|
+
* race, and a symlink swapped between the two steps would win it.
|
|
17
|
+
*
|
|
18
|
+
* @module @try-works/dsh-model-as-tool/authority
|
|
19
|
+
*/
|
|
20
|
+
import { realpathSync } from 'node:fs';
|
|
21
|
+
import { isAbsolute, sep } from 'node:path';
|
|
22
|
+
/** A refused invocation. Carries a recovery hint, not just a refusal. */
|
|
23
|
+
export class AuthorityError extends Error {
|
|
24
|
+
name = 'AuthorityError';
|
|
25
|
+
code;
|
|
26
|
+
/** Actionable next step for the caller or operator. */
|
|
27
|
+
hint;
|
|
28
|
+
constructor(code, message, hint) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.hint = hint;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Conservative defaults: a grant that declares nothing permits almost nothing. */
|
|
35
|
+
export const DEFAULT_LIMITS = {
|
|
36
|
+
maxInputBytes: 64 * 1024 * 1024,
|
|
37
|
+
maxAudioSeconds: 3600,
|
|
38
|
+
maxResultBytes: 8 * 1024 * 1024,
|
|
39
|
+
};
|
|
40
|
+
/** Whether this process is on a case-insensitive filesystem. */
|
|
41
|
+
const CASE_INSENSITIVE = process.platform === 'win32' || process.platform === 'darwin';
|
|
42
|
+
/**
|
|
43
|
+
* Canonicalize a root for containment comparison.
|
|
44
|
+
*
|
|
45
|
+
* A root that does not exist is still admitted, because an operator may declare a
|
|
46
|
+
* root before creating it; it simply can never contain anything until it exists,
|
|
47
|
+
* which fails closed.
|
|
48
|
+
* @param root - the declared root.
|
|
49
|
+
* @returns the canonical root, or undefined when it cannot be resolved yet.
|
|
50
|
+
*/
|
|
51
|
+
function canonicalizeRoot(root) {
|
|
52
|
+
if (!isAbsolute(root))
|
|
53
|
+
return undefined;
|
|
54
|
+
try {
|
|
55
|
+
return realpathSync.native(root);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Build a grant from operator configuration.
|
|
63
|
+
*
|
|
64
|
+
* This is the only constructor: a request cannot reach it, and an approved root
|
|
65
|
+
* that is not absolute or not resolvable is dropped rather than trusted.
|
|
66
|
+
* @param input - the operator's declaration.
|
|
67
|
+
* @returns the canonicalized grant.
|
|
68
|
+
*/
|
|
69
|
+
export function createGrant(input) {
|
|
70
|
+
const roots = [];
|
|
71
|
+
for (const root of input.approvedRoots) {
|
|
72
|
+
const canonical = canonicalizeRoot(root);
|
|
73
|
+
// A root that cannot be canonicalized is dropped, not guessed at. Dropping
|
|
74
|
+
// fails closed; keeping an unresolved string would fail open.
|
|
75
|
+
if (canonical !== undefined)
|
|
76
|
+
roots.push(canonical);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
grantId: input.grantId,
|
|
80
|
+
allowedTools: [...input.allowedTools],
|
|
81
|
+
approvedRoots: roots,
|
|
82
|
+
remoteDestinations: [...(input.remoteDestinations ?? [])],
|
|
83
|
+
hostedCode: input.hostedCode ?? false,
|
|
84
|
+
recordingPolicy: input.recordingPolicy ?? 'none',
|
|
85
|
+
limits: { ...DEFAULT_LIMITS, ...input.limits },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Compare two paths for equality under this platform's case rules. */
|
|
89
|
+
function samePath(a, b) {
|
|
90
|
+
return CASE_INSENSITIVE ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
91
|
+
}
|
|
92
|
+
/** Whether `child` is `root` itself or lies beneath it, on a separator boundary. */
|
|
93
|
+
function isWithin(child, root) {
|
|
94
|
+
if (samePath(child, root))
|
|
95
|
+
return true;
|
|
96
|
+
const prefix = root.endsWith(sep) ? root : root + sep;
|
|
97
|
+
return CASE_INSENSITIVE
|
|
98
|
+
? child.toLowerCase().startsWith(prefix.toLowerCase())
|
|
99
|
+
: child.startsWith(prefix);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Reject path shapes v1 does not admit, before any filesystem call.
|
|
103
|
+
* @param candidate - the caller-supplied path.
|
|
104
|
+
* @throws AuthorityError with `PATH_UNSUPPORTED` or `PATH_NOT_ABSOLUTE`.
|
|
105
|
+
*/
|
|
106
|
+
function assertAdmissibleShape(candidate) {
|
|
107
|
+
// Windows device namespaces bypass normal path handling entirely, and a UNC
|
|
108
|
+
// path reaches another host that no approved root can describe.
|
|
109
|
+
if (candidate.startsWith('\\\\') || candidate.startsWith('//')) {
|
|
110
|
+
throw new AuthorityError('PATH_UNSUPPORTED', 'UNC and Windows device paths are not admitted', 'Use an absolute path on a local drive that lies under an approved root.');
|
|
111
|
+
}
|
|
112
|
+
if (candidate.includes('\0')) {
|
|
113
|
+
throw new AuthorityError('PATH_UNSUPPORTED', 'the path contains a NUL byte', 'Pass the path as a plain string without embedded control characters.');
|
|
114
|
+
}
|
|
115
|
+
if (!isAbsolute(candidate)) {
|
|
116
|
+
throw new AuthorityError('PATH_NOT_ABSOLUTE', 'relative paths are not admitted; v1 authorizes absolute paths only', 'Resolve the path to an absolute path under an approved root before calling.');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Authorize one caller-supplied path against a grant (SEC-06).
|
|
121
|
+
*
|
|
122
|
+
* Returns the CANONICAL path, which the caller must open. Validating the original
|
|
123
|
+
* string and opening it later would leave a window in which a symlink could be
|
|
124
|
+
* swapped; resolving once and returning the result closes it.
|
|
125
|
+
* @param grant - the caller's grant.
|
|
126
|
+
* @param candidate - the caller-supplied path.
|
|
127
|
+
* @returns the canonical, authorized path.
|
|
128
|
+
* @throws AuthorityError when the path is inadmissible, missing, or outside the grant.
|
|
129
|
+
*/
|
|
130
|
+
export function authorizePath(grant, candidate) {
|
|
131
|
+
if (typeof candidate !== 'string' || candidate.length === 0) {
|
|
132
|
+
throw new AuthorityError('PATH_NOT_ABSOLUTE', 'a path is required', 'Supply an absolute path under an approved root.');
|
|
133
|
+
}
|
|
134
|
+
assertAdmissibleShape(candidate);
|
|
135
|
+
let canonical;
|
|
136
|
+
try {
|
|
137
|
+
// Resolves junctions and symlinks, so containment is checked against what the
|
|
138
|
+
// path actually IS rather than what it spells.
|
|
139
|
+
canonical = realpathSync.native(candidate);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
throw new AuthorityError('PATH_NOT_FOUND', 'the path does not exist, so it cannot be authorized', 'Check the path, or ask the operator to add its root to the grant.');
|
|
143
|
+
}
|
|
144
|
+
for (const root of grant.approvedRoots) {
|
|
145
|
+
if (isWithin(canonical, root))
|
|
146
|
+
return canonical;
|
|
147
|
+
}
|
|
148
|
+
// The message names the grant, not the allowed roots: enumerating a caller's
|
|
149
|
+
// permitted directories would disclose the operator's configuration.
|
|
150
|
+
throw new AuthorityError('PATH_OUTSIDE_GRANT', 'the path resolves outside every root approved for this caller', 'Ask the operator to add the containing root to this grant.');
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Authorize a tool name against a grant (SEC-03).
|
|
154
|
+
* @param grant - the caller's grant.
|
|
155
|
+
* @param toolName - the requested tool.
|
|
156
|
+
* @throws AuthorityError with `TOOL_NOT_GRANTED`.
|
|
157
|
+
*/
|
|
158
|
+
export function authorizeTool(grant, toolName) {
|
|
159
|
+
if (!grant.allowedTools.includes(toolName)) {
|
|
160
|
+
throw new AuthorityError('TOOL_NOT_GRANTED', `tool "${toolName}" is not granted to this caller`, 'Request access from the operator; a requested tool list does not extend a grant.');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Authorize a remote destination against a grant (SEC-10).
|
|
165
|
+
*
|
|
166
|
+
* No silent local-to-remote fallback: if a destination was not explicitly
|
|
167
|
+
* approved, the call is refused rather than downgraded or upgraded.
|
|
168
|
+
* @param grant - the caller's grant.
|
|
169
|
+
* @param destination - the destination origin.
|
|
170
|
+
* @throws AuthorityError with `TOOL_NOT_GRANTED`.
|
|
171
|
+
*/
|
|
172
|
+
export function authorizeDestination(grant, destination) {
|
|
173
|
+
if (!grant.remoteDestinations.includes(destination)) {
|
|
174
|
+
throw new AuthorityError('TOOL_NOT_GRANTED', `destination "${destination}" is not authorized for this caller`, 'A remote destination requires explicit authorization; there is no silent fallback.');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Build the invocation authority for one call.
|
|
179
|
+
*
|
|
180
|
+
* The result is constructed by the host; a request body has no path to it.
|
|
181
|
+
* @param grant - the caller's grant.
|
|
182
|
+
* @param callerKind - how the host classified the caller.
|
|
183
|
+
* @returns the authority handed to the implementation.
|
|
184
|
+
*/
|
|
185
|
+
export function createInvocationAuthority(grant, callerKind) {
|
|
186
|
+
return {
|
|
187
|
+
grantId: grant.grantId,
|
|
188
|
+
callerKind,
|
|
189
|
+
limits: grant.limits,
|
|
190
|
+
recordingPolicy: grant.recordingPolicy,
|
|
191
|
+
resolvePath: candidate => authorizePath(grant, candidate),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=authority.js.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract-driven registration: one tool per contract, not one per model.
|
|
3
|
+
*
|
|
4
|
+
* This is the host half of BASE-02. The contract decides the tool's name, schema
|
|
5
|
+
* and description; the implementation behind it is host configuration. A second
|
|
6
|
+
* model implementing `stt.transcribe` therefore joins the existing
|
|
7
|
+
* `stt_transcribe` tool rather than appearing as a competing duplicate — which is
|
|
8
|
+
* the behaviour the previous per-model registrar could not express.
|
|
9
|
+
*
|
|
10
|
+
* The host also owns provenance (DECL-03): `meta` is stamped here, from host
|
|
11
|
+
* state, so an implementation cannot claim another package's identity.
|
|
12
|
+
*
|
|
13
|
+
* @module @try-works/dsh-model-as-tool/bindings
|
|
14
|
+
*/
|
|
15
|
+
import { type ExecutionLocation, type ToolContract, type ToolMeta } from '@try-works/tool-mode-contracts';
|
|
16
|
+
import type { ModelCatalogEntry, ModelRoute } from './types.ts';
|
|
17
|
+
import { type InvocationReceipt } from './receipt.ts';
|
|
18
|
+
import { type InvocationAuthority, type ToolGrant } from './authority.ts';
|
|
19
|
+
import type { ModelAdapter } from './adapters/index.ts';
|
|
20
|
+
/** Host identity stamped into every result. */
|
|
21
|
+
export interface HostIdentity {
|
|
22
|
+
/** The host package name. */
|
|
23
|
+
readonly extension: string;
|
|
24
|
+
/** The host package version. */
|
|
25
|
+
readonly extensionVersion: string;
|
|
26
|
+
/** Activation generation, incremented when the tool surface changes. */
|
|
27
|
+
readonly activationGeneration: number;
|
|
28
|
+
/** Receipt id factory; injectable so tests are deterministic. */
|
|
29
|
+
readonly nextReceiptId: () => string;
|
|
30
|
+
/**
|
|
31
|
+
* The grant this tool surface runs under (SEC-03).
|
|
32
|
+
*
|
|
33
|
+
* Built by the host from operator configuration. A caller's request cannot
|
|
34
|
+
* reach it, which is what makes the authority trustworthy (BASE-05).
|
|
35
|
+
*/
|
|
36
|
+
readonly grant: ToolGrant;
|
|
37
|
+
/** How the host classified this caller. */
|
|
38
|
+
readonly callerKind: InvocationAuthority['callerKind'];
|
|
39
|
+
/**
|
|
40
|
+
* Where an invocation receipt is delivered (EXT-35).
|
|
41
|
+
*
|
|
42
|
+
* Optional so a host without a receipt store still works; when present it sees one
|
|
43
|
+
* receipt per SUCCESSFUL call, which is what makes the provenance recoverable
|
|
44
|
+
* after the fact rather than only during the call.
|
|
45
|
+
*/
|
|
46
|
+
readonly recordReceipt?: (receipt: InvocationReceipt) => void;
|
|
47
|
+
}
|
|
48
|
+
/** One contract with the implementation chosen to serve it. */
|
|
49
|
+
export interface ContractBinding {
|
|
50
|
+
/** The contract that defines the tool. */
|
|
51
|
+
readonly contract: ToolContract;
|
|
52
|
+
/** The catalog entry selected to execute it. */
|
|
53
|
+
readonly implementation: ModelCatalogEntry;
|
|
54
|
+
/** The adapter that invokes that model. */
|
|
55
|
+
readonly adapter: ModelAdapter;
|
|
56
|
+
/** The route the capability resolver proved, used for `executionLocation`. */
|
|
57
|
+
readonly route: ModelRoute;
|
|
58
|
+
/** Other runnable implementations of the same contract, for reporting. */
|
|
59
|
+
readonly alternatives: readonly string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Translate a resolver route into the caller-facing execution location (SPEC §6).
|
|
63
|
+
*
|
|
64
|
+
* "Local" is deliberately not one value: a bridge is another machine, and a
|
|
65
|
+
* caller reasoning about data boundaries needs to know which.
|
|
66
|
+
* @param route - the resolved route.
|
|
67
|
+
* @returns the declared execution location.
|
|
68
|
+
*/
|
|
69
|
+
export declare function executionLocationOf(route: ModelRoute): ExecutionLocation;
|
|
70
|
+
/**
|
|
71
|
+
* Build the `meta` envelope for one execution.
|
|
72
|
+
*
|
|
73
|
+
* Every field comes from host state or the contract, never from the model's
|
|
74
|
+
* output, which is what makes the provenance trustworthy (DECL-03).
|
|
75
|
+
* @param binding - the contract and implementation that are executing.
|
|
76
|
+
* @param host - the host identity to stamp.
|
|
77
|
+
* @param warnings - non-fatal conditions observed during this call.
|
|
78
|
+
* @returns the envelope.
|
|
79
|
+
*/
|
|
80
|
+
export declare function buildMeta(binding: Pick<ContractBinding, 'contract' | 'implementation' | 'route'>, host: HostIdentity, warnings?: readonly string[]): ToolMeta;
|
|
81
|
+
/**
|
|
82
|
+
* Build the EXT-35 receipt for one call.
|
|
83
|
+
*
|
|
84
|
+
* Separate from {@link buildMeta} because it answers a different question: the
|
|
85
|
+
* envelope says what the caller received, while the receipt says what RAN and from
|
|
86
|
+
* which bytes. The extra provenance — the extension package, its content digest,
|
|
87
|
+
* the model and recipe identity — is what makes a receipt usable after the fact,
|
|
88
|
+
* when the version alone no longer identifies the code.
|
|
89
|
+
* @param binding - the contract and the implementation chosen for it.
|
|
90
|
+
* @param host - host identity and the receipt factory.
|
|
91
|
+
* @param meta - the envelope already stamped, for the receipt identity.
|
|
92
|
+
* @returns the invocation receipt.
|
|
93
|
+
*/
|
|
94
|
+
export declare function buildReceipt(binding: Pick<ContractBinding, 'contract' | 'implementation' | 'route'>, host: HostIdentity, meta: Pick<ToolMeta, 'receiptId'>, warnings?: readonly string[]): InvocationReceipt;
|
|
95
|
+
/**
|
|
96
|
+
* Register one contract as a single DSH tool.
|
|
97
|
+
*
|
|
98
|
+
* The tool body validates arguments against the contract, applies the cross-field
|
|
99
|
+
* rules the DSH DSL cannot express, invokes the selected implementation, then
|
|
100
|
+
* validates the RESULT before returning it. A violation surfaces as a tool
|
|
101
|
+
* failure rather than as malformed data reaching a caller (AT-11).
|
|
102
|
+
* @param ctx - the context whose tool registry receives the definition.
|
|
103
|
+
* @param binding - the contract and the implementation chosen for it.
|
|
104
|
+
* @param host - host identity to stamp into results.
|
|
105
|
+
* @returns the registered tool name and the registry disposer.
|
|
106
|
+
*/
|
|
107
|
+
export declare function registerContractTool(ctx: {
|
|
108
|
+
tools: {
|
|
109
|
+
register(definition: unknown): () => void;
|
|
110
|
+
};
|
|
111
|
+
}, binding: ContractBinding, host: HostIdentity): {
|
|
112
|
+
toolName: string;
|
|
113
|
+
dispose: () => void;
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=bindings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bindings.d.ts","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAML,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACd,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE/D,OAAO,EAA0B,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAC7E,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACf,MAAM,gBAAgB,CAAA;AACvB,OAAO,KAAK,EAAE,YAAY,EAAa,MAAM,qBAAqB,CAAA;AAElE,+CAA+C;AAC/C,MAAM,WAAW,YAAY;IAC3B,6BAA6B;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,gCAAgC;IAChC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,wEAAwE;IACxE,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAA;IACrC,iEAAiE;IACjE,QAAQ,CAAC,aAAa,EAAE,MAAM,MAAM,CAAA;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,2CAA2C;IAC3C,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAA;IACtD;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAA;CAC9D;AAED,+DAA+D;AAC/D,MAAM,WAAW,eAAe;IAC9B,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,gDAAgD;IAChD,QAAQ,CAAC,cAAc,EAAE,iBAAiB,CAAA;IAC1C,2CAA2C;IAC3C,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAC9B,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;IAC1B,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CACzC;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,iBAAiB,CAaxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,UAAU,GAAG,gBAAgB,GAAG,OAAO,CAAC,EACvE,IAAI,EAAE,YAAY,EAClB,QAAQ,GAAE,SAAS,MAAM,EAAO,GAC/B,QAAQ,CAYV;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,UAAU,GAAG,gBAAgB,GAAG,OAAO,CAAC,EACvE,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,EACjC,QAAQ,GAAE,SAAS,MAAM,EAAO,GAC/B,iBAAiB,CAoBnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE;IAAE,KAAK,EAAE;QAAE,QAAQ,CAAC,UAAU,EAAE,OAAO,GAAG,MAAM,IAAI,CAAA;KAAE,CAAA;CAAE,EAC7D,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,YAAY,GACjB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAoG3C"}
|
package/lib/bindings.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract-driven registration: one tool per contract, not one per model.
|
|
3
|
+
*
|
|
4
|
+
* This is the host half of BASE-02. The contract decides the tool's name, schema
|
|
5
|
+
* and description; the implementation behind it is host configuration. A second
|
|
6
|
+
* model implementing `stt.transcribe` therefore joins the existing
|
|
7
|
+
* `stt_transcribe` tool rather than appearing as a competing duplicate — which is
|
|
8
|
+
* the behaviour the previous per-model registrar could not express.
|
|
9
|
+
*
|
|
10
|
+
* The host also owns provenance (DECL-03): `meta` is stamped here, from host
|
|
11
|
+
* state, so an implementation cannot claim another package's identity.
|
|
12
|
+
*
|
|
13
|
+
* @module @try-works/dsh-model-as-tool/bindings
|
|
14
|
+
*/
|
|
15
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
16
|
+
import { assertValidContract, assertValidInput, assertValidOutput, crossFieldRules, toDshParameters, } from '@try-works/tool-mode-contracts';
|
|
17
|
+
import { toContractPayload, toNativeArgs } from "./contract-payload.js";
|
|
18
|
+
import { buildInvocationReceipt } from "./receipt.js";
|
|
19
|
+
import { authorizeTool, createInvocationAuthority, } from "./authority.js";
|
|
20
|
+
/**
|
|
21
|
+
* Translate a resolver route into the caller-facing execution location (SPEC §6).
|
|
22
|
+
*
|
|
23
|
+
* "Local" is deliberately not one value: a bridge is another machine, and a
|
|
24
|
+
* caller reasoning about data boundaries needs to know which.
|
|
25
|
+
* @param route - the resolved route.
|
|
26
|
+
* @returns the declared execution location.
|
|
27
|
+
*/
|
|
28
|
+
export function executionLocationOf(route) {
|
|
29
|
+
switch (route) {
|
|
30
|
+
case 'npm-native':
|
|
31
|
+
case 'npm-onnxruntime':
|
|
32
|
+
return 'host';
|
|
33
|
+
case 'cli':
|
|
34
|
+
return 'host';
|
|
35
|
+
case 'remote':
|
|
36
|
+
return 'team-server';
|
|
37
|
+
/* v8 ignore next -- closed union; a new route must fail compilation here. */
|
|
38
|
+
default:
|
|
39
|
+
return 'host';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build the `meta` envelope for one execution.
|
|
44
|
+
*
|
|
45
|
+
* Every field comes from host state or the contract, never from the model's
|
|
46
|
+
* output, which is what makes the provenance trustworthy (DECL-03).
|
|
47
|
+
* @param binding - the contract and implementation that are executing.
|
|
48
|
+
* @param host - the host identity to stamp.
|
|
49
|
+
* @param warnings - non-fatal conditions observed during this call.
|
|
50
|
+
* @returns the envelope.
|
|
51
|
+
*/
|
|
52
|
+
export function buildMeta(binding, host, warnings = []) {
|
|
53
|
+
return {
|
|
54
|
+
contract: binding.contract.contract,
|
|
55
|
+
contractVersion: binding.contract.version,
|
|
56
|
+
implementationId: binding.implementation.id,
|
|
57
|
+
extension: host.extension,
|
|
58
|
+
extensionVersion: host.extensionVersion,
|
|
59
|
+
activationGeneration: host.activationGeneration,
|
|
60
|
+
executionLocation: executionLocationOf(binding.route),
|
|
61
|
+
receiptId: host.nextReceiptId(),
|
|
62
|
+
warnings: [...warnings],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the EXT-35 receipt for one call.
|
|
67
|
+
*
|
|
68
|
+
* Separate from {@link buildMeta} because it answers a different question: the
|
|
69
|
+
* envelope says what the caller received, while the receipt says what RAN and from
|
|
70
|
+
* which bytes. The extra provenance — the extension package, its content digest,
|
|
71
|
+
* the model and recipe identity — is what makes a receipt usable after the fact,
|
|
72
|
+
* when the version alone no longer identifies the code.
|
|
73
|
+
* @param binding - the contract and the implementation chosen for it.
|
|
74
|
+
* @param host - host identity and the receipt factory.
|
|
75
|
+
* @param meta - the envelope already stamped, for the receipt identity.
|
|
76
|
+
* @returns the invocation receipt.
|
|
77
|
+
*/
|
|
78
|
+
export function buildReceipt(binding, host, meta, warnings = []) {
|
|
79
|
+
return buildInvocationReceipt({
|
|
80
|
+
receiptId: meta.receiptId,
|
|
81
|
+
contract: binding.contract.contract,
|
|
82
|
+
contractVersion: binding.contract.version,
|
|
83
|
+
implementationId: binding.implementation.id,
|
|
84
|
+
extension: binding.implementation.provider?.extension
|
|
85
|
+
?? host.extension,
|
|
86
|
+
extensionVersion: host.extensionVersion,
|
|
87
|
+
// The catalog carries no digest, so it is reported as unknown rather than
|
|
88
|
+
// invented. A receipt that guesses at provenance is worse than one that admits
|
|
89
|
+
// it does not have it.
|
|
90
|
+
extensionContentDigest: null,
|
|
91
|
+
dependencyLockDigest: null,
|
|
92
|
+
modelIdentity: binding.implementation.provider?.modelId ?? binding.implementation.id,
|
|
93
|
+
recipeVersion: null,
|
|
94
|
+
activationGeneration: host.activationGeneration,
|
|
95
|
+
executionLocation: executionLocationOf(binding.route),
|
|
96
|
+
warnings,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Register one contract as a single DSH tool.
|
|
101
|
+
*
|
|
102
|
+
* The tool body validates arguments against the contract, applies the cross-field
|
|
103
|
+
* rules the DSH DSL cannot express, invokes the selected implementation, then
|
|
104
|
+
* validates the RESULT before returning it. A violation surfaces as a tool
|
|
105
|
+
* failure rather than as malformed data reaching a caller (AT-11).
|
|
106
|
+
* @param ctx - the context whose tool registry receives the definition.
|
|
107
|
+
* @param binding - the contract and the implementation chosen for it.
|
|
108
|
+
* @param host - host identity to stamp into results.
|
|
109
|
+
* @returns the registered tool name and the registry disposer.
|
|
110
|
+
*/
|
|
111
|
+
export function registerContractTool(ctx, binding, host) {
|
|
112
|
+
const { contract, adapter, implementation } = binding;
|
|
113
|
+
// Registration and argument validation must agree, so the contract is checked
|
|
114
|
+
// here rather than trusted: a definition that cannot compile would otherwise
|
|
115
|
+
// fail only when a caller happened to hit the broken branch.
|
|
116
|
+
assertValidContract(contract);
|
|
117
|
+
const rules = crossFieldRules(contract);
|
|
118
|
+
const definition = defineTool({
|
|
119
|
+
name: contract.toolName,
|
|
120
|
+
description: contract.description,
|
|
121
|
+
// `toDshParameters` returns the projected JSON-Schema subset; the registry's
|
|
122
|
+
// DSL type is structurally narrower, and `ProjectionError` has already
|
|
123
|
+
// refused any keyword the DSL cannot express.
|
|
124
|
+
parameters: toDshParameters(contract),
|
|
125
|
+
output: {
|
|
126
|
+
/**
|
|
127
|
+
* An open object schema, and DECL-02 names this as INADEQUATE.
|
|
128
|
+
*
|
|
129
|
+
* The requirement says in as many words that an unconstrained
|
|
130
|
+
* `{type: "object", additionalProperties: true}` is not an adequate implementation
|
|
131
|
+
* of an ordinary leaf tool's contract. It is right, and this repository cannot yet
|
|
132
|
+
* do better: the contract's real output schema CANNOT be projected into DSH's
|
|
133
|
+
* value-schema DSL. Attempting it produced four distinct rejections at
|
|
134
|
+
* registration, recorded here because the next person will try the same thing:
|
|
135
|
+
*
|
|
136
|
+
* 1. `schema.required is not supported by the value schema DSL`
|
|
137
|
+
* 2. `schema.properties.candidates.items.required is not supported` - so required
|
|
138
|
+
* is unsupported at EVERY depth, not merely at the root
|
|
139
|
+
* 3. `schema.properties.words.items is not supported` - `items` is in the
|
|
140
|
+
* documented supported set but its support does not extend to an array nested
|
|
141
|
+
* inside an object, which is what every output schema has
|
|
142
|
+
* 4. `schema.properties.reliability.enum must be a non-empty array of scalar
|
|
143
|
+
* values` - and separate attempts to express the union type as `oneOf` were
|
|
144
|
+
* rejected too, with `oneOf must be an array of at least two value schemas`
|
|
145
|
+
*
|
|
146
|
+
* Stripping each keyword in turn does converge, but it converges on a schema that
|
|
147
|
+
* advertises almost nothing while APPEARING to be a real declaration - which is
|
|
148
|
+
* worse than this open object, because a client would trust it. The contract
|
|
149
|
+
* itself is unchanged and complete: `assertValidOutput` enforces the real schema at
|
|
150
|
+
* runtime against the untouched contract, so results ARE validated. What is
|
|
151
|
+
* missing is the ADVERTISEMENT.
|
|
152
|
+
*/
|
|
153
|
+
schema: { type: 'object', additionalProperties: true },
|
|
154
|
+
render: (_args, value) => {
|
|
155
|
+
const record = value;
|
|
156
|
+
return [{ type: 'text', text: renderForModel(contract, record) }];
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
async execute(args, exec) {
|
|
160
|
+
const record = args;
|
|
161
|
+
assertValidInput(contract, record);
|
|
162
|
+
for (const rule of rules) {
|
|
163
|
+
const violation = rule(record);
|
|
164
|
+
if (violation !== undefined) {
|
|
165
|
+
throw new Error(`${contract.toolName}: ${violation}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// SEC-03: the grant is consulted before any work happens, so an ungranted
|
|
169
|
+
// tool is refused before inference rather than after it.
|
|
170
|
+
authorizeTool(host.grant, contract.toolName);
|
|
171
|
+
const authority = createInvocationAuthority(host.grant, host.callerKind);
|
|
172
|
+
// The contract's caller-facing arguments are translated into the adapter's
|
|
173
|
+
// native vocabulary before invocation; the reverse translation runs on the
|
|
174
|
+
// result. Both are explicit, so a name change on either side is a compile-
|
|
175
|
+
// or test-time failure rather than a runtime mystery.
|
|
176
|
+
const nativeArgs = toNativeArgs(contract.contract, record);
|
|
177
|
+
const native = await adapter.invoke({ args: nativeArgs, signal: exec.signal, authority });
|
|
178
|
+
// The implementation's native result is translated into the contract's
|
|
179
|
+
// payload before validation: the contract is the authority, so a mismatch is
|
|
180
|
+
// resolved here rather than by loosening the schema.
|
|
181
|
+
const payload = toContractPayload(contract.contract, native, record);
|
|
182
|
+
// `ToolMeta` is a plain interface without an index signature, so it is not
|
|
183
|
+
// structurally a `JsonValue`; the value is losslessly JSON either way, and
|
|
184
|
+
// `assertValidOutput` is the real check.
|
|
185
|
+
const meta = buildMeta(binding, host);
|
|
186
|
+
const value = { ...payload, meta };
|
|
187
|
+
assertValidOutput(contract, value);
|
|
188
|
+
// EXT-35: the receipt is emitted once the call SUCCEEDED. A receipt for a
|
|
189
|
+
// call that failed validation would claim provenance for a result nobody
|
|
190
|
+
// received, so the sink sees only calls that actually completed.
|
|
191
|
+
host.recordReceipt?.(buildReceipt(binding, host, meta));
|
|
192
|
+
return value;
|
|
193
|
+
},
|
|
194
|
+
presentCall(callArgs) {
|
|
195
|
+
const primary = Object.values(callArgs)[0];
|
|
196
|
+
return {
|
|
197
|
+
card: 'generic',
|
|
198
|
+
title: contract.toolName,
|
|
199
|
+
kind: 'read',
|
|
200
|
+
...typeof primary === 'string' ? { rawInput: primary } : {},
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
void implementation;
|
|
205
|
+
return { toolName: contract.toolName, dispose: ctx.tools.register(definition) };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Render a contract result for the model.
|
|
209
|
+
*
|
|
210
|
+
* The canonical value is the source of truth; this only chooses the presentation.
|
|
211
|
+
* It never invents a field the contract did not declare.
|
|
212
|
+
* @param contract - the contract that produced the value.
|
|
213
|
+
* @param value - the validated canonical value.
|
|
214
|
+
* @returns a short text projection.
|
|
215
|
+
*/
|
|
216
|
+
function renderForModel(contract, value) {
|
|
217
|
+
if (contract.contract === 'text.language_detect') {
|
|
218
|
+
const language = value.language;
|
|
219
|
+
const candidates = Array.isArray(value.candidates) ? value.candidates : [];
|
|
220
|
+
const detail = candidates
|
|
221
|
+
.filter((candidate) => typeof candidate === 'object' && candidate !== null
|
|
222
|
+
&& typeof candidate.language === 'string')
|
|
223
|
+
.map(candidate => `${candidate.language} ${(candidate.probability * 100).toFixed(1)}%`)
|
|
224
|
+
.join(', ');
|
|
225
|
+
const tooClose = value.tooCloseToCall === true ? ' (too close to call)' : '';
|
|
226
|
+
return `Detected language: ${String(language)}${tooClose}`
|
|
227
|
+
+ (detail.length > 0 ? `\nCandidates: ${detail}` : '');
|
|
228
|
+
}
|
|
229
|
+
if (contract.contract === 'stt.transcribe') {
|
|
230
|
+
const text = typeof value.text === 'string' ? value.text : '';
|
|
231
|
+
const coverage = value.coverage;
|
|
232
|
+
const partial = coverage?.complete === false
|
|
233
|
+
? `\n[warnings] partial transcript: ${String(coverage.transcribedSeconds ?? 0)}s covered\n`
|
|
234
|
+
: '';
|
|
235
|
+
return text.length > 0 ? `${text}${partial}` : `(no speech recognized)${partial}`;
|
|
236
|
+
}
|
|
237
|
+
return JSON.stringify(value, null, 2);
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=bindings.js.map
|
package/lib/catalog.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAA0D,MAAM,YAAY,CAAA;AAE3G,uEAAuE;AACvE,eAAO,MAAM,oBAAoB,QAEhC,CAAA;AAWD,8EAA8E;AAC9E,qBAAa,YAAa,SAAQ,KAAK;IACrC,SAAkB,IAAI,kBAAiB;CACxC;
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAA0D,MAAM,YAAY,CAAA;AAE3G,uEAAuE;AACvE,eAAO,MAAM,oBAAoB,QAEhC,CAAA;AAWD,8EAA8E;AAC9E,qBAAa,YAAa,SAAQ,KAAK;IACrC,SAAkB,IAAI,kBAAiB;CACxC;AAyGD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAuGhG;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,GAAE,MAA6B,GAAG,iBAAiB,EAAE,CA6BpF;AAED,mCAAmC;AACnC,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,iBAAiB,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAElG"}
|
package/lib/catalog.js
CHANGED
|
@@ -62,6 +62,61 @@ function parseParam(raw, scope, key) {
|
|
|
62
62
|
}
|
|
63
63
|
throw new CatalogError(`${scope} tool.params.${key} must be a description string or a parameter object`);
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Validate the optional `contract` binding.
|
|
67
|
+
*
|
|
68
|
+
* A model without a contract is legitimate — it keeps a per-model tool — but a
|
|
69
|
+
* malformed contract identity would bind a tool to a name no caller can address,
|
|
70
|
+
* so it fails at load rather than at registration.
|
|
71
|
+
* @param value - the raw field.
|
|
72
|
+
* @param scope - entry description for error messages.
|
|
73
|
+
* @param source - document description for error messages.
|
|
74
|
+
* @returns the contract id, or null when unbound.
|
|
75
|
+
*/
|
|
76
|
+
/**
|
|
77
|
+
* Parse a model's extension provider binding (BASE-09).
|
|
78
|
+
*
|
|
79
|
+
* The binding names the package that implements the model. It is validated here
|
|
80
|
+
* rather than trusted, because it decides which package's code the host will
|
|
81
|
+
* import: a malformed binding must fail the catalog load rather than surface later
|
|
82
|
+
* as an import of something unintended.
|
|
83
|
+
* @param raw - the raw `provider` field.
|
|
84
|
+
* @param source - the catalog source, for error messages.
|
|
85
|
+
* @returns the binding field, or an empty object when absent.
|
|
86
|
+
*/
|
|
87
|
+
function parseProvider(raw, source) {
|
|
88
|
+
if (raw === undefined || raw === null)
|
|
89
|
+
return {};
|
|
90
|
+
const value = raw;
|
|
91
|
+
const extension = value.extension;
|
|
92
|
+
const modelId = value.modelId;
|
|
93
|
+
if (typeof extension !== 'string' || extension.length === 0) {
|
|
94
|
+
throw new Error(`${source}: provider.extension must be a non-empty package name`);
|
|
95
|
+
}
|
|
96
|
+
if (typeof modelId !== 'string' || modelId.length === 0) {
|
|
97
|
+
throw new Error(`${source}: provider.modelId must be a non-empty string`);
|
|
98
|
+
}
|
|
99
|
+
// A relative path is admitted so a first-party package can be exercised from a
|
|
100
|
+
// checkout; anything else must look like a package specifier.
|
|
101
|
+
const packageLike = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(extension);
|
|
102
|
+
const relativeLike = extension.startsWith('./') || extension.startsWith('../');
|
|
103
|
+
if (!packageLike && !relativeLike) {
|
|
104
|
+
throw new Error(`${source}: provider.extension ${JSON.stringify(extension)} is neither a package specifier nor a relative path`);
|
|
105
|
+
}
|
|
106
|
+
return { provider: { extension, modelId } };
|
|
107
|
+
}
|
|
108
|
+
function parseContract(value, scope, source) {
|
|
109
|
+
if (value === null || value === undefined)
|
|
110
|
+
return null;
|
|
111
|
+
if (typeof value !== 'string') {
|
|
112
|
+
throw new CatalogError(`${source}: ${scope} field "contract" must be a contract id or null`);
|
|
113
|
+
}
|
|
114
|
+
if (!/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(value)) {
|
|
115
|
+
throw new CatalogError(`${source}: ${scope} field "contract" ${JSON.stringify(value)} must be a dotted lower-case `
|
|
116
|
+
+ 'contract identity (for example "stt.transcribe")');
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
65
120
|
/**
|
|
66
121
|
* Parse and validate one catalog entry. Validation is strict on purpose: every
|
|
67
122
|
* downstream decision (which tool to register, whether it can run, what the
|
|
@@ -139,6 +194,8 @@ export function parseCatalogEntry(raw, source, index) {
|
|
|
139
194
|
name: requireString(entry, 'name', source),
|
|
140
195
|
summary: requireString(entry, 'summary', source),
|
|
141
196
|
capability: requireString(entry, 'capability', source),
|
|
197
|
+
contract: parseContract(entry.contract, scope, source),
|
|
198
|
+
...parseProvider(entry.provider, source),
|
|
142
199
|
input: input,
|
|
143
200
|
output: requireString(entry, 'output', source),
|
|
144
201
|
languages: nullableNumber('languages'),
|