@crowdedkingdoms/crowdyjs 11.1.0 → 12.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 (81) hide show
  1. package/MIGRATION.md +107 -0
  2. package/README.md +203 -0
  3. package/dist/crowdy-agent/browser-dispatcher.d.ts +41 -0
  4. package/dist/crowdy-agent/browser-dispatcher.d.ts.map +1 -0
  5. package/dist/crowdy-agent/browser-dispatcher.js +264 -0
  6. package/dist/crowdy-agent/controller.d.ts +144 -0
  7. package/dist/crowdy-agent/controller.d.ts.map +1 -0
  8. package/dist/crowdy-agent/controller.js +1239 -0
  9. package/dist/crowdy-agent/errors.d.ts +37 -0
  10. package/dist/crowdy-agent/errors.d.ts.map +1 -0
  11. package/dist/crowdy-agent/errors.js +107 -0
  12. package/dist/crowdy-agent/graphql-transport.d.ts +154 -0
  13. package/dist/crowdy-agent/graphql-transport.d.ts.map +1 -0
  14. package/dist/crowdy-agent/graphql-transport.js +854 -0
  15. package/dist/crowdy-agent/index.d.ts +16 -0
  16. package/dist/crowdy-agent/index.d.ts.map +1 -0
  17. package/dist/crowdy-agent/index.js +15 -0
  18. package/dist/crowdy-agent/registry.d.ts +29 -0
  19. package/dist/crowdy-agent/registry.d.ts.map +1 -0
  20. package/dist/crowdy-agent/registry.js +281 -0
  21. package/dist/crowdy-agent/schema.d.ts +72 -0
  22. package/dist/crowdy-agent/schema.d.ts.map +1 -0
  23. package/dist/crowdy-agent/schema.js +467 -0
  24. package/dist/crowdy-agent/studio-tools.d.ts +15 -0
  25. package/dist/crowdy-agent/studio-tools.d.ts.map +1 -0
  26. package/dist/crowdy-agent/studio-tools.js +280 -0
  27. package/dist/crowdy-agent/tool-descriptors.d.ts +4 -0
  28. package/dist/crowdy-agent/tool-descriptors.d.ts.map +1 -0
  29. package/dist/crowdy-agent/tool-descriptors.js +1049 -0
  30. package/dist/crowdy-agent/transport.d.ts +152 -0
  31. package/dist/crowdy-agent/transport.d.ts.map +1 -0
  32. package/dist/crowdy-agent/transport.js +27 -0
  33. package/dist/crowdy-agent/types.d.ts +328 -0
  34. package/dist/crowdy-agent/types.d.ts.map +1 -0
  35. package/dist/crowdy-agent/types.js +37 -0
  36. package/dist/crowdy-client.d.ts +3 -0
  37. package/dist/crowdy-client.d.ts.map +1 -1
  38. package/dist/crowdy-client.js +6 -0
  39. package/dist/crowdy-studio/agent-dom-shell.d.ts +40 -0
  40. package/dist/crowdy-studio/agent-dom-shell.d.ts.map +1 -0
  41. package/dist/crowdy-studio/agent-dom-shell.js +354 -0
  42. package/dist/crowdy-studio/controller.d.ts +74 -4
  43. package/dist/crowdy-studio/controller.d.ts.map +1 -1
  44. package/dist/crowdy-studio/controller.js +450 -17
  45. package/dist/crowdy-studio/dom-shell.d.ts +4 -1
  46. package/dist/crowdy-studio/dom-shell.d.ts.map +1 -1
  47. package/dist/crowdy-studio/dom-shell.js +6 -1
  48. package/dist/crowdy-studio/index.d.ts +6 -3
  49. package/dist/crowdy-studio/index.d.ts.map +1 -1
  50. package/dist/crowdy-studio/index.js +3 -0
  51. package/dist/crowdy-studio/models.d.ts +67 -0
  52. package/dist/crowdy-studio/models.d.ts.map +1 -1
  53. package/dist/crowdy-studio/mount.d.ts +15 -0
  54. package/dist/crowdy-studio/mount.d.ts.map +1 -1
  55. package/dist/crowdy-studio/mount.js +129 -2
  56. package/dist/crowdy-studio/styles.d.ts +1 -1
  57. package/dist/crowdy-studio/styles.d.ts.map +1 -1
  58. package/dist/crowdy-studio/styles.js +2 -0
  59. package/dist/generated/graphql.d.ts +3753 -1
  60. package/dist/generated/graphql.d.ts.map +1 -1
  61. package/dist/generated/graphql.js +296 -0
  62. package/dist/index.d.ts +1 -1
  63. package/dist/index.js +1 -1
  64. package/dist/kit/npcs.d.ts.map +1 -1
  65. package/dist/kit/social.d.ts.map +1 -1
  66. package/dist/player-host/index.d.ts +5 -0
  67. package/dist/player-host/index.d.ts.map +1 -0
  68. package/dist/player-host/index.js +3 -0
  69. package/dist/player-host/lease-manager.d.ts +66 -0
  70. package/dist/player-host/lease-manager.d.ts.map +1 -0
  71. package/dist/player-host/lease-manager.js +428 -0
  72. package/dist/player-host/schemas.d.ts +9 -0
  73. package/dist/player-host/schemas.d.ts.map +1 -0
  74. package/dist/player-host/schemas.js +347 -0
  75. package/dist/player-host/tools.d.ts +13 -0
  76. package/dist/player-host/tools.d.ts.map +1 -0
  77. package/dist/player-host/tools.js +79 -0
  78. package/dist/player-host/types.d.ts +203 -0
  79. package/dist/player-host/types.d.ts.map +1 -0
  80. package/dist/player-host/types.js +1 -0
  81. package/package.json +12 -2
