@cleocode/lafs-protocol 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +0 -0
  3. package/dist/examples/discovery-server.d.ts +8 -0
  4. package/dist/examples/discovery-server.js +216 -0
  5. package/dist/examples/mcp-lafs-client.d.ts +10 -0
  6. package/dist/examples/mcp-lafs-client.js +427 -0
  7. package/dist/examples/mcp-lafs-server.d.ts +10 -0
  8. package/dist/examples/mcp-lafs-server.js +358 -0
  9. package/dist/schemas/v1/envelope.schema.json +0 -0
  10. package/dist/schemas/v1/error-registry.json +0 -0
  11. package/dist/src/budgetEnforcement.d.ts +84 -0
  12. package/dist/src/budgetEnforcement.js +328 -0
  13. package/dist/src/cli.d.ts +0 -0
  14. package/dist/src/cli.js +0 -0
  15. package/dist/src/conformance.d.ts +0 -0
  16. package/dist/src/conformance.js +0 -0
  17. package/dist/src/discovery.d.ts +127 -0
  18. package/dist/src/discovery.js +304 -0
  19. package/dist/src/errorRegistry.d.ts +0 -0
  20. package/dist/src/errorRegistry.js +0 -0
  21. package/dist/src/flagSemantics.d.ts +0 -0
  22. package/dist/src/flagSemantics.js +0 -0
  23. package/dist/src/index.d.ts +4 -0
  24. package/dist/src/index.js +4 -0
  25. package/dist/src/mcpAdapter.d.ts +28 -0
  26. package/dist/src/mcpAdapter.js +281 -0
  27. package/dist/src/tokenEstimator.d.ts +87 -0
  28. package/dist/src/tokenEstimator.js +238 -0
  29. package/dist/src/types.d.ts +25 -0
  30. package/dist/src/types.js +0 -0
  31. package/dist/src/validateEnvelope.d.ts +0 -0
  32. package/dist/src/validateEnvelope.js +0 -0
  33. package/lafs.md +164 -0
  34. package/package.json +8 -3
  35. package/schemas/v1/context-ledger.schema.json +0 -0
  36. package/schemas/v1/discovery.schema.json +132 -0
  37. package/schemas/v1/envelope.schema.json +0 -0
  38. package/schemas/v1/error-registry.json +0 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * LAFS Token Estimator
