@try-works/dsh-model-as-tool 0.1.0 → 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.
Files changed (79) hide show
  1. package/README.md +51 -0
  2. package/bin/dsh-tool-mode-mcp-stdio.mjs +80 -0
  3. package/catalog/desert-ant-models.json +77 -13
  4. package/lib/adapters/index.d.ts +19 -6
  5. package/lib/adapters/index.d.ts.map +1 -1
  6. package/lib/adapters/index.js +28 -13
  7. package/lib/adapters/tongue.d.ts +11 -3
  8. package/lib/adapters/tongue.d.ts.map +1 -1
  9. package/lib/adapters/tongue.js +12 -4
  10. package/lib/adapters/types.d.ts +11 -0
  11. package/lib/adapters/types.d.ts.map +1 -1
  12. package/lib/adapters/voz.d.ts +7 -1
  13. package/lib/adapters/voz.d.ts.map +1 -1
  14. package/lib/adapters/voz.js +16 -4
  15. package/lib/asset-acquisition.d.ts +137 -0
  16. package/lib/asset-acquisition.d.ts.map +1 -0
  17. package/lib/asset-acquisition.js +161 -0
  18. package/lib/authority.d.ts +153 -0
  19. package/lib/authority.d.ts.map +1 -0
  20. package/lib/authority.js +194 -0
  21. package/lib/bindings.d.ts +115 -0
  22. package/lib/bindings.d.ts.map +1 -0
  23. package/lib/bindings.js +239 -0
  24. package/lib/catalog.d.ts.map +1 -1
  25. package/lib/catalog.js +57 -0
  26. package/lib/contract-payload.d.ts +47 -0
  27. package/lib/contract-payload.d.ts.map +1 -0
  28. package/lib/contract-payload.js +137 -0
  29. package/lib/deployment-coordinator.d.ts +213 -0
  30. package/lib/deployment-coordinator.d.ts.map +1 -0
  31. package/lib/deployment-coordinator.js +274 -0
  32. package/lib/harness-budgets.d.ts +158 -0
  33. package/lib/harness-budgets.d.ts.map +1 -0
  34. package/lib/harness-budgets.js +152 -0
  35. package/lib/index.d.ts +41 -0
  36. package/lib/index.d.ts.map +1 -1
  37. package/lib/index.js +424 -16
  38. package/lib/licensing.d.ts +221 -0
  39. package/lib/licensing.d.ts.map +1 -0
  40. package/lib/licensing.js +188 -0
  41. package/lib/mcp/host-binding.d.ts +91 -0
  42. package/lib/mcp/host-binding.d.ts.map +1 -0
  43. package/lib/mcp/host-binding.js +133 -0
  44. package/lib/mcp/http.d.ts +130 -0
  45. package/lib/mcp/http.d.ts.map +1 -0
  46. package/lib/mcp/http.js +274 -0
  47. package/lib/mcp/protocol.d.ts +261 -0
  48. package/lib/mcp/protocol.d.ts.map +1 -0
  49. package/lib/mcp/protocol.js +387 -0
  50. package/lib/mcp/server.d.ts +115 -0
  51. package/lib/mcp/server.d.ts.map +1 -0
  52. package/lib/mcp/server.js +200 -0
  53. package/lib/mcp/stdio.d.ts +80 -0
  54. package/lib/mcp/stdio.d.ts.map +1 -0
  55. package/lib/mcp/stdio.js +176 -0
  56. package/lib/mcp/subscriptions.d.ts +135 -0
  57. package/lib/mcp/subscriptions.d.ts.map +1 -0
  58. package/lib/mcp/subscriptions.js +207 -0
  59. package/lib/provider-bridge.d.ts +69 -0
  60. package/lib/provider-bridge.d.ts.map +1 -0
  61. package/lib/provider-bridge.js +218 -0
  62. package/lib/publication.d.ts +160 -0
  63. package/lib/publication.d.ts.map +1 -0
  64. package/lib/publication.js +173 -0
  65. package/lib/receipt.d.ts +208 -0
  66. package/lib/receipt.d.ts.map +1 -0
  67. package/lib/receipt.js +150 -0
  68. package/lib/registrar.d.ts +13 -0
  69. package/lib/registrar.d.ts.map +1 -1
  70. package/lib/registrar.js +20 -3
  71. package/lib/resource-governance.d.ts +204 -0
  72. package/lib/resource-governance.d.ts.map +1 -0
  73. package/lib/resource-governance.js +213 -0
  74. package/lib/server-lifecycle.d.ts +175 -0
  75. package/lib/server-lifecycle.d.ts.map +1 -0
  76. package/lib/server-lifecycle.js +269 -0
  77. package/lib/types.d.ts +21 -0
  78. package/lib/types.d.ts.map +1 -1
  79. package/package.json +9 -2
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Coherent publication from one versioned generation (EXT-24).
3
+ *
4
+ * EXT-24 forbids two observable states: an available tool WITHOUT its executable
5
+ * binding, and a new schema served by an old handler. The way to avoid both is to
6
+ * build the whole surface first, check it for internal consistency, and only then
7
+ * make it visible — staging plus a single switch, which is what the requirement
8
+ * asks for rather than an invented transactional registry API.
9
+ *
10
+ * The rules encoded here, each with a reason:
11
+ *
12
+ * - A tool is advertised only when a real implementation binding exists. A schema
13
+ * alone is a promise nothing can keep, and EXT-24 states that no contract is
14
+ * automatically callable merely because its schema is installed.
15
+ * - An ACTIVE package whose model is unprepared publishes readiness information and
16
+ * a preparation skill, but NOT a working inference tool. Advertising a callable
17
+ * tool that will fail on first use is worse than advertising nothing, because the
18
+ * caller has no way to tell the difference until it fails.
19
+ * - A usage skill appears only when every tool it requires is visible. A skill that
20
+ * instructs an agent to call a tool that is not there produces a confident
21
+ * failure.
22
+ * - Every skill carries its owning extension, so it can be withdrawn with that
23
+ * package rather than outliving it.
24
+ *
25
+ * @module @try-works/dsh-model-as-tool/publication
26
+ */
27
+ /**
28
+ * Build a coherent surface from the candidates available now.
29
+ *
30
+ * Pure: it decides what MAY be published, and the caller makes the result visible.
31
+ * Keeping the decision separate from the switch is what allows a surface to be
32
+ * rejected before anything observes it.
33
+ * @param options - the candidates and the generation they belong to.
34
+ * @returns the complete surface, including what was withheld and why.
35
+ * @throws Error when the candidates are internally inconsistent in a way that
36
+ * cannot be resolved by withholding, such as two tools sharing a name.
37
+ */
38
+ export function buildSurface(options) {
39
+ const tools = [];
40
+ const readiness = [];
41
+ const withheld = [];
42
+ // A duplicate tool name cannot be resolved by dropping one: the caller asked for
43
+ // a specific name and would get a different implementation than the catalog says.
44
+ const seen = new Set();
45
+ for (const candidate of options.tools) {
46
+ if (seen.has(candidate.name)) {
47
+ throw new Error(`two contracts publish the tool name ${JSON.stringify(candidate.name)}; `
48
+ + 'a name identifies one tool, so this cannot be resolved by withholding');
49
+ }
50
+ seen.add(candidate.name);
51
+ }
52
+ for (const candidate of options.tools) {
53
+ const runnable = candidate.implementations.filter(impl => impl.runnable);
54
+ for (const implementation of candidate.implementations) {
55
+ readiness.push({
56
+ extensionId: implementation.extensionId,
57
+ modelId: implementation.modelId,
58
+ readiness: implementation.readiness,
59
+ // Callable requires BOTH that the host can run it and that it is ready.
60
+ callable: implementation.runnable && implementation.readiness === 'ready',
61
+ reason: !implementation.runnable
62
+ ? 'this model cannot run on this host'
63
+ : implementation.readiness === 'ready'
64
+ ? 'prepared and callable'
65
+ : `model is ${implementation.readiness}; the tool is not advertised as working`,
66
+ });
67
+ }
68
+ const selected = candidate.implementations.find(impl => impl.modelId === candidate.selectedModelId);
69
+ if (selected === undefined || !selected.runnable) {
70
+ withheld.push({
71
+ code: 'no-implementation',
72
+ subject: candidate.name,
73
+ extensionId: selected?.extensionId ?? candidate.implementations[0]?.extensionId ?? 'unknown',
74
+ message: 'no runnable implementation is bound, so the tool is not published',
75
+ });
76
+ continue;
77
+ }
78
+ if (selected.readiness === 'failed') {
79
+ withheld.push({
80
+ code: 'model-failed',
81
+ subject: candidate.name,
82
+ extensionId: selected.extensionId,
83
+ message: 'the selected model deployment has failed and will not become ready by waiting',
84
+ });
85
+ continue;
86
+ }
87
+ if (selected.readiness !== 'ready') {
88
+ // EXT-24: an active package with an unprepared model may publish readiness
89
+ // and preparation guidance, but MUST NOT advertise working inference.
90
+ withheld.push({
91
+ code: 'needs-preparation',
92
+ subject: candidate.name,
93
+ extensionId: selected.extensionId,
94
+ message: `the selected model is ${selected.readiness}; readiness is published instead of a callable tool`,
95
+ });
96
+ continue;
97
+ }
98
+ tools.push({
99
+ name: candidate.name,
100
+ contractId: candidate.contractId,
101
+ extensionId: selected.extensionId,
102
+ modelId: selected.modelId,
103
+ readiness: selected.readiness,
104
+ alternatives: runnable.filter(impl => impl.modelId !== selected.modelId).map(impl => impl.modelId),
105
+ });
106
+ }
107
+ const visible = new Set(tools.map(tool => tool.name));
108
+ const skills = [];
109
+ for (const candidate of options.skills) {
110
+ const purpose = candidate.purpose ?? 'usage';
111
+ const required = candidate.requiresTools ?? [];
112
+ if (purpose === 'usage') {
113
+ const missing = required.filter(name => !visible.has(name));
114
+ if (missing.length > 0) {
115
+ // A usage skill whose tool is absent would instruct an agent to call
116
+ // something that is not there, producing a confident failure.
117
+ withheld.push({
118
+ code: 'unregistered-contribution',
119
+ subject: candidate.name,
120
+ extensionId: candidate.extensionId,
121
+ message: `the required tool(s) ${missing.join(', ')} are not visible, so the usage skill is withheld`,
122
+ });
123
+ continue;
124
+ }
125
+ }
126
+ skills.push({ name: candidate.name, extensionId: candidate.extensionId, purpose, requiresTools: required });
127
+ }
128
+ return { generation: options.generation, tools, skills, readiness, withheld };
129
+ }
130
+ /**
131
+ * The published surface, swapped only as a whole (EXT-24).
132
+ *
133
+ * `publish` refuses a surface that would retire a tool without retiring the skill
134
+ * that teaches it, which is the "new schema with the old handler" failure seen from
135
+ * the other side.
136
+ */
137
+ export class PublicationSurface {
138
+ #current;
139
+ #onSwap;
140
+ constructor(options = {}) {
141
+ this.#current = { generation: options.generation ?? 1, tools: [], skills: [], readiness: [], withheld: [] };
142
+ this.#onSwap = options.onSwap;
143
+ }
144
+ /** The generation currently visible. */
145
+ get current() {
146
+ return this.#current;
147
+ }
148
+ /**
149
+ * Replace the visible surface.
150
+ *
151
+ * The callback receives the whole change at once, so a consumer can update its
152
+ * declarations without ever seeing an intermediate state.
153
+ * @param next - the surface to make visible.
154
+ */
155
+ publish(next) {
156
+ const previous = this.#current;
157
+ this.#current = next;
158
+ this.#onSwap?.(next, previous);
159
+ }
160
+ /**
161
+ * Skills that must be withdrawn because their owning extension is gone.
162
+ *
163
+ * EXT-24 requires the host to track skill provenance and remove a skill with the
164
+ * package or tool that owns it, rather than leaving orphaned guidance behind.
165
+ * @param liveExtensionIds - the extensions still installed and active.
166
+ * @returns the skills that should no longer be visible.
167
+ */
168
+ orphanedSkills(liveExtensionIds) {
169
+ const live = new Set(liveExtensionIds);
170
+ return this.#current.skills.filter(skill => !live.has(skill.extensionId));
171
+ }
172
+ }
173
+ //# sourceMappingURL=publication.js.map
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Invocation receipts and lifecycle records (EXT-35).
3
+ *
4
+ * EXT-35 asks for two different things, recorded SEPARATELY, and the separation is
5
+ * the point:
6
+ *
7
+ * - An **invocation receipt** says what ran: the extension, its version and content
8
+ * digest, the model/recipe identity, and the activation generation. It travels
9
+ * with the result.
10
+ * - A **lifecycle record** says who changed the installation: install, activate,
11
+ * deactivate, update and uninstall outcomes together with the approving
12
+ * principal. It does not travel with a result, because it describes an operator
13
+ * action rather than a call.
14
+ *
15
+ * Mixing them would mean a caller reading a receipt learns about operator actions
16
+ * it has no business seeing, and an audit trail becomes indistinguishable from
17
+ * execution metadata.
18
+ *
19
+ * The metadata-only rule is enforced by CONSTRUCTION rather than by remembering to
20
+ * be careful. Nothing here accepts a request body, a token, or a full skill text,
21
+ * and a path is reduced before it can enter a receipt at all — so a future caller
22
+ * cannot leak one by passing it in.
23
+ *
24
+ * @module @try-works/dsh-model-as-tool/receipt
25
+ */
26
+ import type { ExecutionLocation } from '@try-works/tool-mode-contracts';
27
+ /** Provenance attached to one invocation. All fields are host-authored. */
28
+ export interface InvocationReceipt {
29
+ /** Opaque receipt identity, unique per call. */
30
+ readonly receiptId: string;
31
+ /** The contract that was invoked. */
32
+ readonly contract: string;
33
+ /** The contract version, so a receipt is interpretable after a schema change. */
34
+ readonly contractVersion: string;
35
+ /** The implementation selected by the host. */
36
+ readonly implementationId: string;
37
+ /** The extension package that provided it. */
38
+ readonly extension: string;
39
+ /** The extension package version. */
40
+ readonly extensionVersion: string;
41
+ /**
42
+ * The installed content digest of the extension.
43
+ *
44
+ * Recorded because a version alone does not identify bytes: a package can be
45
+ * replaced at the same version, and a receipt that only names the version could
46
+ * not distinguish the two runs.
47
+ */
48
+ readonly extensionContentDigest: string | null;
49
+ /** The resolved dependency-lock digest, when the store recorded one. */
50
+ readonly dependencyLockDigest: string | null;
51
+ /** The model or recipe identity that actually ran. */
52
+ readonly modelIdentity: string;
53
+ /** The recipe version, which pins preprocessing and postprocessing. */
54
+ readonly recipeVersion: string | null;
55
+ /** The catalog generation this invocation was served from. */
56
+ readonly activationGeneration: number;
57
+ /**
58
+ * Where inference happened.
59
+ *
60
+ * The shared vocabulary from the contracts package, reused rather than restated:
61
+ * a second definition would drift, and a receipt whose location field disagreed
62
+ * with its own envelope's would be worse than one without the field.
63
+ */
64
+ readonly executionLocation: ExecutionLocation;
65
+ /** Non-fatal notes, already free of caller content. */
66
+ readonly warnings: readonly string[];
67
+ }
68
+ /** The lifecycle operations EXT-35 requires recorded. */
69
+ export type LifecycleOperation = 'install' | 'activate' | 'deactivate' | 'update' | 'uninstall' | 'prepare';
70
+ /** The outcome of a lifecycle operation. */
71
+ export type LifecycleOutcome = 'succeeded' | 'failed' | 'refused' | 'rolled-back';
72
+ /** Who approved an operator action. */
73
+ export type ApprovingPrincipal =
74
+ /** The interactive operator at the console. */
75
+ {
76
+ readonly kind: 'operator';
77
+ readonly identity: string;
78
+ }
79
+ /** A management grant presented by an external client. */
80
+ | {
81
+ readonly kind: 'client-grant';
82
+ readonly grantId: string;
83
+ }
84
+ /** An automated policy, named so it can be audited. */
85
+ | {
86
+ readonly kind: 'policy';
87
+ readonly policyId: string;
88
+ };
89
+ /** One operator action, recorded apart from execution metadata. */
90
+ export interface LifecycleRecord {
91
+ /** What was attempted. */
92
+ readonly operation: LifecycleOperation;
93
+ /** The extension it concerned. */
94
+ readonly extensionId: string;
95
+ /** The version, when the operation concerned one. */
96
+ readonly version: string | null;
97
+ /** The content digest, when known. */
98
+ readonly contentDigest: string | null;
99
+ /** The profile, for the profile-scoped operations. */
100
+ readonly profile: string | null;
101
+ /** What happened. */
102
+ readonly outcome: LifecycleOutcome;
103
+ /** Why it did not succeed, when it did not. */
104
+ readonly reason: string | null;
105
+ /** Who approved it. Never inferred from the request. */
106
+ readonly approvedBy: ApprovingPrincipal;
107
+ /** When it happened, ISO 8601. */
108
+ readonly at: string;
109
+ }
110
+ /** Raised when a value cannot be admitted to a receipt. */
111
+ export declare class ReceiptRedactionError extends Error {
112
+ readonly name = "ReceiptRedactionError";
113
+ }
114
+ /**
115
+ * Reduce a caller-supplied path to something safe to record.
116
+ *
117
+ * EXT-35 forbids unrestricted file paths in default receipts, and the reason is
118
+ * that a path discloses the operator's layout: directory names, a username, a
119
+ * project name. The basename is enough to correlate a receipt with a call, and a
120
+ * short digest makes two references to the same path comparable without revealing
121
+ * where it is.
122
+ * @param path - the caller-supplied path.
123
+ * @returns the basename plus a short digest, or null.
124
+ */
125
+ export declare function redactPath(path: string | undefined): string | null;
126
+ /**
127
+ * Redact anything token-shaped from a free-text note.
128
+ *
129
+ * Notes are authored by the host, but a note that embedded a value read from the
130
+ * environment would leak it. This is a guard against that, not a substitute for
131
+ * not putting secrets in notes.
132
+ * @param text - the note.
133
+ * @returns the note with credential-shaped spans removed.
134
+ */
135
+ export declare function redactText(text: string): string;
136
+ /**
137
+ * Build the invocation receipt for one call.
138
+ *
139
+ * Takes only host-resolved inputs. There is no parameter for a request body, a
140
+ * token or a skill text, so those cannot reach a receipt through this function at
141
+ * all — which is a stronger guarantee than remembering to omit them.
142
+ * @param input - the resolved provenance.
143
+ * @returns the receipt.
144
+ */
145
+ export declare function buildInvocationReceipt(input: {
146
+ readonly receiptId: string;
147
+ readonly contract: string;
148
+ readonly contractVersion: string;
149
+ readonly implementationId: string;
150
+ readonly extension: string;
151
+ readonly extensionVersion: string;
152
+ readonly extensionContentDigest?: string | null;
153
+ readonly dependencyLockDigest?: string | null;
154
+ readonly modelIdentity: string;
155
+ readonly recipeVersion?: string | null;
156
+ readonly activationGeneration: number;
157
+ readonly executionLocation: InvocationReceipt['executionLocation'];
158
+ readonly warnings?: readonly string[];
159
+ }): InvocationReceipt;
160
+ /**
161
+ * Record one operator action, separately from execution metadata.
162
+ * @param input - the action, its outcome and its approving principal.
163
+ * @returns the lifecycle record.
164
+ */
165
+ export declare function buildLifecycleRecord(input: {
166
+ readonly operation: LifecycleOperation;
167
+ readonly extensionId: string;
168
+ readonly version?: string | null;
169
+ readonly contentDigest?: string | null;
170
+ readonly profile?: string | null;
171
+ readonly outcome: LifecycleOutcome;
172
+ readonly reason?: string | null;
173
+ readonly approvedBy: ApprovingPrincipal;
174
+ readonly at?: string;
175
+ }): LifecycleRecord;
176
+ /**
177
+ * The bounded-logging policy an extension's diagnostics run under (EXT-35).
178
+ *
179
+ * Extension logging is bounded and host-policy controlled, and extension-declared
180
+ * telemetry or network behaviour must be disclosed rather than assumed. This
181
+ * applies the policy; it does not infer one.
182
+ */
183
+ export interface ExtensionLogPolicy {
184
+ /** Maximum records retained per extension. */
185
+ readonly maxRecords: number;
186
+ /** Levels admitted. A disabled level is dropped, not rewritten. */
187
+ readonly levels: readonly ('debug' | 'info' | 'warn' | 'error')[];
188
+ /** Whether the extension declared telemetry, which the operator must approve. */
189
+ readonly telemetryApproved: boolean;
190
+ /** Origins the extension declared, which must all be approved. */
191
+ readonly approvedOrigins: readonly string[];
192
+ }
193
+ /** The default policy: quiet, metadata-only, and telemetry off unless approved. */
194
+ export declare const DEFAULT_LOG_POLICY: ExtensionLogPolicy;
195
+ /**
196
+ * Apply a logging policy to one extension record.
197
+ *
198
+ * A dropped record is dropped rather than summarised, because a summarised
199
+ * diagnostic is a diagnostic an operator will misread.
200
+ * @param policy - the policy in force.
201
+ * @param record - the proposed record.
202
+ * @returns whether to retain it.
203
+ */
204
+ export declare function admitsLogRecord(policy: ExtensionLogPolicy, record: {
205
+ readonly level: 'debug' | 'info' | 'warn' | 'error';
206
+ readonly declaredOrigin?: string;
207
+ }): boolean;
208
+ //# sourceMappingURL=receipt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receipt.d.ts","sourceRoot":"","sources":["../src/receipt.ts"],"names":[],"mappings":"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"}
package/lib/receipt.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Invocation receipts and lifecycle records (EXT-35).
3
+ *
4
+ * EXT-35 asks for two different things, recorded SEPARATELY, and the separation is
5
+ * the point:
6
+ *
7
+ * - An **invocation receipt** says what ran: the extension, its version and content
8
+ * digest, the model/recipe identity, and the activation generation. It travels
9
+ * with the result.
10
+ * - A **lifecycle record** says who changed the installation: install, activate,
11
+ * deactivate, update and uninstall outcomes together with the approving
12
+ * principal. It does not travel with a result, because it describes an operator
13
+ * action rather than a call.
14
+ *
15
+ * Mixing them would mean a caller reading a receipt learns about operator actions
16
+ * it has no business seeing, and an audit trail becomes indistinguishable from
17
+ * execution metadata.
18
+ *
19
+ * The metadata-only rule is enforced by CONSTRUCTION rather than by remembering to
20
+ * be careful. Nothing here accepts a request body, a token, or a full skill text,
21
+ * and a path is reduced before it can enter a receipt at all — so a future caller
22
+ * cannot leak one by passing it in.
23
+ *
24
+ * @module @try-works/dsh-model-as-tool/receipt
25
+ */
26
+ import { createHash } from 'node:crypto';
27
+ import { basename } from 'node:path';
28
+ /** Raised when a value cannot be admitted to a receipt. */
29
+ export class ReceiptRedactionError extends Error {
30
+ name = 'ReceiptRedactionError';
31
+ }
32
+ /**
33
+ * Reduce a caller-supplied path to something safe to record.
34
+ *
35
+ * EXT-35 forbids unrestricted file paths in default receipts, and the reason is
36
+ * that a path discloses the operator's layout: directory names, a username, a
37
+ * project name. The basename is enough to correlate a receipt with a call, and a
38
+ * short digest makes two references to the same path comparable without revealing
39
+ * where it is.
40
+ * @param path - the caller-supplied path.
41
+ * @returns the basename plus a short digest, or null.
42
+ */
43
+ export function redactPath(path) {
44
+ if (typeof path !== 'string' || path.length === 0)
45
+ return null;
46
+ const digest = createHash('sha256').update(path).digest('hex').slice(0, 8);
47
+ // `basename` on either separator: a receipt must not leak a Windows path shape
48
+ // merely because the host runs on Windows.
49
+ const leaf = basename(path.replace(/\\/g, '/'));
50
+ return `${leaf}#${digest}`;
51
+ }
52
+ /**
53
+ * Redact anything token-shaped from a free-text note.
54
+ *
55
+ * Notes are authored by the host, but a note that embedded a value read from the
56
+ * environment would leak it. This is a guard against that, not a substitute for
57
+ * not putting secrets in notes.
58
+ * @param text - the note.
59
+ * @returns the note with credential-shaped spans removed.
60
+ */
61
+ export function redactText(text) {
62
+ return text
63
+ .replace(/\b(?:npm_|ghp_|gho_|ghs_|sk-|xox[baprs]-)[A-Za-z0-9_-]{8,}/g, '<redacted>')
64
+ .replace(/\bBearer\s+[A-Za-z0-9._-]{8,}/gi, 'Bearer <redacted>')
65
+ .replace(/\b[A-Fa-f0-9]{40,}\b/g, '<redacted>')
66
+ .replace(/[A-Za-z]:\\[^\s"']*/g, '<path>')
67
+ .replace(/(?<![\w.])\/(?:[\w.-]+\/)+[\w.-]+/g, '<path>');
68
+ }
69
+ /**
70
+ * Build the invocation receipt for one call.
71
+ *
72
+ * Takes only host-resolved inputs. There is no parameter for a request body, a
73
+ * token or a skill text, so those cannot reach a receipt through this function at
74
+ * all — which is a stronger guarantee than remembering to omit them.
75
+ * @param input - the resolved provenance.
76
+ * @returns the receipt.
77
+ */
78
+ export function buildInvocationReceipt(input) {
79
+ if (input.receiptId.length === 0) {
80
+ throw new ReceiptRedactionError('a receipt needs an identity; an unidentifiable receipt is not an audit trail');
81
+ }
82
+ return {
83
+ receiptId: input.receiptId,
84
+ contract: input.contract,
85
+ contractVersion: input.contractVersion,
86
+ implementationId: input.implementationId,
87
+ extension: input.extension,
88
+ extensionVersion: input.extensionVersion,
89
+ extensionContentDigest: input.extensionContentDigest ?? null,
90
+ dependencyLockDigest: input.dependencyLockDigest ?? null,
91
+ modelIdentity: input.modelIdentity,
92
+ recipeVersion: input.recipeVersion ?? null,
93
+ activationGeneration: input.activationGeneration,
94
+ executionLocation: input.executionLocation,
95
+ // Every note passes through the redactor, so a host-authored note that happened
96
+ // to embed a token or a path cannot leak one.
97
+ warnings: (input.warnings ?? []).map(redactText),
98
+ };
99
+ }
100
+ /**
101
+ * Record one operator action, separately from execution metadata.
102
+ * @param input - the action, its outcome and its approving principal.
103
+ * @returns the lifecycle record.
104
+ */
105
+ export function buildLifecycleRecord(input) {
106
+ if (input.approvedBy.kind === 'operator' && input.approvedBy.identity.length === 0) {
107
+ // An approval with no principal cannot be audited, which defeats the record.
108
+ throw new ReceiptRedactionError('an operator approval needs an identity');
109
+ }
110
+ return {
111
+ operation: input.operation,
112
+ extensionId: input.extensionId,
113
+ version: input.version ?? null,
114
+ contentDigest: input.contentDigest ?? null,
115
+ profile: input.profile ?? null,
116
+ outcome: input.outcome,
117
+ // A failed operation with no reason is the record an operator cannot act on.
118
+ reason: input.reason === undefined || input.reason === null ? null : redactText(input.reason),
119
+ approvedBy: input.approvedBy,
120
+ at: input.at ?? new Date().toISOString(),
121
+ };
122
+ }
123
+ /** The default policy: quiet, metadata-only, and telemetry off unless approved. */
124
+ export const DEFAULT_LOG_POLICY = {
125
+ maxRecords: 500,
126
+ levels: ['warn', 'error'],
127
+ telemetryApproved: false,
128
+ approvedOrigins: [],
129
+ };
130
+ /**
131
+ * Apply a logging policy to one extension record.
132
+ *
133
+ * A dropped record is dropped rather than summarised, because a summarised
134
+ * diagnostic is a diagnostic an operator will misread.
135
+ * @param policy - the policy in force.
136
+ * @param record - the proposed record.
137
+ * @returns whether to retain it.
138
+ */
139
+ export function admitsLogRecord(policy, record) {
140
+ if (!policy.levels.includes(record.level))
141
+ return false;
142
+ if (record.declaredOrigin !== undefined) {
143
+ // An undeclared network origin is refused rather than recorded and forgotten:
144
+ // EXT-35 requires the behaviour to be disclosed and approved, not logged.
145
+ if (!policy.approvedOrigins.includes(record.declaredOrigin))
146
+ return false;
147
+ }
148
+ return true;
149
+ }
150
+ //# sourceMappingURL=receipt.js.map
@@ -40,6 +40,19 @@ export declare function buildParameters(entry: ModelCatalogEntry): ParameterSche
40
40
  * @param value - the canonical value the adapter returned.
41
41
  * @returns the model-facing content blocks.
42
42
  */
43
+ /**
44
+ * Render a canonical value as model-facing content blocks.
45
+ *
46
+ * Keyed on the CONTRACT, never on the model id. A model-specific presentation
47
+ * branch is a model-specific postprocessing recipe living in the host, which
48
+ * EXT-01 forbids — and it would also mean a second implementation of the same
49
+ * contract rendered differently, contradicting the point of having a contract.
50
+ * Anything unrecognised falls through to the canonical JSON, so a new contract
51
+ * degrades to readable data rather than to nothing.
52
+ * @param entry - the catalog entry, for its contract binding.
53
+ * @param value - the canonical value the adapter returned.
54
+ * @returns the model-facing content blocks.
55
+ */
43
56
  export declare function renderResult(entry: ModelCatalogEntry, value: Record<string, unknown>): {
44
57
  type: 'text';
45
58
  text: string;
@@ -1 +1 @@
1
- {"version":3,"file":"registrar.d.ts","sourceRoot":"","sources":["../src/registrar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAA0C,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AACzG,OAAO,KAAK,EAAE,iBAAiB,EAAkB,MAAM,YAAY,CAAA;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,4EAA4E;AAC5E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAC7B;AAwBD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,mBAAmB,CAY7E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,iBAAiB,EACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CA0BlC;AAcD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE;IAAE,KAAK,EAAE;QAAE,QAAQ,CAAC,UAAU,EAAE,OAAO,GAAG,MAAM,IAAI,CAAA;KAAE,CAAA;CAAE,EAC7D,KAAK,EAAE,iBAAiB,EACxB,OAAO,EAAE,YAAY,GACpB,mBAAmB,CA6BrB"}
1
+ {"version":3,"file":"registrar.d.ts","sourceRoot":"","sources":["../src/registrar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAA0C,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AACzG,OAAO,KAAK,EAAE,iBAAiB,EAAkB,MAAM,YAAY,CAAA;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,4EAA4E;AAC5E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAC7B;AAwBD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,mBAAmB,CAY7E;AAED;;;;;;;;;;GAUG;AACH;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,iBAAiB,EACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CA8BlC;AAcD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE;IAAE,KAAK,EAAE;QAAE,QAAQ,CAAC,UAAU,EAAE,OAAO,GAAG,MAAM,IAAI,CAAA;KAAE,CAAA;CAAE,EAC7D,KAAK,EAAE,iBAAiB,EACxB,OAAO,EAAE,YAAY,GACpB,mBAAmB,CA6BrB"}
package/lib/registrar.js CHANGED
@@ -62,22 +62,39 @@ export function buildParameters(entry) {
62
62
  * @param value - the canonical value the adapter returned.
63
63
  * @returns the model-facing content blocks.
64
64
  */
65
+ /**
66
+ * Render a canonical value as model-facing content blocks.
67
+ *
68
+ * Keyed on the CONTRACT, never on the model id. A model-specific presentation
69
+ * branch is a model-specific postprocessing recipe living in the host, which
70
+ * EXT-01 forbids — and it would also mean a second implementation of the same
71
+ * contract rendered differently, contradicting the point of having a contract.
72
+ * Anything unrecognised falls through to the canonical JSON, so a new contract
73
+ * degrades to readable data rather than to nothing.
74
+ * @param entry - the catalog entry, for its contract binding.
75
+ * @param value - the canonical value the adapter returned.
76
+ * @returns the model-facing content blocks.
77
+ */
65
78
  export function renderResult(entry, value) {
66
- if (entry.id === 'tongue' && typeof value.language === 'string') {
79
+ if (entry.contract === 'text.language_detect' && typeof value.language === 'string') {
67
80
  const candidates = Array.isArray(value.candidates) ? value.candidates : [];
68
81
  const detail = candidates
69
82
  .filter((candidate) => typeof candidate === 'object' && candidate !== null
70
83
  && typeof candidate.language === 'string')
71
84
  .map(candidate => `${candidate.language} ${(candidate.probability * 100).toFixed(1)}%`)
72
85
  .join(', ');
73
- const tooClose = value.isTooCloseToCall === true ? ' (too close to call)' : '';
86
+ // Both spellings are accepted: an implementation reports its own vocabulary and
87
+ // the host translates, so this reads whichever one reached here.
88
+ const tooClose = value.tooCloseToCall === true || value.isTooCloseToCall === true
89
+ ? ' (too close to call)'
90
+ : '';
74
91
  return [{
75
92
  type: 'text',
76
93
  text: `Detected language: ${value.language}${tooClose}`
77
94
  + (detail.length > 0 ? `\nCandidates: ${detail}` : ''),
78
95
  }];
79
96
  }
80
- if (entry.id === 'voz' && typeof value.text === 'string') {
97
+ if (entry.contract === 'stt.transcribe' && typeof value.text === 'string') {
81
98
  const wordCount = typeof value.wordCount === 'number' ? value.wordCount : 0;
82
99
  return [{
83
100
  type: 'text',