arbiter-server 0.9.1.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,706 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, ItemsView, KeysView, Mapping, Sequence
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, NoReturn, Protocol, TypeVar, cast
6
+
7
+ from .version import arbiter_server_version, compatibility_line, server_api_version
8
+
9
+
10
+ RuntimeT = TypeVar("RuntimeT")
11
+ SERVER_VERSION = arbiter_server_version()
12
+ SERVER_API_VERSION = server_api_version()
13
+ ACCOUNT_TEST_STATUSES = {"ok", "failed", "skipped"}
14
+
15
+
16
+ class ToolServer(Protocol):
17
+ def tool(
18
+ self,
19
+ name: str | None = None,
20
+ title: str | None = None,
21
+ description: str | None = None,
22
+ annotations: Any = None,
23
+ icons: Any = None,
24
+ meta: Any = None,
25
+ structured_output: bool | None = None,
26
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ...
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class RuntimeRegistry:
31
+ runtimes: Mapping[str, object]
32
+
33
+ def require_object(self, service_name: str) -> object:
34
+ runtime = self.runtimes.get(service_name)
35
+ if runtime is None:
36
+ raise RuntimeError(f"service runtime is not configured: {service_name}")
37
+ return runtime
38
+
39
+ def require(self, service_name: str, runtime_type: type[RuntimeT]) -> RuntimeT:
40
+ runtime = self.require_object(service_name)
41
+ if not isinstance(runtime, runtime_type):
42
+ raise RuntimeError(f"service runtime is not configured: {service_name}")
43
+ return runtime
44
+
45
+ def items(self) -> ItemsView[str, object]:
46
+ return self.runtimes.items()
47
+
48
+ def keys(self) -> KeysView[str]:
49
+ return self.runtimes.keys()
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class ServiceRuntimeContext:
54
+ dependencies: Mapping[str, object] = field(default_factory=dict)
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class ServicePluginContext:
59
+ runtimes: RuntimeRegistry
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class CapabilityDescriptor:
64
+ name: str
65
+ description: str
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class OperationDescriptor:
70
+ name: str
71
+ description: str
72
+ input_schema: Mapping[str, object]
73
+
74
+
75
+ def operation_id(capability: str, operation: str) -> str:
76
+ return f"{capability}:{operation}"
77
+
78
+
79
+ def parse_operation_id(value: str) -> tuple[str, str]:
80
+ capability, separator, operation = value.partition(":")
81
+ if not capability or not separator or not operation:
82
+ raise ValueError(
83
+ "operation id must use CAPABILITY:OPERATION syntax: " f"{value}"
84
+ )
85
+ return capability, operation
86
+
87
+
88
+ class ServicePlugin(Protocol):
89
+ name: str
90
+ version: str
91
+ server_api_version: str
92
+
93
+ # Called before Hydra composes application config. Plugins register all
94
+ # service-owned schema and example options in their ConfigStore groups here.
95
+ def register_configs(self, config_store: Any) -> None: ...
96
+
97
+ def bootstrap_config(self, *, kind: str, name: str) -> object | None: ...
98
+
99
+ def build_runtime(
100
+ self,
101
+ accounts: Mapping[str, object],
102
+ policies: Mapping[str, object],
103
+ context: ServiceRuntimeContext,
104
+ ) -> object: ...
105
+
106
+ def describe_capability(
107
+ self,
108
+ context: ServicePluginContext,
109
+ ) -> CapabilityDescriptor: ...
110
+
111
+ def describe_operations(
112
+ self,
113
+ context: ServicePluginContext,
114
+ ) -> Sequence[OperationDescriptor]: ...
115
+
116
+ def invoke_operation(
117
+ self,
118
+ operation: str,
119
+ arguments: Mapping[str, Any],
120
+ context: ServicePluginContext,
121
+ ) -> object: ...
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class ServicePluginRuntimeInfo:
126
+ name: str
127
+ version: str
128
+ server_api_version: str
129
+
130
+
131
+ def service_plugin_runtime_info(
132
+ service_plugin: ServicePlugin,
133
+ ) -> ServicePluginRuntimeInfo:
134
+ try:
135
+ plugin_version = service_plugin.version
136
+ except AttributeError as exc:
137
+ raise RuntimeError(
138
+ f"service plugin {service_plugin.name} does not declare a version"
139
+ ) from exc
140
+
141
+ try:
142
+ plugin_server_api_version = service_plugin.server_api_version
143
+ except AttributeError as exc:
144
+ raise RuntimeError(
145
+ f"service plugin {service_plugin.name} does not declare "
146
+ "an Arbiter server API version"
147
+ ) from exc
148
+
149
+ return ServicePluginRuntimeInfo(
150
+ name=service_plugin.name,
151
+ version=plugin_version,
152
+ server_api_version=plugin_server_api_version,
153
+ )
154
+
155
+
156
+ def validate_service_plugin_compatibility(
157
+ service_plugin: ServicePlugin,
158
+ ) -> None:
159
+ info = service_plugin_runtime_info(service_plugin)
160
+ if info.server_api_version != SERVER_API_VERSION:
161
+ raise RuntimeError(
162
+ f"service plugin {info.name} targets Arbiter server API "
163
+ f"{info.server_api_version}, but loaded server API is {SERVER_API_VERSION}"
164
+ )
165
+
166
+ plugin_line = compatibility_line(info.version)
167
+ if plugin_line != SERVER_API_VERSION:
168
+ raise RuntimeError(
169
+ f"service plugin {info.name} version {info.version} is not on "
170
+ f"loaded server API line {SERVER_API_VERSION}"
171
+ )
172
+
173
+
174
+ def validate_service_plugins(
175
+ service_plugins: Sequence[ServicePlugin],
176
+ ) -> None:
177
+ for service_plugin in service_plugins:
178
+ validate_service_plugin_compatibility(service_plugin)
179
+
180
+
181
+ class OperationCatalog:
182
+ def __init__(
183
+ self,
184
+ service_plugins: Sequence[ServicePlugin],
185
+ context: ServicePluginContext,
186
+ *,
187
+ max_account_preview_limit: int,
188
+ max_operation_preview_limit: int,
189
+ ) -> None:
190
+ if max_account_preview_limit < 1:
191
+ raise ValueError("max_account_preview_limit must be >= 1")
192
+ if max_operation_preview_limit < 1:
193
+ raise ValueError("max_operation_preview_limit must be >= 1")
194
+ self._context = context
195
+ self._capabilities: dict[str, CapabilityDescriptor] = {}
196
+ self._operations: dict[str, dict[str, OperationDescriptor]] = {}
197
+ self._plugins: dict[str, ServicePlugin] = {}
198
+ self._max_account_preview_limit = max_account_preview_limit
199
+ self._max_operation_preview_limit = max_operation_preview_limit
200
+
201
+ for plugin in service_plugins:
202
+ capability = plugin.describe_capability(context)
203
+ if capability.name in self._capabilities:
204
+ raise RuntimeError(f"duplicate capability: {capability.name}")
205
+ operations: dict[str, OperationDescriptor] = {}
206
+ for operation in plugin.describe_operations(context):
207
+ if operation.name in operations:
208
+ raise RuntimeError(
209
+ f"duplicate operation for {capability.name}: {operation.name}"
210
+ )
211
+ operations[operation.name] = operation
212
+ self._capabilities[capability.name] = capability
213
+ self._operations[capability.name] = operations
214
+ self._plugins[capability.name] = plugin
215
+
216
+ def list_capabilities(self) -> dict[str, object]:
217
+ return {"capabilities": sorted(self._capabilities)}
218
+
219
+ def describe_capabilities(
220
+ self,
221
+ *,
222
+ operation_preview_limit: int = 8,
223
+ account_preview_limit: int = 8,
224
+ ) -> dict[str, object]:
225
+ if operation_preview_limit < 0:
226
+ raise ValueError("operation_preview_limit must be >= 0")
227
+ if account_preview_limit < 0:
228
+ raise ValueError("account_preview_limit must be >= 0")
229
+ effective_operation_preview_limit = min(
230
+ operation_preview_limit,
231
+ self._max_operation_preview_limit,
232
+ )
233
+ effective_account_preview_limit = min(
234
+ account_preview_limit,
235
+ self._max_account_preview_limit,
236
+ )
237
+ return {
238
+ "capabilities": [
239
+ self._capability_summary(
240
+ capability,
241
+ operation_preview_limit=effective_operation_preview_limit,
242
+ account_preview_limit=effective_account_preview_limit,
243
+ )
244
+ for capability in sorted(self._capabilities)
245
+ ]
246
+ }
247
+
248
+ def describe_capability(self, capability: str) -> dict[str, object]:
249
+ descriptor = self._require_capability(capability)
250
+ return {
251
+ "id": descriptor.name,
252
+ "description": descriptor.description,
253
+ "accounts": self._account_summaries(capability),
254
+ "operations": [
255
+ self._operation_summary(capability, operation)
256
+ for operation in sorted(self._operations[capability])
257
+ ],
258
+ }
259
+
260
+ def describe_operation(self, operation_ref: str) -> dict[str, object]:
261
+ capability, operation = parse_operation_id(operation_ref)
262
+ descriptor = self._require_operation(capability, operation)
263
+ return {
264
+ "id": operation_id(capability, operation),
265
+ "capability": capability,
266
+ "name": descriptor.name,
267
+ "description": descriptor.description,
268
+ "input_schema": dict(descriptor.input_schema),
269
+ }
270
+
271
+ def info(
272
+ self,
273
+ *,
274
+ kind: str = "overview",
275
+ plugin: str | None = None,
276
+ account: str | None = None,
277
+ operation: str | None = None,
278
+ version_info: Mapping[str, object] | None = None,
279
+ ) -> dict[str, object]:
280
+ if kind == "overview":
281
+ return self._info_overview(version_info or {})
282
+ if kind == "plugins":
283
+ return {
284
+ "kind": "plugins",
285
+ "plugins": [
286
+ self._info_plugin_summary(capability, include_accounts=False)
287
+ for capability in sorted(self._capabilities)
288
+ ],
289
+ }
290
+ if kind == "plugin":
291
+ if plugin is None:
292
+ raise ValueError("info plugin requires plugin")
293
+ return self._info_plugin(plugin)
294
+ if kind == "accounts":
295
+ if plugin is None:
296
+ raise ValueError("info accounts requires plugin")
297
+ return {
298
+ "kind": "accounts",
299
+ "plugin": plugin,
300
+ "accounts": self._info_account_summaries(plugin),
301
+ }
302
+ if kind == "account":
303
+ if plugin is None or account is None:
304
+ raise ValueError("info account requires plugin and account")
305
+ return self._info_account(plugin, account)
306
+ if kind == "tests":
307
+ return {
308
+ "kind": "tests",
309
+ "plugins": [
310
+ self._info_plugin_tests(capability)
311
+ for capability in sorted(self._capabilities)
312
+ ],
313
+ }
314
+ if kind == "test":
315
+ if plugin is None:
316
+ raise ValueError("info test requires plugin")
317
+ if account is not None:
318
+ return self._info_account_test(plugin, account)
319
+ plugin_tests = self._info_plugin_tests(plugin)
320
+ plugin_tests["kind"] = "test"
321
+ return plugin_tests
322
+ if kind == "ops":
323
+ if plugin is None:
324
+ raise ValueError("info ops requires plugin")
325
+ self._require_capability(plugin)
326
+ return {
327
+ "kind": "ops",
328
+ "plugin": plugin,
329
+ "operations": [
330
+ self._operation_summary(plugin, operation_name)
331
+ for operation_name in sorted(self._operations[plugin])
332
+ ],
333
+ }
334
+ if kind == "op":
335
+ if plugin is None or operation is None:
336
+ raise ValueError("info op requires plugin and operation")
337
+ operation_info = self.describe_operation(operation_id(plugin, operation))
338
+ operation_info["kind"] = "op"
339
+ return operation_info
340
+ supported = "account, accounts, op, ops, overview, plugin, plugins, test, tests"
341
+ raise ValueError(f"unknown info kind: {kind}; supported kinds: {supported}")
342
+
343
+ def invoke_operation(
344
+ self,
345
+ operation_ref: str,
346
+ arguments: Mapping[str, Any] | None = None,
347
+ ) -> object:
348
+ capability, operation = parse_operation_id(operation_ref)
349
+ descriptor = self._require_operation(capability, operation)
350
+ operation_arguments = dict(arguments or {})
351
+ _validate_operation_arguments(
352
+ operation_id(capability, operation),
353
+ descriptor.input_schema,
354
+ operation_arguments,
355
+ )
356
+ return self._plugins[capability].invoke_operation(
357
+ operation,
358
+ operation_arguments,
359
+ self._context,
360
+ )
361
+
362
+ def _require_capability(self, capability: str) -> CapabilityDescriptor:
363
+ descriptor = self._capabilities.get(capability)
364
+ if descriptor is None:
365
+ raise ValueError(f"unknown capability: {capability}")
366
+ return descriptor
367
+
368
+ def _require_operation(
369
+ self,
370
+ capability: str,
371
+ operation: str,
372
+ ) -> OperationDescriptor:
373
+ self._require_capability(capability)
374
+ descriptor = self._operations[capability].get(operation)
375
+ if descriptor is None:
376
+ raise ValueError(
377
+ f"unknown operation: {operation_id(capability, operation)}"
378
+ )
379
+ return descriptor
380
+
381
+ def _info_overview(
382
+ self,
383
+ version_info: Mapping[str, object],
384
+ ) -> dict[str, object]:
385
+ overview: dict[str, object] = {
386
+ "kind": "overview",
387
+ "deployment_scope": version_info.get("deployment_scope", "unknown"),
388
+ "plugins": [
389
+ self._info_plugin_summary(capability, include_accounts=True)
390
+ for capability in sorted(self._capabilities)
391
+ ],
392
+ }
393
+ server = version_info.get("server")
394
+ if server is not None:
395
+ overview["server"] = server
396
+ source = version_info.get("source")
397
+ if source is not None:
398
+ overview["source"] = source
399
+ return overview
400
+
401
+ def _info_plugin_summary(
402
+ self,
403
+ capability: str,
404
+ *,
405
+ include_accounts: bool,
406
+ ) -> dict[str, object]:
407
+ descriptor = self._capabilities[capability]
408
+ account_summaries = self._account_summaries(capability)
409
+ operation_names = sorted(self._operations[capability])
410
+ summary: dict[str, object] = {
411
+ "id": descriptor.name,
412
+ "description": descriptor.description,
413
+ "version": self._plugins[capability].version,
414
+ "account_count": len(account_summaries),
415
+ "operation_count": len(operation_names),
416
+ }
417
+ if include_accounts:
418
+ summary["accounts"] = self._info_account_summaries(capability)
419
+ return summary
420
+
421
+ def _info_plugin(self, capability: str) -> dict[str, object]:
422
+ self._require_capability(capability)
423
+ summary = self._info_plugin_summary(capability, include_accounts=True)
424
+ summary["kind"] = "plugin"
425
+ summary["operations"] = [
426
+ self._operation_summary(capability, operation)
427
+ for operation in sorted(self._operations[capability])
428
+ ]
429
+ return summary
430
+
431
+ def _info_account_summaries(self, capability: str) -> list[dict[str, object]]:
432
+ self._require_capability(capability)
433
+ summaries = self._account_summaries(capability)
434
+ return [
435
+ self._info_account_summary(capability, account_name, account)
436
+ for account_name, account in sorted(summaries.items())
437
+ if isinstance(account_name, str)
438
+ ]
439
+
440
+ def _info_account_summary(
441
+ self,
442
+ capability: str,
443
+ account_name: str,
444
+ account: object,
445
+ ) -> dict[str, object]:
446
+ description = ""
447
+ guidance = ""
448
+ if isinstance(account, Mapping):
449
+ raw_description = account.get("description")
450
+ if isinstance(raw_description, str):
451
+ description = raw_description
452
+ raw_guidance = account.get("guidance")
453
+ if isinstance(raw_guidance, str):
454
+ guidance = raw_guidance
455
+ return {
456
+ "plugin": capability,
457
+ "name": account_name,
458
+ "description": description,
459
+ "guidance": guidance,
460
+ }
461
+
462
+ def _info_account(self, capability: str, account_name: str) -> dict[str, object]:
463
+ self._require_capability(capability)
464
+ accounts = self._account_summaries(capability)
465
+ account = accounts.get(account_name)
466
+ if account is None:
467
+ raise ValueError(f"unknown account for {capability}: {account_name}")
468
+ details: dict[str, object] = {
469
+ "kind": "account",
470
+ "plugin": capability,
471
+ "account": account_name,
472
+ "guidance": "",
473
+ }
474
+ if isinstance(account, Mapping):
475
+ details.update(account)
476
+ if "guidance" not in details:
477
+ details["guidance"] = ""
478
+ else:
479
+ details["details"] = account
480
+ return details
481
+
482
+ def _info_plugin_tests(self, capability: str) -> dict[str, object]:
483
+ self._require_capability(capability)
484
+ account_summaries = self._account_summaries(capability)
485
+ account_tests = self._account_tests(capability)
486
+ account_names_set: set[str] = set()
487
+ for account_name in account_summaries:
488
+ if isinstance(account_name, str):
489
+ account_names_set.add(account_name)
490
+ for account_name in account_tests:
491
+ if isinstance(account_name, str):
492
+ account_names_set.add(account_name)
493
+ account_names = sorted(account_names_set)
494
+ return {
495
+ "plugin": capability,
496
+ "accounts": [
497
+ self._info_account_test_summary(
498
+ capability,
499
+ account_name,
500
+ account_tests.get(
501
+ account_name,
502
+ {
503
+ "status": "skipped",
504
+ "reason": "account test did not return a result",
505
+ },
506
+ ),
507
+ )
508
+ for account_name in account_names
509
+ ],
510
+ }
511
+
512
+ def _info_account_test(
513
+ self,
514
+ capability: str,
515
+ account_name: str,
516
+ ) -> dict[str, object]:
517
+ self._require_capability(capability)
518
+ account_summaries = self._account_summaries(capability)
519
+ if account_name not in account_summaries:
520
+ raise ValueError(f"unknown account for {capability}: {account_name}")
521
+ account_tests = self._account_tests(capability)
522
+ return {
523
+ "kind": "test",
524
+ **self._info_account_test_summary(
525
+ capability,
526
+ account_name,
527
+ account_tests.get(
528
+ account_name,
529
+ {
530
+ "status": "skipped",
531
+ "reason": "account test did not return a result",
532
+ },
533
+ ),
534
+ ),
535
+ }
536
+
537
+ def _info_account_test_summary(
538
+ self,
539
+ capability: str,
540
+ account_name: str,
541
+ account_test: object,
542
+ ) -> dict[str, object]:
543
+ summary: dict[str, object] = {
544
+ "plugin": capability,
545
+ "account": account_name,
546
+ }
547
+ if isinstance(account_test, Mapping):
548
+ summary.update(account_test)
549
+ else:
550
+ summary["status"] = "ok"
551
+ summary["details"] = account_test
552
+ status = summary.get("status", "ok")
553
+ if not isinstance(status, str) or status not in ACCOUNT_TEST_STATUSES:
554
+ raise RuntimeError(
555
+ "account test status must be one of "
556
+ f"{', '.join(sorted(ACCOUNT_TEST_STATUSES))}: "
557
+ f"{capability}:{account_name}"
558
+ )
559
+ summary["status"] = status
560
+ return summary
561
+
562
+ def _capability_summary(
563
+ self,
564
+ capability: str,
565
+ *,
566
+ operation_preview_limit: int,
567
+ account_preview_limit: int,
568
+ ) -> dict[str, object]:
569
+ descriptor = self._capabilities[capability]
570
+ account_names = sorted(self._account_summaries(capability))
571
+ operation_names = sorted(self._operations[capability])
572
+ return {
573
+ "id": descriptor.name,
574
+ "description": descriptor.description,
575
+ "version": self._plugins[capability].version,
576
+ "account_count": len(account_names),
577
+ "accounts": account_names[:account_preview_limit],
578
+ "accounts_truncated": len(account_names) > account_preview_limit,
579
+ "operation_count": len(operation_names),
580
+ "operations": operation_names[:operation_preview_limit],
581
+ "operations_truncated": len(operation_names) > operation_preview_limit,
582
+ }
583
+
584
+ def _operation_summary(
585
+ self,
586
+ capability: str,
587
+ operation: str,
588
+ ) -> dict[str, object]:
589
+ descriptor = self._operations[capability][operation]
590
+ return {
591
+ "id": operation_id(capability, operation),
592
+ "name": descriptor.name,
593
+ "description": descriptor.description,
594
+ }
595
+
596
+ def _account_summaries(self, capability: str) -> Mapping[str, object]:
597
+ runtime = self._context.runtimes.require_object(capability)
598
+ account_summaries = getattr(runtime, "account_summaries", None)
599
+ if not callable(account_summaries):
600
+ return {}
601
+ result = account_summaries()
602
+ if not isinstance(result, Mapping):
603
+ raise RuntimeError(
604
+ f"capability account summaries must be a mapping: {capability}"
605
+ )
606
+ return result
607
+
608
+ def _account_tests(self, capability: str) -> Mapping[str, object]:
609
+ runtime = self._context.runtimes.require_object(capability)
610
+ test_accounts = getattr(runtime, "test_accounts", None)
611
+ if not callable(test_accounts):
612
+ return {
613
+ account_name: {
614
+ "status": "skipped",
615
+ "reason": "runtime does not implement account tests",
616
+ }
617
+ for account_name in self._account_summaries(capability)
618
+ }
619
+ result = test_accounts()
620
+ if not isinstance(result, Mapping):
621
+ raise RuntimeError(f"account tests must be a mapping: {capability}")
622
+ return result
623
+
624
+
625
+ def _validate_operation_arguments(
626
+ operation_ref: str,
627
+ schema: Mapping[str, object],
628
+ arguments: Mapping[str, Any],
629
+ ) -> None:
630
+ required = schema.get("required", [])
631
+ if isinstance(required, Sequence) and not isinstance(required, str):
632
+ missing = [
633
+ key for key in required if isinstance(key, str) and key not in arguments
634
+ ]
635
+ if missing:
636
+ raise ValueError(
637
+ f"{operation_ref} missing required argument(s): {', '.join(missing)}"
638
+ )
639
+
640
+ properties = schema.get("properties", {})
641
+ if not isinstance(properties, Mapping):
642
+ return
643
+
644
+ if schema.get("additionalProperties") is False:
645
+ unknown = sorted(str(key) for key in arguments if key not in properties)
646
+ if unknown:
647
+ raise ValueError(
648
+ f"{operation_ref} received unknown argument(s): {', '.join(unknown)}"
649
+ )
650
+
651
+ for name, value in arguments.items():
652
+ property_schema = properties.get(name)
653
+ if not isinstance(property_schema, Mapping):
654
+ continue
655
+ _validate_argument_value(operation_ref, str(name), property_schema, value)
656
+
657
+
658
+ def _validate_argument_value(
659
+ operation_ref: str,
660
+ name: str,
661
+ schema: Mapping[str, object],
662
+ value: object,
663
+ ) -> None:
664
+ expected_type = schema.get("type")
665
+ if expected_type == "string":
666
+ if not isinstance(value, str):
667
+ _raise_argument_type_error(operation_ref, name, "string")
668
+ return
669
+ if expected_type == "integer":
670
+ if not isinstance(value, int) or isinstance(value, bool):
671
+ _raise_argument_type_error(operation_ref, name, "integer")
672
+ integer_value = cast(int, value)
673
+ minimum = schema.get("minimum")
674
+ maximum = schema.get("maximum")
675
+ if isinstance(minimum, int) and integer_value < minimum:
676
+ raise ValueError(f"{operation_ref} argument {name} must be >= {minimum}")
677
+ if isinstance(maximum, int) and integer_value > maximum:
678
+ raise ValueError(f"{operation_ref} argument {name} must be <= {maximum}")
679
+ return
680
+ if expected_type == "boolean":
681
+ if not isinstance(value, bool):
682
+ _raise_argument_type_error(operation_ref, name, "boolean")
683
+ return
684
+ if expected_type == "array":
685
+ if not isinstance(value, list):
686
+ _raise_argument_type_error(operation_ref, name, "array")
687
+ list_value = cast(list[object], value)
688
+ items = schema.get("items")
689
+ if isinstance(items, Mapping) and items.get("type") == "string":
690
+ for index, item in enumerate(list_value):
691
+ if not isinstance(item, str):
692
+ raise ValueError(
693
+ f"{operation_ref} argument {name}[{index}] must be string"
694
+ )
695
+
696
+
697
+ def _raise_argument_type_error(
698
+ operation_ref: str,
699
+ name: str,
700
+ expected_type: str,
701
+ ) -> NoReturn:
702
+ raise ValueError(f"{operation_ref} argument {name} must be {expected_type}")
703
+
704
+
705
+ ServicePluginFactory = Callable[[], ServicePlugin]
706
+ SERVICE_PLUGIN_ENTRY_POINT_GROUP = "arbiter.services"