3
+ *
4
+ * Provides character-based token estimation for LAFS envelopes and JSON payloads.
5
+ * Uses the approximation: 1 token ≈ 4 characters.
6
+ * Properly handles nested objects, arrays, Unicode graphemes, and circular references.
7
+ */
8
+ export interface TokenEstimatorOptions {
9
+ /**
10
+ * Characters per token ratio (default: 4)
11
+ */
12
+ charsPerToken?: number;
13
+ /**
14
+ * Maximum depth to traverse for circular reference detection (default: 100)
15
+ */
16
+ maxDepth?: number;
17
+ /**
18
+ * Maximum string length to process for Unicode grapheme counting (default: 100000)
19
+ */
20
+ maxStringLength?: number;
21
+ }
22
+ /**
23
+ * TokenEstimator provides character-based token counting for JSON payloads.
24
+ *
25
+ * Algorithm:
26
+ * 1. Serialize value to JSON (handling circular refs)
27
+ * 2. Count Unicode graphemes (not bytes)
28
+ * 3. Divide by charsPerToken ratio (default 4)
29
+ * 4. Add overhead for structural characters
30
+ */
31
+ export declare class TokenEstimator {
32
+ private options;
33
+ constructor(options?: TokenEstimatorOptions);
34
+ /**
35
+ * Estimate tokens for any JavaScript value.
36
+ * Handles circular references, nested objects, arrays, and Unicode.
37
+ *
38
+ * @param value - Any value to estimate
39
+ * @returns Estimated token count
40
+ */
41
+ estimate(value: unknown): number;
42
+ /**
43
+ * Estimate tokens from a JSON string.
44
+ * More efficient if you already have the JSON string.
45
+ *
46
+ * @param json - JSON string to estimate
47
+ * @returns Estimated token count
48
+ */
49
+ estimateJSON(json: string): number;
50
+ /**
51
+ * Internal recursive estimation with circular reference tracking.
52
+ */
53
+ private estimateWithTracking;
54
+ /**
55
+ * Estimate tokens for an array.
56
+ */
57
+ private estimateArray;
58
+ /**
59
+ * Estimate tokens for a plain object.
60
+ */
61
+ private estimateObject;
62
+ /**
63
+ * Check if a value can be safely serialized (no circular refs).
64
+ */
65
+ canSerialize(value: unknown): boolean;
66
+ /**
67
+ * Serialize value to JSON with circular reference handling.
68
+ * Circular refs are replaced with "[Circular]".
69
+ */
70
+ safeStringify(value: unknown): string;
71
+ /**
72
+ * Create a safe copy of a value with circular refs removed.
73
+ */
74
+ safeCopy<T>(value: T): T;
75
+ }
76
+ /**
77
+ * Global token estimator instance with default settings.
78
+ */
79
+ export declare const defaultEstimator: TokenEstimator;
80
+ /**
81
+ * Convenience function to estimate tokens for a value.
82
+ */
83
+ export declare function estimateTokens(value: unknown, options?: TokenEstimatorOptions): number;
84
+ /**
85
+ * Convenience function to estimate tokens from a JSON string.
86
+ */
87
+ export declare function estimateTokensJSON(json: string, options?: TokenEstimatorOptions): number;
@@ -0,0 +1,238 @@
1
+ /**
2
+ * LAFS Token Estimator
3
+ *
4
+ * Provides character-based token estimation for LAFS envelopes and JSON payloads.
5
+ * Uses the approximation: 1 token ≈ 4 characters.
6
+ * Properly handles nested objects, arrays, Unicode graphemes, and circular references.
7
+ */
8
+ /**
9
+ * Counts Unicode graphemes in a string using Intl.Segmenter when available.
10
+ * Falls back to character counting for environments without Intl.Segmenter.
11
+ */
12
+ function countGraphemes(str) {
13
+ // Use Intl.Segmenter for proper grapheme counting (Node.js 16+, modern browsers)
14
+ if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {
15
+ // @ts-ignore - Intl.Segmenter may not be in all TypeScript lib versions
16
+ const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
17
+ // @ts-ignore
18
+ return Array.from(segmenter.segment(str)).length;
19
+ }
20
+ // Fallback: count code points using spread operator (handles surrogate pairs)
21
+ return [...str].length;
22
+ }
23
+ /**
24
+ * Default options for token estimation
25
+ */
26
+ const DEFAULT_OPTIONS = {
27
+ charsPerToken: 4,
28
+ maxDepth: 100,
29
+ maxStringLength: 100000,
30
+ };
31
+ /**
32
+ * TokenEstimator provides character-based token counting for JSON payloads.
33
+ *
34
+ * Algorithm:
35
+ * 1. Serialize value to JSON (handling circular refs)
36
+ * 2. Count Unicode graphemes (not bytes)
37
+ * 3. Divide by charsPerToken ratio (default 4)
38
+ * 4. Add overhead for structural characters
39
+ */
40
+ export class TokenEstimator {
41
+ options;
42
+ constructor(options = {}) {
43
+ this.options = { ...DEFAULT_OPTIONS, ...options };
44
+ }
45
+ /**
46
+ * Estimate tokens for any JavaScript value.
47
+ * Handles circular references, nested objects, arrays, and Unicode.
48
+ *
49
+ * @param value - Any value to estimate
50
+ * @returns Estimated token count
51
+ */
52
+ estimate(value) {
53
+ return this.estimateWithTracking(value, new WeakSet(), 0);
54
+ }
55
+ /**
56
+ * Estimate tokens from a JSON string.
57
+ * More efficient if you already have the JSON string.
58
+ *
59
+ * @param json - JSON string to estimate
60
+ * @returns Estimated token count
61
+ */
62
+ estimateJSON(json) {
63
+ // Count graphemes in the JSON string
64
+ const graphemes = countGraphemes(json);
65
+ // Add overhead for JSON structure (brackets, quotes, colons, etc.)
66
+ const structuralOverhead = Math.ceil(graphemes * 0.1);
67
+ return Math.ceil((graphemes + structuralOverhead) / this.options.charsPerToken);
68
+ }
69
+ /**
70
+ * Internal recursive estimation with circular reference tracking.
71
+ */
72
+ estimateWithTracking(value, seen, depth) {
73
+ // Prevent infinite recursion
74
+ if (depth > this.options.maxDepth) {
75
+ return 1; // Minimal cost for max depth exceeded
76
+ }
77
+ // Handle null
78
+ if (value === null) {
79
+ return 1; // "null" = 4 chars / 4 = 1 token
80
+ }
81
+ // Handle undefined
82
+ if (value === undefined) {
83
+ return 1;
84
+ }
85
+ // Handle primitives
86
+ const type = typeof value;
87
+ if (type === 'boolean') {
88
+ return value ? 1 : 1; // "true" or "false" ≈ 1 token
89
+ }
90
+ if (type === 'number') {
91
+ const str = String(value);
92
+ return Math.ceil(countGraphemes(str) / this.options.charsPerToken);
93
+ }
94
+ if (type === 'string') {
95
+ const str = value;
96
+ // Limit string length to prevent performance issues
97
+ const truncated = str.length > this.options.maxStringLength
98
+ ? str.slice(0, this.options.maxStringLength) + '…'
99
+ : str;
100
+ const graphemes = countGraphemes(truncated);
101
+ // Add 2 for quotes
102
+ return Math.ceil((graphemes + 2) / this.options.charsPerToken);
103
+ }
104
+ // Handle objects and arrays
105
+ if (type === 'object') {
106
+ const obj = value;
107
+ // Check for circular reference
108
+ if (seen.has(obj)) {
109
+ return 1; // Minimal cost for circular ref placeholder
110
+ }
111
+ seen.add(obj);
112
+ try {
113
+ if (Array.isArray(obj)) {
114
+ return this.estimateArray(obj, seen, depth);
115
+ }
116
+ return this.estimateObject(obj, seen, depth);
117
+ }
118
+ finally {
119
+ seen.delete(obj);
120
+ }
121
+ }
122
+ // Handle symbols, functions, etc.
123
+ return 1;
124
+ }
125
+ /**
126
+ * Estimate tokens for an array.
127
+ */
128
+ estimateArray(arr, seen, depth) {
129
+ let tokens = 1; // Opening bracket [ (already counted as structural)
130
+ for (let i = 0; i < arr.length; i++) {
131
+ tokens += this.estimateWithTracking(arr[i], seen, depth + 1);
132
+ // Add comma separator (except for last element)
133
+ if (i < arr.length - 1) {
134
+ tokens += 1; // comma + space ≈ 2 chars / 4 = 0.5, round up to 1
135
+ }
136
+ }
137
+ tokens += 1; // Closing bracket ]
138
+ return tokens;
139
+ }
140
+ /**
141
+ * Estimate tokens for a plain object.
142
+ */
143
+ estimateObject(obj, seen, depth) {
144
+ let tokens = 1; // Opening brace {
145
+ const keys = Object.keys(obj);
146
+ for (let i = 0; i < keys.length; i++) {
147
+ const key = keys[i];
148
+ const value = obj[key];
149
+ // Estimate key (with quotes)
150
+ tokens += Math.ceil((countGraphemes(key) + 2) / this.options.charsPerToken);
151
+ // Colon separator
152
+ tokens += 1; // " : " ≈ 3 chars / 4 = 0.75, round up to 1
153
+ // Estimate value
154
+ tokens += this.estimateWithTracking(value, seen, depth + 1);
155
+ // Comma separator (except for last property)
156
+ if (i < keys.length - 1) {
157
+ tokens += 1; // comma + space ≈ 2 chars / 4 = 0.5, round up to 1
158
+ }
159
+ }
160
+ tokens += 1; // Closing brace }
161
+ return tokens;
162
+ }
163
+ /**
164
+ * Check if a value can be safely serialized (no circular refs).
165
+ */
166
+ canSerialize(value) {
167
+ try {
168
+ JSON.stringify(value);
169
+ return true;
170
+ }
171
+ catch {
172
+ return false;
173
+ }
174
+ }
175
+ /**
176
+ * Serialize value to JSON with circular reference handling.
177
+ * Circular refs are replaced with "[Circular]".
178
+ */
179
+ safeStringify(value) {
180
+ const seen = new WeakSet();
181
+ return JSON.stringify(value, (key, val) => {
182
+ if (typeof val === 'object' && val !== null) {
183
+ if (seen.has(val)) {
184
+ return '[Circular]';
185
+ }
186
+ seen.add(val);
187
+ }
188
+ return val;
189
+ });
190
+ }
191
+ /**
192
+ * Create a safe copy of a value with circular refs removed.
193
+ */
194
+ safeCopy(value) {
195
+ const seen = new WeakSet();
196
+ function clone(val) {
197
+ if (val === null || typeof val !== 'object') {
198
+ return val;
199
+ }
200
+ if (seen.has(val)) {
201
+ return '[Circular]';
202
+ }
203
+ seen.add(val);
204
+ try {
205
+ if (Array.isArray(val)) {
206
+ return val.map(clone);
207
+ }
208
+ const result = {};
209
+ for (const [k, v] of Object.entries(val)) {
210
+ result[k] = clone(v);
211
+ }
212
+ return result;
213
+ }
214
+ finally {
215
+ seen.delete(val);
216
+ }
217
+ }
218
+ return clone(value);
219
+ }
220
+ }
221
+ /**
222
+ * Global token estimator instance with default settings.
223
+ */
224
+ export const defaultEstimator = new TokenEstimator();
225
+ /**
226
+ * Convenience function to estimate tokens for a value.
227
+ */
228
+ export function estimateTokens(value, options) {
229
+ const estimator = options ? new TokenEstimator(options) : defaultEstimator;
230
+ return estimator.estimate(value);
231
+ }
232
+ /**
233
+ * Convenience function to estimate tokens from a JSON string.
234
+ */
235
+ export function estimateTokensJSON(json, options) {
236
+ const estimator = options ? new TokenEstimator(options) : defaultEstimator;
237
+ return estimator.estimateJSON(json);
238
+ }
@@ -85,3 +85,28 @@ export interface ConformanceReport {
85
85
  detail?: string;
86
86
  }>;