@@ -0,0 +1,1049 @@
1
+ import { deepFreeze, } from './schema.js';
2
+ import { GAME_COMMAND_RESULT_SCHEMA_V1, GAME_OBSERVATION_SCHEMA_V1, OBSERVE_REQUEST_SCHEMA_V1, PLAYER_HOST_CAPABILITIES_SCHEMA_V1, } from '../player-host/schemas.js';
3
+ const text = (maxLength, minLength = 0) => ({
4
+ type: 'string',
5
+ minLength,
6
+ maxLength,
7
+ });
8
+ const enumText = (values) => ({
9
+ type: 'string',
10
+ minLength: 1,
11
+ maxLength: Math.max(...values.map((value) => value.length)),
12
+ enum: values,
13
+ });
14
+ const decimal = (allowNegative = false) => ({
15
+ type: 'string',
16
+ minLength: 1,
17
+ maxLength: 40,
18
+ pattern: allowNegative ? '^(0|-[1-9][0-9]*|[1-9][0-9]*)$' : '^(0|[1-9][0-9]*)$',
19
+ });
20
+ const digest = () => ({
21
+ type: 'string',
22
+ minLength: 71,
23
+ maxLength: 71,
24
+ pattern: '^sha256:[0-9a-f]{64}$',
25
+ });
26
+ const dateTime = () => ({
27
+ type: 'string',
28
+ minLength: 20,
29
+ maxLength: 40,
30
+ format: 'date-time',
31
+ });
32
+ const integer = (minimum, maximum) => ({
33
+ type: 'integer',
34
+ minimum,
35
+ maximum,
36
+ });
37
+ const number = (minimum, maximum) => ({
38
+ type: 'number',
39
+ minimum,
40
+ maximum,
41
+ });
42
+ const boolean = () => ({ type: 'boolean' });
43
+ const array = (items, maxItems, minItems = 0, uniqueItems = false) => ({
44
+ type: 'array',
45
+ minItems,
46
+ maxItems,
47
+ uniqueItems,
48
+ items,
49
+ });
50
+ const object = (properties, required = Object.keys(properties)) => ({
51
+ type: 'object',
52
+ additionalProperties: false,
53
+ required,
54
+ maxProperties: Object.keys(properties).length,
55
+ properties,
56
+ });
57
+ const EMPTY = object({});
58
+ const OK = object({ ok: boolean() });
59
+ const TARGET = enumText(['SERVER', 'CLIENT']);
60
+ const PATH = {
61
+ ...text(256, 1),
62
+ pattern: '^(?!/)(?!.*(?:^|/)\\.\\.?(?:/|$))(?!.*[\\u0000-\\u001f\\u007f])[^\\\\]+$',
63
+ };
64
+ const PROJECT_REF = text(128, 1);
65
+ const REFERENCE_REF = text(128, 1);
66
+ const CHECKPOINT_REF = text(128, 1);
67
+ const EXPECTED_REVISION = decimal();
68
+ const FILE_SUMMARY = object({
69
+ target: TARGET,
70
+ path: PATH,
71
+ contentHash: digest(),
72
+ byteLength: integer(0, 1048576),
73
+ });
74
+ const FILE_BODY = object({
75
+ target: TARGET,
76
+ path: PATH,
77
+ content: text(65536),
78
+ contentHash: digest(),
79
+ });
80
+ const PROJECT_SUMMARY = object({
81
+ projectId: text(128, 1),
82
+ name: text(120, 1),
83
+ kind: enumText(['SERVER', 'CLIENT', 'FULL_STACK']),
84
+ revision: EXPECTED_REVISION,
85
+ updatedAt: dateTime(),
86
+ archived: boolean(),
87
+ }, ['projectId', 'name', 'kind', 'revision', 'updatedAt', 'archived']);
88
+ const PROJECT = object({
89
+ projectId: text(128, 1),
90
+ name: text(120, 1),
91
+ description: text(1024),
92
+ kind: enumText(['SERVER', 'CLIENT', 'FULL_STACK']),
93
+ revision: EXPECTED_REVISION,
94
+ files: array(FILE_SUMMARY, 128),
95
+ serverModuleName: text(120, 1),
96
+ clientModuleName: text(120, 1),
97
+ pairingPreference: enumText(['NONE', 'OPTIONAL', 'REQUIRED']),
98
+ updatedAt: dateTime(),
99
+ }, [
100
+ 'projectId',
101
+ 'name',
102
+ 'kind',
103
+ 'revision',
104
+ 'files',
105
+ 'pairingPreference',
106
+ 'updatedAt',
107
+ ]);
108
+ const REFERENCE = object({
109
+ referenceId: text(128, 1),
110
+ source: enumText(['PERSONAL_LIBRARY', 'COMMON']),
111
+ title: text(120, 1),
112
+ target: TARGET,
113
+ path: PATH,
114
+ contentHash: digest(),
115
+ byteLength: integer(0, 65536),
116
+ version: EXPECTED_REVISION,
117
+ }, [
118
+ 'referenceId',
119
+ 'source',
120
+ 'title',
121
+ 'target',
122
+ 'path',
123
+ 'contentHash',
124
+ 'byteLength',
125
+ 'version',
126
+ ]);
127
+ const REFERENCE_BODY = object({
128
+ reference: REFERENCE,
129
+ content: text(65536),
130
+ });
131
+ const CHECKPOINT = object({
132
+ checkpointId: text(128, 1),
133
+ projectRevision: EXPECTED_REVISION,
134
+ contentHash: digest(),
135
+ reason: enumText(['AGENT_WRITE', 'RESTORE_PREIMAGE', 'MANUAL']),
136
+ createdAt: dateTime(),
137
+ });
138
+ const PATCH_CHANGE = object({
139
+ target: TARGET,
140
+ path: PATH,
141
+ content: text(65536),
142
+ expectedContentHash: {
143
+ ...text(71, 6),
144
+ pattern: '^(ABSENT|sha256:[0-9a-f]{64})$',
145
+ },
146
+ });
147
+ const PATCH_RESULT = object({
148
+ projectRevision: EXPECTED_REVISION,
149
+ checkpointId: text(128, 1),
150
+ changedFiles: array(FILE_SUMMARY, 16, 1),
151
+ });
152
+ const RUNTIME_STATUS = object({
153
+ phase: enumText([
154
+ 'IDLE',
155
+ 'TESTING_DRAFT',
156
+ 'DEPLOYING_LIVE',
157
+ 'COMPILING',
158
+ 'ENABLING',
159
+ 'RUNNING',
160
+ 'COMPILE_FAILED',
161
+ 'STOPPING',
162
+ 'STOPPED',
163
+ 'PARTIAL_FAILURE',
164
+ 'ERROR',
165
+ ]),
166
+ savedRevision: EXPECTED_REVISION,
167
+ runningRevision: EXPECTED_REVISION,
168
+ sync: enumText(['NEVER_RUN', 'RUNNING_SAVED', 'RUNNING_STALE', 'STOPPED']),
169
+ target: TARGET,
170
+ draft: boolean(),
171
+ message: text(512),
172
+ }, ['phase', 'savedRevision', 'sync']);
173
+ const DIAGNOSTIC = object({
174
+ source: enumText(['LOCAL_ADVISORY', 'RUSTC', 'RUNTIME']),
175
+ target: TARGET,
176
+ path: PATH,
177
+ line: integer(1, 1000000),
178
+ column: integer(1, 1000000),
179
+ severity: enumText(['ERROR', 'WARNING', 'INFO', 'HINT']),
180
+ code: text(64),
181
+ message: text(2048, 1),
182
+ }, ['source', 'target', 'path', 'line', 'column', 'severity', 'message']);
183
+ const RUN_ROW = object({
184
+ runRef: text(128, 1),
185
+ moduleName: text(120, 1),
186
+ triggerSource: text(80, 1),
187
+ startedAt: dateTime(),
188
+ durationUs: integer(0, 2147483647),
189
+ fuelUsed: decimal(),
190
+ success: boolean(),
191
+ errorMessage: text(1024),
192
+ }, [
193
+ 'runRef',
194
+ 'moduleName',
195
+ 'triggerSource',
196
+ 'startedAt',
197
+ 'durationUs',
198
+ 'fuelUsed',
199
+ 'success',
200
+ ]);
201
+ const READ = ['ASK', 'BUILD'];
202
+ const ALL_MODES = ['ASK', 'BUILD', 'PLAY'];
203
+ const BUILD = ['BUILD'];
204
+ const PLAY = ['PLAY'];
205
+ const ASK_PLAY = ['ASK', 'PLAY'];
206
+ const BUILD_PLAY = ['BUILD', 'PLAY'];
207
+ const specs = [
208
+ {
209
+ name: 'studio.context.get',
210
+ summary: 'Read bounded current Studio, permission, runtime, epoch, and lease context.',
211
+ executor: 'BROWSER',
212
+ modes: ALL_MODES,
213
+ output: object({
214
+ appRef: text(128, 1),
215
+ projectRef: text(128, 1),
216
+ gridRef: text(128, 1),
217
+ contextVersion: text(128, 1),
218
+ saveState: enumText(['SAVING', 'SAVED', 'CONFLICT', 'OFFLINE']),
219
+ runtime: RUNTIME_STATUS,
220
+ clientEpoch: decimal(),
221
+ leaseKinds: array(enumText(['WORKSPACE', 'PLAY']), 2, 0, true),
222
+ hostCapabilityRevision: text(128, 1),
223
+ }, ['appRef', 'gridRef', 'contextVersion', 'saveState', 'runtime', 'leaseKinds']),
224
+ },
225
+ {
226
+ name: 'studio.state.get',
227
+ summary: 'Read a bounded, credential-free projection of the headless Studio kernel.',
228
+ executor: 'BROWSER',
229
+ modes: READ,
230
+ output: object({
231
+ project: PROJECT,
232
+ openFiles: array(object({
233
+ source: enumText(['PROJECT', 'PERSONAL_LIBRARY', 'COMMON']),
234
+ target: TARGET,
235
+ path: PATH,
236
+ }), 32),
237
+ saveState: enumText(['SAVING', 'SAVED', 'CONFLICT', 'OFFLINE']),
238
+ runtime: RUNTIME_STATUS,
239
+ }, ['openFiles', 'saveState', 'runtime']),
240
+ },
241
+ {
242
+ name: 'project.list',
243
+ summary: 'List bounded metadata for projects owned by the current player and app.',
244
+ executor: 'SERVER',
245
+ modes: READ,
246
+ input: object({ cursor: text(128), limit: integer(1, 50) }),
247
+ output: object({
248
+ projects: array(PROJECT_SUMMARY, 50),
249
+ nextCursor: text(128),
250
+ }, ['projects']),
251
+ scopes: ['studio.project.read'],
252
+ },
253
+ {
254
+ name: 'project.get',
255
+ summary: 'Read one owner-scoped project and immutable provenance using a safe project reference.',
256
+ executor: 'SERVER',
257
+ modes: READ,
258
+ input: object({ projectRef: PROJECT_REF }),
259
+ output: PROJECT,
260
+ scopes: ['studio.project.read'],
261
+ outputRedaction: [{ path: '$.files', action: 'SUMMARY' }],
262
+ },
263
+ {
264
+ name: 'project.select',
265
+ summary: 'Select an owner-scoped project after pending human edits are safely resolved.',
266
+ executor: 'BROWSER',
267
+ modes: READ,
268
+ input: object({ projectRef: PROJECT_REF }),
269
+ output: object({ selectedProjectRef: PROJECT_REF, revision: EXPECTED_REVISION }),
270
+ },
271
+ {
272
+ name: 'project.create',
273
+ summary: 'Create a private bounded project from an enumerated starter kind.',
274
+ executor: 'SERVER',
275
+ modes: BUILD,
276
+ input: object({
277
+ name: text(120, 1),
278
+ description: text(1024),
279
+ kind: enumText(['SERVER', 'CLIENT', 'FULL_STACK']),
280
+ starter: enumText(['EMPTY', 'MINIMAL', 'TICK', 'HUD']),
281
+ }, ['name', 'kind', 'starter']),
282
+ output: PROJECT,
283
+ risk: 'ROUTINE_WRITE',
284
+ effects: ['project_create'],
285
+ scopes: ['studio.project.write.server', 'studio.project.write.client'],
286
+ },
287
+ {
288
+ name: 'project.create_from_modules',
289
+ summary: 'Copy readable immutable module sources into a new private project by value.',
290
+ executor: 'SERVER',
291
+ modes: BUILD,
292
+ input: object({
293
+ name: text(120, 1),
294
+ modules: array(object({ target: TARGET, moduleRef: text(128, 1), versionRef: text(128, 1) }), 2, 1),
295
+ }),
296
+ output: PROJECT,
297
+ risk: 'ROUTINE_WRITE',
298
+ effects: ['project_create', 'source_copy'],
299
+ scopes: ['studio.project.read', 'studio.project.write.server', 'studio.project.write.client'],
300
+ },
301
+ {
302
+ name: 'project.settings.update',
303
+ summary: 'Update bounded project metadata and pairing at an expected revision.',
304
+ executor: 'SERVER',
305
+ modes: BUILD,
306
+ input: object({
307
+ expectedRevision: EXPECTED_REVISION,
308
+ name: text(120, 1),
309
+ description: text(1024),
310
+ serverModuleName: text(120, 1),
311
+ clientModuleName: text(120, 1),
312
+ pairingPreference: enumText(['NONE', 'OPTIONAL', 'REQUIRED']),
313
+ }, ['expectedRevision', 'name', 'pairingPreference']),
314
+ output: PROJECT,
315
+ risk: 'ROUTINE_WRITE',
316
+ effects: ['project_metadata_write'],
317
+ scopes: ['studio.project.write.server', 'studio.project.write.client'],
318
+ },
319
+ {
320
+ name: 'project.archive',
321
+ summary: 'Archive or restore a private project after exact human approval.',
322
+ executor: 'SERVER',
323
+ modes: BUILD,
324
+ input: object({
325
+ expectedRevision: EXPECTED_REVISION,
326
+ action: enumText(['ARCHIVE', 'RESTORE']),
327
+ }),
328
+ output: PROJECT_SUMMARY,
329
+ risk: 'DESTRUCTIVE',
330
+ effects: ['project_archive_state'],
331
+ reversible: true,
332
+ scopes: ['studio.project.write.server', 'studio.project.write.client'],
333
+ approvalReasons: ['Archiving or restoring changes project availability.'],
334
+ },
335
+ {
336
+ name: 'project.checkpoint.list',
337
+ summary: 'List immutable checkpoint metadata for the selected readable project.',
338
+ executor: 'SERVER',
339
+ modes: READ,
340
+ input: object({ cursor: text(128), limit: integer(1, 50) }),
341
+ output: object({ checkpoints: array(CHECKPOINT, 50), nextCursor: text(128) }, ['checkpoints']),
342
+ scopes: ['studio.project.read'],
343
+ },
344
+ {
345
+ name: 'project.checkpoint.restore',
346
+ summary: 'Restore an exact checkpoint into a new revision after human approval.',
347
+ executor: 'SERVER',
348
+ modes: BUILD,
349
+ input: object({
350
+ checkpointId: CHECKPOINT_REF,
351
+ checkpointContentHash: digest(),
352
+ expectedRevision: EXPECTED_REVISION,
353
+ }),
354
+ output: object({ project: PROJECT, preRestoreCheckpoint: CHECKPOINT }),
355
+ risk: 'DESTRUCTIVE',
356
+ effects: ['checkpoint_create', 'project_restore'],
357
+ reversible: true,
358
+ scopes: ['studio.project.write.server', 'studio.project.write.client'],
359
+ approvalReasons: ['Restore replaces the current project snapshot.'],
360
+ },
361
+ {
362
+ name: 'workspace.file.list',
363
+ summary: 'List bounded file path, target, size, and hash metadata.',
364
+ executor: 'SERVER',
365
+ modes: READ,
366
+ output: object({ files: array(FILE_SUMMARY, 128) }),
367
+ scopes: ['studio.project.read'],
368
+ },
369
+ {
370
+ name: 'workspace.file.read',
371
+ summary: 'Read one bounded file from the selected owner project.',
372
+ executor: 'SERVER',
373
+ modes: READ,
374
+ input: object({ target: TARGET, path: PATH }),
375
+ output: FILE_BODY,
376
+ scopes: ['studio.project.read'],
377
+ outputRedaction: [{ path: '$.content', action: 'TRUNCATE', maxBytes: 16384 }],
378
+ maxPersistedBytes: 20480,
379
+ },
380
+ {
381
+ name: 'workspace.file.create',
382
+ summary: 'Create one bounded file at an expected project revision with a checkpoint.',
383
+ executor: 'SERVER',
384
+ modes: BUILD,
385
+ input: object({
386
+ expectedRevision: EXPECTED_REVISION,
387
+ target: TARGET,
388
+ path: PATH,
389
+ content: text(65536),
390
+ }),
391
+ output: PATCH_RESULT,
392
+ risk: 'ROUTINE_WRITE',
393
+ effects: ['project_write', 'checkpoint_create'],
394
+ scopeRequirements: [
395
+ {
396
+ scope: 'studio.project.write.server',
397
+ when: {
398
+ argumentPath: '$.target',
399
+ operator: 'EQUALS',
400
+ value: 'SERVER',
401
+ },
402
+ },
403
+ {
404
+ scope: 'studio.project.write.client',
405
+ when: {
406
+ argumentPath: '$.target',
407
+ operator: 'EQUALS',
408
+ value: 'CLIENT',
409
+ },
410
+ },
411
+ ],
412
+ inputRedaction: [{ path: '$.content', action: 'SUMMARY' }],
413
+ },
414
+ {
415
+ name: 'workspace.file.patch',
416
+ summary: 'Atomically create or replace bounded files with one pre-image checkpoint.',
417
+ executor: 'SERVER',
418
+ modes: BUILD,
419
+ input: object({
420
+ expectedRevision: EXPECTED_REVISION,
421
+ changes: array(PATCH_CHANGE, 16, 1),
422
+ }),
423
+ output: PATCH_RESULT,
424
+ risk: 'ROUTINE_WRITE',
425
+ effects: ['project_write', 'checkpoint_create'],
426
+ scopeRequirements: [
427
+ {
428
+ scope: 'studio.project.write.server',
429
+ when: {
430
+ argumentPath: '$.changes[*].target',
431
+ operator: 'CONTAINS',
432
+ value: 'SERVER',
433
+ },
434
+ },
435
+ {
436
+ scope: 'studio.project.write.client',
437
+ when: {
438
+ argumentPath: '$.changes[*].target',
439
+ operator: 'CONTAINS',
440
+ value: 'CLIENT',
441
+ },
442
+ },
443
+ ],
444
+ inputRedaction: [{ path: '$.changes[*].content', action: 'SUMMARY' }],
445
+ },
446
+ {
447
+ name: 'workspace.file.rename',
448
+ summary: 'Rename one project path after exact approval because references may be lost.',
449
+ executor: 'SERVER',
450
+ modes: BUILD,
451
+ input: object({
452
+ expectedRevision: EXPECTED_REVISION,
453
+ target: TARGET,
454
+ fromPath: PATH,
455
+ toPath: PATH,
456
+ expectedContentHash: digest(),
457
+ }),
458
+ output: PATCH_RESULT,
459
+ risk: 'DESTRUCTIVE',
460
+ effects: ['project_path_change', 'checkpoint_create'],
461
+ reversible: true,
462
+ scopeRequirements: [
463
+ {
464
+ scope: 'studio.project.write.server',
465
+ when: {
466
+ argumentPath: '$.target',
467
+ operator: 'EQUALS',
468
+ value: 'SERVER',
469
+ },
470
+ },
471
+ {
472
+ scope: 'studio.project.write.client',
473
+ when: {
474
+ argumentPath: '$.target',
475
+ operator: 'EQUALS',
476
+ value: 'CLIENT',
477
+ },
478
+ },
479
+ ],
480
+ approvalReasons: ['Renaming a file can break project references.'],
481
+ },
482
+ {
483
+ name: 'workspace.file.delete',
484
+ summary: 'Delete one exact project file after approval and preserve a checkpoint.',
485
+ executor: 'SERVER',
486
+ modes: BUILD,
487
+ input: object({
488
+ expectedRevision: EXPECTED_REVISION,
489
+ target: TARGET,
490
+ path: PATH,
491
+ expectedContentHash: digest(),
492
+ }),
493
+ output: PATCH_RESULT,
494
+ risk: 'DESTRUCTIVE',
495
+ effects: ['project_file_delete', 'checkpoint_create'],
496
+ reversible: true,
497
+ scopeRequirements: [
498
+ {
499
+ scope: 'studio.project.write.server',
500
+ when: {
501
+ argumentPath: '$.target',
502
+ operator: 'EQUALS',
503
+ value: 'SERVER',
504
+ },
505
+ },
506
+ {
507
+ scope: 'studio.project.write.client',
508
+ when: {
509
+ argumentPath: '$.target',
510
+ operator: 'EQUALS',
511
+ value: 'CLIENT',
512
+ },
513
+ },
514
+ ],
515
+ approvalReasons: ['Deleting source removes a project path.'],
516
+ },
517
+ {
518
+ name: 'workspace.tab.open',
519
+ summary: 'Open a loaded Studio file tab without changing project source.',
520
+ executor: 'BROWSER',
521
+ modes: READ,
522
+ input: object({
523
+ source: enumText(['PROJECT', 'PERSONAL_LIBRARY', 'COMMON']),
524
+ target: TARGET,
525
+ path: PATH,
526
+ referenceRef: REFERENCE_REF,
527
+ }, ['source', 'target', 'path']),
528
+ output: OK,
529
+ },
530
+ {
531
+ name: 'workspace.tab.close',
532
+ summary: 'Close a loaded Studio file tab without changing project source.',
533
+ executor: 'BROWSER',
534
+ modes: READ,
535
+ input: object({
536
+ source: enumText(['PROJECT', 'PERSONAL_LIBRARY', 'COMMON']),
537
+ target: TARGET,
538
+ path: PATH,
539
+ referenceRef: REFERENCE_REF,
540
+ }, ['source', 'target', 'path']),
541
+ output: OK,
542
+ },
543
+ {
544
+ name: 'workspace.save',
545
+ summary: 'Flush the complete pending project delta with optimistic revision checking.',
546
+ executor: 'SERVER',
547
+ modes: BUILD,
548
+ input: object({ expectedRevision: EXPECTED_REVISION }),
549
+ output: object({ projectRevision: EXPECTED_REVISION, changedFiles: array(FILE_SUMMARY, 128) }),
550
+ risk: 'ROUTINE_WRITE',
551
+ effects: ['project_write'],
552
+ scopes: ['studio.project.write.server', 'studio.project.write.client'],
553
+ },
554
+ {
555
+ name: 'workspace.conflict.resolve',
556
+ summary: 'Resolve a revision conflict in either direction after exact approval.',
557
+ executor: 'SERVER',
558
+ modes: BUILD,
559
+ input: object({
560
+ localRevision: EXPECTED_REVISION,
561
+ remoteRevision: EXPECTED_REVISION,
562
+ resolution: enumText(['ACCEPT_REMOTE', 'OVERWRITE_REMOTE']),
563
+ localContentHash: digest(),
564
+ remoteContentHash: digest(),
565
+ }),
566
+ output: PROJECT,
567
+ risk: 'DESTRUCTIVE',
568
+ effects: ['project_conflict_resolution', 'checkpoint_create'],
569
+ reversible: true,
570
+ scopes: ['studio.project.write.server', 'studio.project.write.client'],
571
+ approvalReasons: ['Conflict resolution discards one side of an edit.'],
572
+ },
573
+ {
574
+ name: 'library.personal.list',
575
+ summary: 'List owner-scoped personal library metadata for the current app.',
576
+ executor: 'SERVER',
577
+ modes: READ,
578
+ input: object({ cursor: text(128), limit: integer(1, 100) }),
579
+ output: object({ files: array(REFERENCE, 100), nextCursor: text(128) }, ['files']),
580
+ scopes: ['studio.project.read'],
581
+ },
582
+ {
583
+ name: 'library.personal.read',
584
+ summary: 'Read one owner-scoped personal library file and exact version.',
585
+ executor: 'SERVER',
586
+ modes: READ,
587
+ input: object({ referenceRef: REFERENCE_REF, version: EXPECTED_REVISION }),
588
+ output: REFERENCE_BODY,
589
+ scopes: ['studio.project.read'],
590
+ outputRedaction: [{ path: '$.content', action: 'TRUNCATE', maxBytes: 16384 }],
591
+ maxPersistedBytes: 20480,
592
+ },
593
+ {
594
+ name: 'library.personal.save',
595
+ summary: 'Create or update a bounded personal library file by expected version.',
596
+ executor: 'SERVER',
597
+ modes: BUILD,
598
+ input: object({
599
+ referenceRef: REFERENCE_REF,
600
+ expectedVersion: EXPECTED_REVISION,
601
+ title: text(120, 1),
602
+ target: TARGET,
603
+ path: PATH,
604
+ content: text(65536),
605
+ }, ['title', 'target', 'path', 'content']),
606
+ output: REFERENCE,
607
+ risk: 'ROUTINE_WRITE',
608
+ effects: ['personal_library_write'],
609
+ scopes: ['studio.project.write.server', 'studio.project.write.client'],
610
+ inputRedaction: [{ path: '$.content', action: 'SUMMARY' }],
611
+ },
612
+ {
613
+ name: 'library.personal.save_from_project',
614
+ summary: 'Copy one selected project file by value into the personal library.',
615
+ executor: 'SERVER',
616
+ modes: BUILD,
617
+ input: object({
618
+ target: TARGET,
619
+ path: PATH,
620
+ expectedContentHash: digest(),
621
+ title: text(120, 1),
622
+ }),
623
+ output: REFERENCE,
624
+ risk: 'ROUTINE_WRITE',
625
+ effects: ['personal_library_write', 'source_copy'],
626
+ scopes: ['studio.project.read', 'studio.project.write.server', 'studio.project.write.client'],
627
+ },
628
+ {
629
+ name: 'library.common.list',
630
+ summary: 'List immutable published common-file metadata for the current app.',
631
+ executor: 'SERVER',
632
+ modes: READ,
633
+ input: object({ cursor: text(128), limit: integer(1, 100) }),
634
+ output: object({ files: array(REFERENCE, 100), nextCursor: text(128) }, ['files']),
635
+ scopes: ['studio.project.read'],
636
+ },
637
+ {
638
+ name: 'library.common.read',
639
+ summary: 'Read one immutable published common-file version.',
640
+ executor: 'SERVER',
641
+ modes: READ,
642
+ input: object({ referenceRef: REFERENCE_REF, version: EXPECTED_REVISION }),
643
+ output: REFERENCE_BODY,
644
+ scopes: ['studio.project.read'],
645
+ outputRedaction: [{ path: '$.content', action: 'TRUNCATE', maxBytes: 16384 }],
646
+ maxPersistedBytes: 20480,
647
+ },
648
+ {
649
+ name: 'library.import',
650
+ summary: 'Copy one readable library version by value into the selected project.',
651
+ executor: 'SERVER',
652
+ modes: BUILD,
653
+ input: object({
654
+ expectedRevision: EXPECTED_REVISION,
655
+ source: enumText(['PERSONAL_LIBRARY', 'COMMON']),
656
+ referenceRef: REFERENCE_REF,
657
+ version: EXPECTED_REVISION,
658
+ destinationTarget: TARGET,
659
+ destinationPath: PATH,
660
+ }),
661
+ output: PATCH_RESULT,
662
+ risk: 'ROUTINE_WRITE',
663
+ effects: ['project_write', 'source_copy', 'checkpoint_create'],
664
+ scopes: ['studio.project.read', 'studio.project.write.server', 'studio.project.write.client'],
665
+ },
666
+ {
667
+ name: 'template.list',
668
+ summary: 'List allowed starter templates and target compatibility.',
669
+ executor: 'SERVER',
670
+ modes: READ,
671
+ output: object({
672
+ templates: array(object({
673
+ templateRef: text(128, 1),
674
+ name: text(120, 1),
675
+ kind: enumText(['SERVER', 'CLIENT', 'FULL_STACK']),
676
+ contentHash: digest(),
677
+ }), 50),
678
+ }),
679
+ scopes: ['studio.project.read'],
680
+ },
681
+ {
682
+ name: 'template.apply',
683
+ summary: 'Apply an exact starter template, requiring approval when replacing files.',
684
+ executor: 'SERVER',
685
+ modes: BUILD,
686
+ input: object({
687
+ expectedRevision: EXPECTED_REVISION,
688
+ templateRef: text(128, 1),
689
+ templateContentHash: digest(),
690
+ behavior: enumText(['EMPTY_ONLY', 'REPLACE']),
691
+ }),
692
+ output: PATCH_RESULT,
693
+ risk: 'DESTRUCTIVE',
694
+ effects: ['project_write', 'checkpoint_create'],
695
+ reversible: true,
696
+ scopes: ['studio.project.write.server', 'studio.project.write.client'],
697
+ approvalReasons: ['Replacing existing files requires exact human approval.'],
698
+ },
699
+ {
700
+ name: 'diagnostics.local.get',
701
+ summary: 'Read bounded browser-local advisory diagnostics labeled as untrusted.',
702
+ executor: 'BROWSER',
703
+ modes: READ,
704
+ output: object({ diagnostics: array(DIAGNOSTIC, 256) }),
705
+ outputRedaction: [{ path: '$.diagnostics[*].message', action: 'TRUNCATE', maxBytes: 512 }],
706
+ },
707
+ {
708
+ name: 'diagnostics.compile.get',
709
+ summary: 'Read authoritative bounded compiler diagnostics and redacted logs.',
710
+ executor: 'SERVER',
711
+ modes: READ,
712
+ input: object({ target: TARGET, runRef: text(128, 1) }, ['target']),
713
+ output: object({ diagnostics: array(DIAGNOSTIC, 256), log: text(32768) }),
714
+ scopes: ['studio.project.read'],
715
+ outputRedaction: [{ path: '$.log', action: 'TRUNCATE', maxBytes: 8192 }],
716
+ maxPersistedBytes: 16384,
717
+ },
718
+ {
719
+ name: 'runtime.status.get',
720
+ summary: 'Read saved-versus-running project revision, target, phase, and pairing status.',
721
+ executor: 'BROWSER',
722
+ modes: READ,
723
+ output: RUNTIME_STATUS,
724
+ },
725
+ {
726
+ name: 'runtime.runs.list',
727
+ summary: 'List bounded own-project runtime execution summaries.',
728
+ executor: 'SERVER',
729
+ modes: READ,
730
+ input: object({ cursor: text(128), limit: integer(1, 50) }),
731
+ output: object({ runs: array(RUN_ROW, 50), nextCursor: text(128) }, ['runs']),
732
+ scopes: ['studio.project.read'],
733
+ },
734
+ {
735
+ name: 'runtime.logs.list',
736
+ summary: 'List bounded redacted own-project runtime logs as untrusted data.',
737
+ executor: 'SERVER',
738
+ modes: READ,
739
+ input: object({ cursor: text(128), limit: integer(1, 50) }),
740
+ output: object({
741
+ logs: array(object({
742
+ runRef: text(128, 1),
743
+ createdAt: dateTime(),
744
+ level: enumText(['DEBUG', 'INFO', 'WARN', 'ERROR']),
745
+ message: text(2048),
746
+ }), 50),
747
+ nextCursor: text(128),
748
+ }, ['logs']),
749
+ scopes: ['studio.project.read'],
750
+ outputRedaction: [{ path: '$.logs[*].message', action: 'TRUNCATE', maxBytes: 512 }],
751
+ },
752
+ {
753
+ name: 'runtime.usage.get',
754
+ summary: 'Read compile, runtime, and agent budget plus read-only wallet balance.',
755
+ executor: 'SERVER',
756
+ modes: READ,
757
+ output: object({
758
+ compileUnitsUsed: decimal(),
759
+ compileUnitsLimit: decimal(),
760
+ agentUnitsUsed: decimal(),
761
+ agentUnitsLimit: decimal(),
762
+ walletBalanceMinor: decimal(true),
763
+ currency: text(8, 3),
764
+ }),
765
+ },
766
+ {
767
+ name: 'runtime.test_draft',
768
+ summary: 'Compile and test the saved project as draft without making it live.',
769
+ executor: 'BROWSER',
770
+ modes: BUILD,
771
+ input: object({
772
+ expectedRevision: EXPECTED_REVISION,
773
+ targets: array(TARGET, 2, 1, true),
774
+ }),
775
+ output: object({ runtime: RUNTIME_STATUS, compiledTargets: array(TARGET, 2, 1, true) }),
776
+ risk: 'ROUTINE_WRITE',
777
+ effects: ['draft_compile', 'draft_runtime'],
778
+ scopeRequirements: [
779
+ {
780
+ scope: 'studio.runtime.run.server',
781
+ when: {
782
+ argumentPath: '$.targets[*]',
783
+ operator: 'CONTAINS',
784
+ value: 'SERVER',
785
+ },
786
+ },
787
+ {
788
+ scope: 'studio.runtime.run.client',
789
+ when: {
790
+ argumentPath: '$.targets[*]',
791
+ operator: 'CONTAINS',
792
+ value: 'CLIENT',
793
+ },
794
+ },
795
+ ],
796
+ timeoutMs: 120000,
797
+ },
798
+ {
799
+ name: 'runtime.deploy_live',
800
+ summary: 'Deploy the exact saved revision live after bound human approval.',
801
+ executor: 'BROWSER',
802
+ modes: BUILD,
803
+ input: object({
804
+ expectedRevision: EXPECTED_REVISION,
805
+ projectContentHash: digest(),
806
+ targets: array(TARGET, 2, 1, true),
807
+ pairingPreference: enumText(['NONE', 'OPTIONAL', 'REQUIRED']),
808
+ draft: { ...boolean(), const: false },
809
+ }),
810
+ output: object({ runtime: RUNTIME_STATUS, deployedTargets: array(TARGET, 2, 1, true) }),
811
+ risk: 'ROUTINE_WRITE',
812
+ effects: ['live_deployment', 'runtime_enable'],
813
+ reversible: true,
814
+ scopeRequirements: [
815
+ { scope: 'studio.runtime.live' },
816
+ {
817
+ scope: 'studio.runtime.run.server',
818
+ when: {
819
+ argumentPath: '$.targets[*]',
820
+ operator: 'CONTAINS',
821
+ value: 'SERVER',
822
+ },
823
+ },
824
+ {
825
+ scope: 'studio.runtime.run.client',
826
+ when: {
827
+ argumentPath: '$.targets[*]',
828
+ operator: 'CONTAINS',
829
+ value: 'CLIENT',
830
+ },
831
+ },
832
+ ],
833
+ approval: 'REQUIRED',
834
+ approvalReasons: ['Live deployment changes the running project for players.'],
835
+ timeoutMs: 120000,
836
+ },
837
+ {
838
+ name: 'runtime.invoke',
839
+ summary: 'Invoke one contract-advertised runtime export with bounded scalar parameters.',
840
+ executor: 'BROWSER',
841
+ modes: BUILD_PLAY,
842
+ input: object({
843
+ exportName: text(120, 1),
844
+ environment: enumText(['DRAFT', 'LIVE']),
845
+ params: array(object({
846
+ name: text(64, 1),
847
+ type: enumText(['STRING', 'DECIMAL', 'BOOLEAN']),
848
+ value: text(1024),
849
+ }), 32),
850
+ }),
851
+ output: object({
852
+ resultType: enumText(['EMPTY', 'TEXT', 'BASE64']),
853
+ result: text(16384),
854
+ fuelUsed: decimal(),
855
+ durationUs: integer(0, 2147483647),
856
+ }),
857
+ risk: 'WORLD_CONTROL',
858
+ effects: ['runtime_invoke'],
859
+ scopes: ['studio.runtime.run.server'],
860
+ approval: 'CONDITIONAL',
861
+ approvalReasons: ['Invoking a live runtime export requires Play authority and exact approval.'],
862
+ outputRedaction: [{ path: '$.result', action: 'TRUNCATE', maxBytes: 4096 }],
863
+ },
864
+ {
865
+ name: 'runtime.stop',
866
+ summary: 'Idempotently stop project server runtime and the local client worker.',
867
+ executor: 'BROWSER',
868
+ modes: BUILD,
869
+ output: object({
870
+ serverStopped: boolean(),
871
+ clientStopped: boolean(),
872
+ failures: array(text(512, 1), 2),
873
+ }),
874
+ risk: 'ROUTINE_WRITE',
875
+ effects: ['runtime_disable', 'client_worker_stop'],
876
+ reversible: true,
877
+ scopes: ['studio.runtime.run.server', 'studio.runtime.run.client'],
878
+ },
879
+ {
880
+ name: 'game.capabilities.get',
881
+ summary: 'Read the current generic host capability revision and typed command catalog.',
882
+ executor: 'BROWSER',
883
+ modes: ASK_PLAY,
884
+ output: PLAYER_HOST_CAPABILITIES_SCHEMA_V1,
885
+ },
886
+ {
887
+ name: 'game.observe',
888
+ summary: 'Read one bounded fresh game snapshot; observations never grant authority.',
889
+ executor: 'BROWSER',
890
+ modes: ASK_PLAY,
891
+ input: OBSERVE_REQUEST_SCHEMA_V1,
892
+ output: GAME_OBSERVATION_SCHEMA_V1,
893
+ scopes: ['game.observe'],
894
+ outputRedaction: [
895
+ { path: '$.nearbyActors', action: 'SUMMARY' },
896
+ { path: '$.nearbyVoxels', action: 'SUMMARY' },
897
+ ],
898
+ maxPersistedBytes: 8192,
899
+ },
900
+ gameCommand('game.control.move', 'Move the controlled entity briefly through the ordinary locomotion intent service.', object({
901
+ observationId: text(128, 1),
902
+ capabilityRevision: text(128, 1),
903
+ controlledEntityId: text(128, 1),
904
+ direction: enumText(['FORWARD', 'BACKWARD', 'LEFT', 'RIGHT', 'UP', 'DOWN']),
905
+ intensity: number(0, 1),
906
+ durationMs: integer(16, 2000),
907
+ }), 'game.locomotion'),
908
+ gameCommand('game.control.look', 'Adjust controlled look direction through the ordinary player camera intent service.', object({
909
+ observationId: text(128, 1),
910
+ capabilityRevision: text(128, 1),
911
+ controlledEntityId: text(128, 1),
912
+ deltaYaw: number(-180, 180),
913
+ deltaPitch: number(-90, 90),
914
+ }), 'game.locomotion'),
915
+ {
916
+ name: 'game.control.stop',
917
+ summary: 'Synchronously clear every agent movement and action intent; always locally allowed.',
918
+ executor: 'BROWSER',
919
+ modes: PLAY,
920
+ output: GAME_COMMAND_RESULT_SCHEMA_V1,
921
+ risk: 'WORLD_CONTROL',
922
+ effects: ['agent_intent_clear'],
923
+ reversible: true,
924
+ },
925
+ gameCommand('game.inventory.select', 'Select one bounded inventory slot through the ordinary inventory service.', plannedInput({ slot: integer(0, 255) }), 'game.interact'),
926
+ gameCommand('game.inventory.consume', 'Consume a bounded quantity from one inventory slot through ordinary rules.', plannedInput({ slot: integer(0, 255), quantity: integer(1, 64) }), 'game.interact'),
927
+ gameCommand('game.inventory.transfer', 'Transfer bounded items to or from the currently opened authorized container.', plannedInput({
928
+ direction: enumText(['TO_CONTAINER', 'FROM_CONTAINER']),
929
+ slot: integer(0, 255),
930
+ quantity: integer(1, 64),
931
+ containerRef: text(128, 1),
932
+ }), 'game.interact'),
933
+ gameCommand('game.interact', 'Perform one enumerated ordinary interaction against the fresh observed target.', object({
934
+ observationId: text(128, 1),
935
+ capabilityRevision: text(128, 1),
936
+ controlledEntityId: text(128, 1),
937
+ action: enumText(['MINE', 'PLACE', 'USE', 'FISH', 'NPC_TALK']),
938
+ targetRef: text(128, 1),
939
+ inventorySlot: integer(0, 255),
940
+ }, [
941
+ 'observationId',
942
+ 'capabilityRevision',
943
+ 'controlledEntityId',
944
+ 'action',
945
+ 'targetRef',
946
+ ]), 'game.interact'),
947
+ gameCommand('game.craft', 'Craft a bounded non-economic recipe using the player material inventory.', plannedInput({ recipeId: text(128, 1), quantity: integer(1, 64) }), 'game.craft'),
948
+ gameCommand('game.mount', 'Mount or dismount one currently controllable host-advertised entity.', object({
949
+ observationId: text(128, 1),
950
+ capabilityRevision: text(128, 1),
951
+ controlledEntityId: text(128, 1),
952
+ action: enumText(['MOUNT', 'DISMOUNT']),
953
+ mountRef: text(128, 1),
954
+ }, ['observationId', 'capabilityRevision', 'controlledEntityId', 'action']), 'game.locomotion'),
955
+ {
956
+ ...gameCommand('game.combat.attack', 'Attack one fresh observed target through the ordinary combat referee path.', plannedInput({
957
+ targetRef: text(128, 1),
958
+ attack: enumText(['PRIMARY', 'SECONDARY']),
959
+ }), 'game.combat'),
960
+ approval: {
961
+ policy: 'CONDITIONAL',
962
+ reasons: ['Player or PvP targets may require exact human approval.'],
963
+ maxTtlSeconds: 120,
964
+ },
965
+ },
966
+ gameCommand('game.chat.send', 'Send bounded text through an existing rate-limited game chat channel.', plannedInput({
967
+ channel: enumText(['LOCAL', 'GROUP']),
968
+ text: text(280, 1),
969
+ }), 'game.communicate'),
970
+ gameCommand('game.travel.teleport', 'Travel to one current host-advertised destination through ordinary authorization.', plannedInput({ destinationRef: text(128, 1) }), 'game.travel'),
971
+ ];
972
+ /** Complete minimum Studio and generic game registry for contract version 1. */
973
+ export const CROWDY_AGENT_TOOL_DESCRIPTORS_V1 = deepFreeze(specs.map((spec) => 'schemaVersion' in spec ? spec : buildDescriptor(spec)));
974
+ function plannedInput(extra) {
975
+ return object({
976
+ observationId: text(128, 1),
977
+ capabilityRevision: text(128, 1),
978
+ controlledEntityId: text(128, 1),
979
+ ...extra,
980
+ });
981
+ }
982
+ function gameCommand(name, summary, input, scope) {
983
+ return buildDescriptor({
984
+ name,
985
+ summary,
986
+ executor: 'BROWSER',
987
+ modes: PLAY,
988
+ input,
989
+ output: GAME_COMMAND_RESULT_SCHEMA_V1,
990
+ risk: 'WORLD_CONTROL',
991
+ effects: ['game_intent'],
992
+ scopes: [scope],
993
+ });
994
+ }
995
+ function buildDescriptor(spec) {
996
+ const risk = spec.risk ?? 'READ_ONLY';
997
+ const approval = spec.approval ??
998
+ (['DESTRUCTIVE', 'TRUST_CONSENT', 'ECONOMIC', 'IRREVERSIBLE'].includes(risk)
999
+ ? 'REQUIRED'
1000
+ : 'NONE');
1001
+ const idempotency = spec.idempotency ??
1002
+ (risk === 'READ_ONLY'
1003
+ ? 'PURE'
1004
+ : spec.executor === 'SERVER'
1005
+ ? 'KEYED'
1006
+ : 'TOOL_CALL_ONCE');
1007
+ return {
1008
+ schemaVersion: 'crowdy.agent-tool/1',
1009
+ name: spec.name,
1010
+ wireName: `${spec.name.replace(/\./gu, '_')}_v1`,
1011
+ version: '1.0.0',
1012
+ summary: spec.summary,
1013
+ executor: spec.executor,
1014
+ modes: spec.modes,
1015
+ inputSchema: spec.input ?? EMPTY,
1016
+ outputSchema: spec.output ?? OK,
1017
+ risk: {
1018
+ class: risk,
1019
+ effects: spec.effects ?? [],
1020
+ reversible: spec.reversible ?? (risk === 'READ_ONLY' || risk === 'ROUTINE_WRITE'),
1021
+ },
1022
+ scopes: [
1023
+ { scope: 'agent.use' },
1024
+ ...(spec.scopeRequirements ??
1025
+ (spec.scopes ?? []).map((scope) => ({ scope }))),
1026
+ ],
1027
+ approval: {
1028
+ policy: approval,
1029
+ reasons: approval === 'NONE'
1030
+ ? []
1031
+ : spec.approvalReasons ?? ['Exact human approval is required.'],
1032
+ maxTtlSeconds: approval === 'NONE' ? 0 : 120,
1033
+ },
1034
+ idempotency: {
1035
+ class: idempotency,
1036
+ keyScope: idempotency === 'PURE'
1037
+ ? 'NONE'
1038
+ : idempotency === 'KEYED'
1039
+ ? 'USER_TOOL_ARGUMENTS'
1040
+ : 'TOOL_CALL',
1041
+ },
1042
+ timeoutMs: spec.timeoutMs ?? (spec.executor === 'BROWSER' ? 10000 : 15000),
1043
+ redaction: {
1044
+ input: spec.inputRedaction ?? [],
1045
+ output: spec.outputRedaction ?? [],
1046
+ maxPersistedBytes: spec.maxPersistedBytes ?? 4096,
1047
+ },
1048
+ };
1049
+ }