87
87
  }
88
+ export type BudgetEnforcementOptions = {
89
+ truncateOnExceed?: boolean;
90
+ onBudgetExceeded?: (estimated: number, budget: number) => void;
91
+ };
92
+ export interface TokenEstimate {
93
+ estimated: number;
94
+ truncated?: boolean;
95
+ originalEstimate?: number;
96
+ }
97
+ export interface LAFSMetaWithBudget extends LAFSMeta {
98
+ _tokenEstimate?: TokenEstimate;
99
+ }
100
+ export interface LAFSEnvelopeWithBudget extends Omit<LAFSEnvelope, '_meta'> {
101
+ _meta: LAFSMetaWithBudget;
102
+ }
103
+ export type MiddlewareFunction = (envelope: LAFSEnvelope) => LAFSEnvelope | Promise<LAFSEnvelope>;
104
+ export type NextFunction = () => LAFSEnvelope | Promise<LAFSEnvelope>;
105
+ export type BudgetMiddleware = (envelope: LAFSEnvelope, next: NextFunction) => Promise<LAFSEnvelope> | LAFSEnvelope;
106
+ export interface BudgetEnforcementResult {
107
+ envelope: LAFSEnvelope;
108
+ withinBudget: boolean;
109
+ estimatedTokens: number;
110
+ budget: number;
111
+ truncated: boolean;
112
+ }
package/dist/src/types.js CHANGED
File without changes
File without changes
File without changes
package/lafs.md CHANGED
@@ -176,6 +176,56 @@ Rules:
176
176
  - Decisions affecting output MUST be represented in ledger state.
177
177
  - Missing required context for a mutating step MUST fail with structured error.
178
178
 
179
+ ### 8.1 Context Retrieval
180
+
181
+ Agents MAY retrieve context ledger state via `GET /_lafs/context/{ledgerId}` with projection modes.
182
+
183
+ #### 8.1.1 Projection Modes
184
+
185
+ **Full Mode (`mode=full`):**
186
+ Returns complete ledger including all entries.
187
+ - Use for: Initial loads, recovery scenarios
188
+ - Supports: Offset-based pagination
189
+ - Response includes: All ledger fields
190
+
191
+ **Delta Mode (`mode=delta&sinceVersion=N`):**
192
+ Returns only entries added since version N.
193
+ - Use for: Active workflows (efficient sync)
194
+ - Response includes:
195
+ ```json
196
+ {
197
+ "ledgerId": "ctx_abc123",
198
+ "mode": "delta",
199
+ "fromVersion": 10,
200
+ "toVersion": 15,
201
+ "entries": [/* new entries only */],
202
+ "removedConstraints": [/* constraints no longer active */],
203
+ "checksum": "sha256:..."
204
+ }
205
+ ```
206
+
207
+ **Summary Mode (`mode=summary`):**
208
+ Returns checksum and version for validation.
209
+ - Use for: Quick sync validation
210
+ - Response includes only: `ledgerId`, `version`, `checksum`, `entryCount`
211
+
212
+ #### 8.1.2 Query Parameters
213
+
214
+ | Parameter | Type | Description |
215
+ |-----------|------|-------------|
216
+ | `mode` | enum | `full`, `delta`, `summary` |
217
+ | `sinceVersion` | integer | For delta mode: return entries after this version |
218
+ | `filterByOperation` | string[] | Filter entries by operation name(s) |
219
+ | `limit` | integer | Max entries (1-1000, default 100) |
220
+ | `includeChecksum` | boolean | Include integrity checksum (default true) |
221
+
222
+ #### 8.1.3 Agent Guidance
223
+
224
+ - **Initial load**: Use `mode=full` once
225
+ - **Active workflows**: Use `mode=delta` with last known version
226
+ - **Validation**: Use `mode=summary` to verify sync state
227
+ - **Default recommendation**: `delta` mode for agent-optimal behavior
228
+
179
229
  ---
180
230
 
181
231
  ## 9. MVI and Progressive Disclosure
@@ -212,6 +262,120 @@ Clients MAY request expanded/nested data via the `_expand` request parameter.
212
262
  - Pagination mode (offset or cursor) MUST be documented.
213
263
  - Mixed pagination modes in one request MUST fail validation.
214
264
 
265
+ ### 9.5 Token Budget Signaling
266
+
267
+ Token budget signaling enables clients to declare resource constraints that servers MUST respect when generating responses. This mechanism prevents context window overflow in LLM-driven workflows.
268
+
269
+ #### 9.5.1 Budget Declaration (`_budget`)
270
+
271
+ Clients MAY declare resource constraints via the `_budget` request parameter:
272
+
273
+ ```json
274
+ {
275
+ "_budget": {
276
+ "maxTokens": 4000,
277
+ "maxBytes": 32768,
278
+ "maxItems": 100
279
+ }
280
+ }
281
+ ```
282
+
283
+ **Fields:**
284
+ - `maxTokens` (integer) - Maximum approximate tokens
285
+ - `maxBytes` (integer) - Maximum byte size
286
+ - `maxItems` (integer) - Maximum items in lists
287
+
288
+ **Constraints:**
289
+ - At least one field MUST be present
290
+ - All values MUST be positive integers
291
+ - Servers MAY reject budgets exceeding implementation limits
292
+
293
+ #### 9.5.2 Server Behavior
294
+
295
+ Servers MUST:
296
+ 1. Parse `_budget` from incoming requests
297
+ 2. Estimate/measure response size
298
+ 3. Return response within budget OR fail with `E_MVI_BUDGET_EXCEEDED`
299
+
300
+ Servers MAY truncate responses using:
301
+ - **Depth-first**: Remove deepest nested fields
302
+ - **Field priority**: Remove non-essential fields first
303
+ - **Hybrid**: Combine both strategies
304
+
305
+ When truncation occurs, servers MUST include:
306
+ ```json
307
+ {
308
+ "_meta": {
309
+ "warnings": [{
310
+ "code": "E_MVI_BUDGET_TRUNCATED",
311
+ "message": "Response truncated to fit token budget"
312
+ }],
313
+ "_tokenEstimate": {
314
+ "estimated": 2847,
315
+ "budget": 4000,
316
+ "method": "character_based"
317
+ }
318
+ }
319
+ }
320
+ ```
321
+
322
+ #### 9.5.3 Error Specification
323
+
324
+ **E_MVI_BUDGET_EXCEEDED:**
325
+ - **Category:** `VALIDATION`
326
+ - **Retryable:** `true`
327
+ - **Details:** `estimatedTokens`, `budget`, `excessTokens`, `constraint`
328
+
329
+ ```json
330
+ {
331
+ "error": {
332
+ "code": "E_MVI_BUDGET_EXCEEDED",
333
+ "message": "Response exceeds declared token budget",
334
+ "category": "VALIDATION",
335
+ "retryable": true,
336
+ "details": {
337
+ "estimatedTokens": 5234,
338
+ "budget": 4000,
339
+ "excessTokens": 1234,
340
+ "constraint": "maxTokens"
341
+ }
342
+ }
343
+ }
344
+ ```
345
+
346
+ #### 9.5.4 Token Estimation Algorithm (Normative)
347
+
348
+ Servers MUST implement this algorithm or equivalent (within +/- 10%):
349
+
350
+ ```
351
+ FUNCTION estimate_tokens(value, depth = 0):
352
+ IF depth > 20: RETURN INFINITY
353
+ IF value IS null: RETURN 1
354
+ IF value IS boolean: RETURN 1
355
+ IF value IS number: RETURN max(1, len(stringify(value)) / 4)
356
+ IF value IS string:
357
+ graphemes = count_grapheme_clusters(value)
358
+ RETURN max(1, graphemes / 4.0)
359
+ IF value IS array:
360
+ tokens = 2 // []
361
+ FOR item IN value:
362
+ tokens += estimate_tokens(item, depth + 1) + 1
363
+ RETURN tokens
364
+ IF value IS object:
365
+ tokens = 2 // {}
366
+ FOR key, val IN value:
367
+ tokens += estimate_tokens(key, depth + 1)
368
+ tokens += 2 // : and ,
369
+ tokens += estimate_tokens(val, depth + 1)
370
+ RETURN tokens
371
+ ```
372
+
373
+ **Requirements:**
374
+ - Count grapheme clusters (not bytes) for unicode
375
+ - Enforce max depth of 20
376
+ - Handle circular references
377
+ - Complete within 10ms for 100KB payloads
378
+
215
379
  ---
216
380
 
217
381
  ## 10. Strictness
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/lafs-protocol",
3
- "version": "0.5.0",
3
+ "version": "1.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "LLM-Agent-First Specification schemas and conformance tooling",
@@ -50,13 +50,18 @@
50
50
  ],
51
51
  "license": "MIT",
52
52
  "devDependencies": {
53
+ "@modelcontextprotocol/sdk": "^1.26.0",
54
+ "@types/express": "^5.0.6",
53
55
  "@types/node": "^24.3.0",
56
+ "@types/supertest": "^6.0.3",
57
+ "supertest": "^7.2.2",
54
58
  "tsx": "^4.20.5",
55
59
  "typescript": "^5.9.2",
56
60
  "vitest": "^2.1.9"
57
61
  },
58
62
  "dependencies": {
59
- "ajv": "^8.17.1",
60
- "ajv-formats": "^3.0.1"
63
+ "ajv": "^8.18.0",
64
+ "ajv-formats": "^3.0.1",
65
+ "express": "^5.2.1"
61
66
  }
62
67
  }
File without changes
@@ -0,0 +1,132 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://lafs.dev/schemas/v1/discovery.schema.json",
4
+ "title": "LAFS Discovery Document",
5
+ "description": "Schema for LAFS agent discovery documents served at /.well-known/lafs.json",
6
+ "type": "object",
7
+ "required": [
8
+ "$schema",
9
+ "lafs_version",
10
+ "service",
11
+ "capabilities",
12
+ "endpoints"
13
+ ],
14
+ "properties": {
15
+ "$schema": {
16
+ "type": "string",
17
+ "format": "uri",
18
+ "description": "URL of the schema this document conforms to"
19
+ },
20
+ "lafs_version": {
21
+ "type": "string",
22
+ "pattern": "^\\d+\\.\\d+\\.\\d+$",
23
+ "description": "LAFS protocol version (semantic versioning)"
24
+ },
25
+ "service": {
26
+ "type": "object",
27
+ "description": "Service identification and metadata",
28
+ "required": [
29
+ "name",
30
+ "version"
31
+ ],
32
+ "properties": {
33
+ "name": {
34
+ "type": "string",
35
+ "minLength": 1,
36
+ "maxLength": 100,
37
+ "description": "Unique service name (kebab-case recommended)"
38
+ },
39
+ "version": {
40
+ "type": "string",
41
+ "pattern": "^\\d+\\.\\d+\\.\\d+$",
42
+ "description": "Service version (semantic versioning)"
43
+ },
44
+ "description": {
45
+ "type": "string",
46
+ "maxLength": 500,
47
+ "description": "Human-readable service description"
48
+ }
49
+ },
50
+ "additionalProperties": false
51
+ },
52
+ "capabilities": {
53
+ "type": "array",
54
+ "description": "List of LAFS capabilities this service provides",
55
+ "minItems": 1,
56
+ "items": {
57
+ "type": "object",
58
+ "required": [
59
+ "name",
60
+ "version",
61
+ "operations"
62
+ ],
63
+ "properties": {
64
+ "name": {
65
+ "type": "string",
66
+ "minLength": 1,
67
+ "maxLength": 100,
68
+ "description": "Capability identifier (kebab-case recommended)"
69
+ },
70
+ "version": {
71
+ "type": "string",
72
+ "pattern": "^\\d+\\.\\d+\\.\\d+$",
73
+ "description": "Capability version (semantic versioning)"
74
+ },
75
+ "description": {
76
+ "type": "string",
77
+ "maxLength": 500,
78
+ "description": "Human-readable capability description"
79
+ },
80
+ "operations": {
81
+ "type": "array",
82
+ "description": "List of operations this capability supports",
83
+ "minItems": 1,
84
+ "items": {
85
+ "type": "string",
86
+ "minLength": 1,
87
+ "maxLength": 50
88
+ }
89
+ },
90
+ "optional": {
91
+ "type": "boolean",
92
+ "default": false,
93
+ "description": "Whether this capability is optional for clients"
94
+ }
95
+ },
96
+ "additionalProperties": false
97
+ }
98
+ },
99
+ "endpoints": {
100
+ "type": "object",
101
+ "description": "URL endpoints for LAFS operations",
102
+ "required": [
103
+ "envelope",
104
+ "discovery"
105
+ ],
106
+ "properties": {
107
+ "envelope": {
108
+ "type": "string",
109
+ "minLength": 1,
110
+ "description": "URL for envelope submission (POST)"
111
+ },
112
+ "context": {
113
+ "type": "string",
114
+ "minLength": 1,
115
+ "description": "URL for context ledger operations"
116
+ },
117
+ "discovery": {
118
+ "type": "string",
119
+ "minLength": 1,
120
+ "description": "URL of this discovery document"
121
+ }
122
+ },
123
+ "additionalProperties": false
124
+ },
125
+ "extensions": {
126
+ "type": "object",
127
+ "description": "Extension fields for vendor-specific metadata",
128
+ "additionalProperties": true
129
+ }
130
+ },
131
+ "additionalProperties": false
132
+ }