asfops 0.1.0__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.
asfops/__init__.py ADDED
@@ -0,0 +1,83 @@
1
+ """asfops — Agentic Security Fleet Ops.
2
+
3
+ A security department of LLM agents: a Security Orchestrator triages any
4
+ assessment request, fans out to the relevant fleet members, and composes a
5
+ single comprehensive markdown report with optional usage metadata.
6
+
7
+ Quickstart::
8
+
9
+ import asfops
10
+
11
+ result = asfops.assess_sync("Review our new file-upload API design")
12
+ print(result.report_md)
13
+ """
14
+
15
+ from asfops._version import __version__
16
+ from asfops.api import (
17
+ Fleet,
18
+ assess,
19
+ assess_sync,
20
+ compose_report,
21
+ list_roles,
22
+ )
23
+ from asfops.config import FleetConfig
24
+ from asfops.exceptions import (
25
+ AsfopsError,
26
+ CopilotRuntimeError,
27
+ RoleNotFoundError,
28
+ ToolsNotSupportedError,
29
+ TriageError,
30
+ )
31
+ from asfops.fleet.roles import REGISTRY, RoleRegistry, RoleSpec
32
+ from asfops.fleet.schemas import (
33
+ AgentReport,
34
+ Finding,
35
+ RoleSelection,
36
+ Severity,
37
+ SynthesisSummary,
38
+ TriageDecision,
39
+ )
40
+ from asfops.models import CopilotBridge, CopilotModel, ModelRef, resolve_model, shutdown
41
+ from asfops.results import (
42
+ AgentResult,
43
+ AgentUsage,
44
+ FleetEvent,
45
+ FleetMetadata,
46
+ FleetResult,
47
+ ModelUsageTotals,
48
+ )
49
+
50
+ __all__ = [
51
+ "REGISTRY",
52
+ "AgentReport",
53
+ "AgentResult",
54
+ "AgentUsage",
55
+ "AsfopsError",
56
+ "CopilotBridge",
57
+ "CopilotModel",
58
+ "CopilotRuntimeError",
59
+ "Finding",
60
+ "Fleet",
61
+ "FleetConfig",
62
+ "FleetEvent",
63
+ "FleetMetadata",
64
+ "FleetResult",
65
+ "ModelRef",
66
+ "ModelUsageTotals",
67
+ "RoleNotFoundError",
68
+ "RoleRegistry",
69
+ "RoleSelection",
70
+ "RoleSpec",
71
+ "Severity",
72
+ "SynthesisSummary",
73
+ "ToolsNotSupportedError",
74
+ "TriageDecision",
75
+ "TriageError",
76
+ "__version__",
77
+ "assess",
78
+ "assess_sync",
79
+ "compose_report",
80
+ "list_roles",
81
+ "resolve_model",
82
+ "shutdown",
83
+ ]
asfops/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
asfops/api.py ADDED
@@ -0,0 +1,128 @@
1
+ """The public asfops API: the Fleet and module-level conveniences.
2
+
3
+ Designed as the interface an LLM tool-caller would want: one free-text
4
+ entrypoint (:meth:`Fleet.assess`), a discoverable roster
5
+ (:func:`list_roles`), a targeted single-expert call (:meth:`Fleet.run_role`),
6
+ and a fully ``model_dump_json()``-able :class:`~asfops.results.FleetResult`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ from datetime import UTC, datetime
13
+
14
+ from asfops.config import FleetConfig
15
+ from asfops.fleet.roles import REGISTRY, RoleRegistry, RoleSpec
16
+ from asfops.fleet.schemas import RoleSelection, TriageDecision
17
+ from asfops.models.client import shutdown
18
+ from asfops.orchestrator import EventCallback, Orchestrator
19
+ from asfops.results import (
20
+ AgentResult,
21
+ FleetMetadata,
22
+ FleetResult,
23
+ aggregate_usage,
24
+ build_report_md,
25
+ )
26
+
27
+
28
+ class Fleet:
29
+ """The security fleet: triage, invoke specialists, synthesize a report."""
30
+
31
+ def __init__(
32
+ self, config: FleetConfig | None = None, *, registry: RoleRegistry | None = None
33
+ ) -> None:
34
+ self.config = config or FleetConfig()
35
+ self.registry = registry or REGISTRY
36
+ self._orchestrator = Orchestrator(self.config, registry=self.registry)
37
+
38
+ async def assess(self, request: str, *, on_event: EventCallback | None = None) -> FleetResult:
39
+ """Run a full assessment: triage → fan-out → synthesis → report."""
40
+ return await self._orchestrator.run(request, on_event=on_event)
41
+
42
+ def assess_sync(self, request: str, *, on_event: EventCallback | None = None) -> FleetResult:
43
+ """Synchronous wrapper around :meth:`assess`.
44
+
45
+ Raises if called from within a running event loop (e.g. Jupyter);
46
+ ``await fleet.assess(...)`` there instead.
47
+ """
48
+ _guard_no_running_loop()
49
+ return asyncio.run(self.assess(request, on_event=on_event))
50
+
51
+ async def run_role(self, slug: str, request: str) -> AgentResult:
52
+ """Engage a single specialist by slug (no triage, no synthesis)."""
53
+ self.registry.get(slug) # validates slug, raises RoleNotFoundError
54
+ sel = RoleSelection(slug=slug, rationale="Directly requested.", priority="primary")
55
+ return await self._orchestrator._run_role(sel, request, on_event=None)
56
+
57
+ def run_role_sync(self, slug: str, request: str) -> AgentResult:
58
+ _guard_no_running_loop()
59
+ return asyncio.run(self.run_role(slug, request))
60
+
61
+ def roster(self) -> tuple[RoleSpec, ...]:
62
+ """The specialists available to this fleet."""
63
+ return self.registry.all()
64
+
65
+ async def aclose(self) -> None:
66
+ """Shut down the shared Copilot runtime, if one was started."""
67
+ await shutdown()
68
+
69
+
70
+ def _guard_no_running_loop() -> None:
71
+ try:
72
+ asyncio.get_running_loop()
73
+ except RuntimeError:
74
+ return
75
+ raise RuntimeError(
76
+ "assess_sync() cannot be called from a running event loop; "
77
+ "use `await fleet.assess(...)` instead."
78
+ )
79
+
80
+
81
+ async def assess(request: str, *, config: FleetConfig | None = None) -> FleetResult:
82
+ """Assess a request with a default (or supplied) fleet configuration."""
83
+ return await Fleet(config).assess(request)
84
+
85
+
86
+ def assess_sync(request: str, *, config: FleetConfig | None = None) -> FleetResult:
87
+ """Synchronous convenience wrapper around :func:`assess`."""
88
+ return Fleet(config).assess_sync(request)
89
+
90
+
91
+ def list_roles(*, registry: RoleRegistry | None = None) -> tuple[RoleSpec, ...]:
92
+ """The full security-department roster."""
93
+ return (registry or REGISTRY).all()
94
+
95
+
96
+ def compose_report(
97
+ request: str,
98
+ triage: TriageDecision,
99
+ agent_results: list[AgentResult],
100
+ *,
101
+ include_metadata: bool = True,
102
+ ) -> FleetResult:
103
+ """Assemble a :class:`FleetResult` from pre-computed pieces (no synthesis).
104
+
105
+ Useful when a caller has already run roles and just wants the composed
106
+ markdown report and usage rollup.
107
+ """
108
+ now = datetime.now(UTC)
109
+ metadata: FleetMetadata | None = None
110
+ if include_metadata:
111
+ per_agent = [r.usage for r in agent_results]
112
+ totals, grand = aggregate_usage(per_agent)
113
+ metadata = FleetMetadata(
114
+ per_agent=per_agent,
115
+ totals_by_model=totals,
116
+ grand_total=grand,
117
+ started_at=now,
118
+ finished_at=now,
119
+ )
120
+ report_md = build_report_md(request, triage, agent_results, None, metadata)
121
+ return FleetResult(
122
+ request=request,
123
+ triage=triage,
124
+ agent_results=agent_results,
125
+ synthesis=None,
126
+ report_md=report_md,
127
+ metadata=metadata,
128
+ )
asfops/cli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """asfops command-line interface."""
2
+
3
+ from asfops.cli.app import app, main
4
+
5
+ __all__ = ["app", "main"]
asfops/cli/app.py ADDED
@@ -0,0 +1,282 @@
1
+ """The asfops command-line interface (typer + rich)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import sys
7
+ from enum import StrEnum
8
+ from pathlib import Path
9
+ from typing import Annotated
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.markdown import Markdown
14
+
15
+ from asfops._version import __version__
16
+ from asfops.api import Fleet
17
+ from asfops.cli.render import ProgressReporter, metadata_table, roster_table
18
+ from asfops.config import FleetConfig
19
+ from asfops.exceptions import AsfopsError, RoleNotFoundError
20
+ from asfops.results import AgentResult, FleetResult, build_agent_report_md
21
+
22
+ app = typer.Typer(
23
+ name="asfops",
24
+ help="Agentic Security Fleet Ops — a security department of LLM agents.",
25
+ no_args_is_help=True,
26
+ add_completion=False,
27
+ )
28
+
29
+ _stdout = Console()
30
+ _stderr = Console(stderr=True)
31
+
32
+
33
+ class OutputFormat(StrEnum):
34
+ md = "md"
35
+ json = "json"
36
+
37
+
38
+ class RosterFormat(StrEnum):
39
+ table = "table"
40
+ json = "json"
41
+
42
+
43
+ def _read_request(request: str | None, file: Path | None) -> str:
44
+ if file is not None:
45
+ return file.read_text(encoding="utf-8")
46
+ if request in (None, "-"):
47
+ data = sys.stdin.read()
48
+ if not data.strip():
49
+ _fail("No request provided (empty stdin).")
50
+ return data
51
+ assert request is not None
52
+ return request
53
+
54
+
55
+ def _fail(message: str, code: int = 1) -> None:
56
+ _stderr.print(f"[red]error:[/red] {message}")
57
+ raise typer.Exit(code)
58
+
59
+
60
+ async def _assess_and_close(fleet: Fleet, text: str, reporter: object) -> FleetResult:
61
+ # Run the assessment and shut the shared Copilot client down inside the SAME
62
+ # event loop — stopping it from a second asyncio.run() fails (loop closed).
63
+ try:
64
+ return await fleet.assess(text, on_event=reporter) # type: ignore[arg-type]
65
+ finally:
66
+ await fleet.aclose()
67
+
68
+
69
+ async def _run_role_and_close(fleet: Fleet, slug: str, text: str) -> AgentResult:
70
+ try:
71
+ return await fleet.run_role(slug, text)
72
+ finally:
73
+ await fleet.aclose()
74
+
75
+
76
+ def _build_config(
77
+ model: str | None,
78
+ triage_model: str | None,
79
+ roles: list[str] | None,
80
+ exclude: list[str] | None,
81
+ concurrency: int | None,
82
+ timeout: float | None,
83
+ no_metadata: bool,
84
+ ) -> FleetConfig:
85
+ cfg = FleetConfig()
86
+ if model:
87
+ cfg.default_model = model
88
+ if triage_model:
89
+ cfg.triage_model = triage_model
90
+ if roles:
91
+ cfg.force_roles = tuple(roles)
92
+ if exclude:
93
+ cfg.exclude_roles = tuple(exclude)
94
+ if concurrency:
95
+ cfg.max_concurrency = concurrency
96
+ if timeout:
97
+ cfg.per_agent_timeout_s = timeout
98
+ cfg.include_metadata = not no_metadata
99
+ return cfg
100
+
101
+
102
+ @app.command()
103
+ def assess(
104
+ request: Annotated[
105
+ str | None, typer.Argument(help="The assessment request text, or '-' for stdin.")
106
+ ] = None,
107
+ file: Annotated[
108
+ Path | None, typer.Option("--file", "-f", help="Read the request from a file.")
109
+ ] = None,
110
+ model: Annotated[
111
+ str | None, typer.Option("--model", "-m", help="Default model ref for all agents.")
112
+ ] = None,
113
+ triage_model: Annotated[
114
+ str | None, typer.Option("--triage-model", help="Model ref for the triage step.")
115
+ ] = None,
116
+ role: Annotated[
117
+ list[str] | None, typer.Option("--role", "-r", help="Force a role (repeatable).")
118
+ ] = None,
119
+ exclude: Annotated[
120
+ list[str] | None, typer.Option("--exclude", "-x", help="Exclude a role (repeatable).")
121
+ ] = None,
122
+ concurrency: Annotated[
123
+ int | None, typer.Option("--concurrency", "-c", help="Max concurrent agents.")
124
+ ] = None,
125
+ timeout: Annotated[
126
+ float | None, typer.Option("--timeout", help="Per-agent timeout (seconds).")
127
+ ] = None,
128
+ output_format: Annotated[
129
+ OutputFormat, typer.Option("--format", help="Output format.")
130
+ ] = OutputFormat.md,
131
+ output: Annotated[
132
+ Path | None, typer.Option("--output", "-o", help="Write output to a file.")
133
+ ] = None,
134
+ no_metadata: Annotated[
135
+ bool, typer.Option("--no-metadata", help="Omit usage metadata.")
136
+ ] = False,
137
+ quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress progress output.")] = False,
138
+ ) -> None:
139
+ """Run a full fleet assessment and print the composed report."""
140
+ text = _read_request(request, file)
141
+ cfg = _build_config(model, triage_model, role, exclude, concurrency, timeout, no_metadata)
142
+ fleet = Fleet(cfg)
143
+
144
+ show_progress = not quiet and output_format is OutputFormat.md and output is None
145
+ reporter = ProgressReporter(_stderr) if show_progress else None
146
+
147
+ try:
148
+ result = asyncio.run(_assess_and_close(fleet, text, reporter))
149
+ except AsfopsError as exc:
150
+ _fail(str(exc))
151
+ return
152
+
153
+ if output_format is OutputFormat.json:
154
+ payload = result.model_dump_json(indent=2)
155
+ _emit(payload, output)
156
+ return
157
+
158
+ if output is not None:
159
+ _emit(result.report_md, output)
160
+ return
161
+
162
+ _stdout.print(Markdown(result.report_md))
163
+ table = metadata_table(result)
164
+ if table is not None and not no_metadata:
165
+ _stdout.print(table)
166
+
167
+
168
+ @app.command()
169
+ def run(
170
+ slug: Annotated[str, typer.Argument(help="Role slug to engage (see `asfops roster`).")],
171
+ request: Annotated[
172
+ str | None, typer.Argument(help="The request text, or '-' for stdin.")
173
+ ] = None,
174
+ file: Annotated[Path | None, typer.Option("--file", "-f")] = None,
175
+ model: Annotated[str | None, typer.Option("--model", "-m")] = None,
176
+ output_format: Annotated[
177
+ OutputFormat, typer.Option("--format", help="Output format.")
178
+ ] = OutputFormat.md,
179
+ ) -> None:
180
+ """Engage a single specialist directly (no triage, no synthesis)."""
181
+ text = _read_request(request, file)
182
+ cfg = FleetConfig()
183
+ if model:
184
+ cfg.default_model = model
185
+ fleet = Fleet(cfg)
186
+ try:
187
+ result = asyncio.run(_run_role_and_close(fleet, slug, text))
188
+ except RoleNotFoundError as exc:
189
+ _fail(str(exc), code=2)
190
+ return
191
+ except AsfopsError as exc:
192
+ _fail(str(exc))
193
+ return
194
+
195
+ if output_format is OutputFormat.json:
196
+ _stdout.print_json(result.model_dump_json())
197
+ return
198
+ if result.report is None:
199
+ _fail(f"{slug} failed: {result.error}")
200
+ return
201
+ _stdout.print(Markdown(build_agent_report_md(result.role_name, result.report)))
202
+
203
+
204
+ @app.command()
205
+ def roster(
206
+ output_format: Annotated[
207
+ RosterFormat, typer.Option("--format", help="Output format.")
208
+ ] = RosterFormat.table,
209
+ ) -> None:
210
+ """List the security specialists in the fleet."""
211
+ roles = Fleet().roster()
212
+ if output_format is RosterFormat.json:
213
+ import json
214
+
215
+ _stdout.print_json(
216
+ json.dumps(
217
+ [
218
+ {"slug": r.slug, "name": r.name, "charter": r.charter, "tags": list(r.tags)}
219
+ for r in roles
220
+ ]
221
+ )
222
+ )
223
+ return
224
+ _stdout.print(roster_table(roles))
225
+
226
+
227
+ @app.command()
228
+ def models() -> None:
229
+ """Check GitHub Copilot availability and list available models."""
230
+ try:
231
+ from copilot import CopilotClient
232
+ except ImportError:
233
+ _fail("github-copilot-sdk is not installed.")
234
+ return
235
+
236
+ async def _list() -> list[str]:
237
+ client = CopilotClient(log_level="error")
238
+ await client.start()
239
+ try:
240
+ infos = await client.list_models()
241
+ return [getattr(m, "id", str(m)) for m in infos]
242
+ finally:
243
+ await client.stop()
244
+
245
+ try:
246
+ ids = asyncio.run(_list())
247
+ except Exception as exc:
248
+ _stderr.print(f"[yellow]Copilot runtime unavailable:[/yellow] {exc}")
249
+ _stdout.print(
250
+ "You can still run the fleet on any pydantic-ai model, e.g. "
251
+ "[cyan]--model openai:gpt-5.2[/cyan] or "
252
+ "[cyan]--model anthropic:claude-sonnet-4-5[/cyan]."
253
+ )
254
+ raise typer.Exit(0) from None
255
+ _stdout.print("[green]Copilot runtime OK.[/green] Available models:")
256
+ for mid in ids:
257
+ _stdout.print(f" • copilot:{mid}")
258
+
259
+
260
+ @app.command()
261
+ def version() -> None:
262
+ """Print the asfops version."""
263
+ _stdout.print(__version__)
264
+
265
+
266
+ def _emit(content: str, output: Path | None) -> None:
267
+ if output is not None:
268
+ output.write_text(content, encoding="utf-8")
269
+ _stderr.print(f"[green]Wrote[/green] {output}")
270
+ else:
271
+ # plain stdout, no rich formatting (pipe-friendly)
272
+ sys.stdout.write(content)
273
+ if not content.endswith("\n"):
274
+ sys.stdout.write("\n")
275
+
276
+
277
+ def main() -> None:
278
+ app()
279
+
280
+
281
+ if __name__ == "__main__":
282
+ main()
asfops/cli/render.py ADDED
@@ -0,0 +1,76 @@
1
+ """Rich rendering helpers for the CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from asfops.fleet.roles import RoleSpec
11
+ from asfops.results import FleetEvent, FleetResult
12
+
13
+
14
+ def roster_table(roles: tuple[RoleSpec, ...]) -> Table:
15
+ table = Table(title="Security Fleet Roster", show_lines=False, expand=True)
16
+ table.add_column("Slug", style="cyan", no_wrap=True)
17
+ table.add_column("Role", style="bold")
18
+ table.add_column("Charter")
19
+ for role in roles:
20
+ table.add_row(role.slug, role.name, role.charter)
21
+ return table
22
+
23
+
24
+ def metadata_table(result: FleetResult) -> Table | None:
25
+ if result.metadata is None:
26
+ return None
27
+ table = Table(title="Usage by Model", expand=True)
28
+ table.add_column("Model", style="cyan")
29
+ table.add_column("Requests", justify="right")
30
+ table.add_column("Input", justify="right")
31
+ table.add_column("Output", justify="right")
32
+ for t in result.metadata.totals_by_model:
33
+ table.add_row(t.model_id, str(t.requests), str(t.input_tokens), str(t.output_tokens))
34
+ g = result.metadata.grand_total
35
+ table.add_row(
36
+ "[bold]all[/bold]",
37
+ f"[bold]{g.requests}[/bold]",
38
+ f"[bold]{g.input_tokens}[/bold]",
39
+ f"[bold]{g.output_tokens}[/bold]",
40
+ )
41
+ return table
42
+
43
+
44
+ class ProgressReporter:
45
+ """Renders live per-role progress from fleet events.
46
+
47
+ Kept deliberately simple (line-based, thread-safe) so it composes with any
48
+ console and needs no live-refresh teardown.
49
+ """
50
+
51
+ def __init__(self, console: Console) -> None:
52
+ self.console = console
53
+ self._lock = threading.Lock()
54
+
55
+ def __call__(self, event: FleetEvent) -> None:
56
+ with self._lock:
57
+ self._render(event)
58
+
59
+ def _render(self, event: FleetEvent) -> None:
60
+ c = self.console
61
+ match event.kind:
62
+ case "triage_started":
63
+ c.print("[dim]Triaging request…[/dim]")
64
+ case "triage_finished":
65
+ c.print(f"[green]Triage selected:[/green] {event.detail}")
66
+ case "agent_started":
67
+ c.print(f" [yellow]▶ {event.slug}[/yellow] running…")
68
+ case "agent_finished":
69
+ c.print(f" [green]✓ {event.slug}[/green] done")
70
+ case "agent_failed":
71
+ c.print(f" [red]✗ {event.slug}[/red] failed: {event.detail}")
72
+ case "synthesis_started":
73
+ c.print("[dim]Synthesizing report…[/dim]")
74
+ case "synthesis_finished":
75
+ detail = f" ({event.detail})" if event.detail else ""
76
+ c.print(f"[green]Synthesis complete[/green]{detail}")
asfops/config.py ADDED
@@ -0,0 +1,56 @@
1
+ """Fleet configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Literal
7
+
8
+ from asfops.models.resolve import ModelRef
9
+
10
+ CopilotMode = Literal["model", "bridge"]
11
+
12
+ DEFAULT_MODEL: ModelRef = "copilot:claude-sonnet-4.5"
13
+ DEFAULT_TRIAGE_FALLBACK: tuple[str, ...] = ("security-architect", "threat-model", "appsec")
14
+
15
+
16
+ @dataclass
17
+ class FleetConfig:
18
+ """Configuration for a :class:`~asfops.api.Fleet` run.
19
+
20
+ Any :class:`~asfops.models.resolve.ModelRef` is accepted for a model: a
21
+ ``"copilot:<name>"`` string, a native pydantic-ai model string
22
+ (``"openai:gpt-5.2"``, ``"anthropic:claude-sonnet-4-5"``, ``"test"``), or a
23
+ ``Model`` instance.
24
+ """
25
+
26
+ default_model: ModelRef = DEFAULT_MODEL
27
+ triage_model: ModelRef | None = None
28
+ """Model used for triage; falls back to ``default_model``."""
29
+ synthesis_model: ModelRef | None = None
30
+ """Model used for synthesis; falls back to ``default_model``."""
31
+ model_overrides: dict[str, ModelRef] = field(default_factory=dict)
32
+ """Per-role-slug model overrides."""
33
+
34
+ max_concurrency: int = 5
35
+ per_agent_timeout_s: float = 300.0
36
+
37
+ force_roles: tuple[str, ...] = ()
38
+ """Roles always engaged, regardless of triage."""
39
+ exclude_roles: tuple[str, ...] = ()
40
+ """Roles never engaged, even if triage or force selects them."""
41
+
42
+ include_metadata: bool = True
43
+ copilot_mode: CopilotMode = "model"
44
+ """Reserved: ``"bridge"`` routes Copilot agents through the manual bridge."""
45
+
46
+ on_empty_triage: Literal["fallback", "error"] = "fallback"
47
+ """When triage selects nothing usable: engage a default core set, or raise."""
48
+
49
+ def triage_model_ref(self) -> ModelRef:
50
+ return self.triage_model if self.triage_model is not None else self.default_model
51
+
52
+ def synthesis_model_ref(self) -> ModelRef:
53
+ return self.synthesis_model if self.synthesis_model is not None else self.default_model
54
+
55
+ def model_for_role(self, slug: str) -> ModelRef:
56
+ return self.model_overrides.get(slug, self.default_model)
asfops/exceptions.py ADDED
@@ -0,0 +1,33 @@
1
+ """Exception hierarchy for asfops."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class AsfopsError(Exception):
7
+ """Base class for all asfops errors."""
8
+
9
+
10
+ class CopilotRuntimeError(AsfopsError):
11
+ """The Copilot runtime failed to start, respond, or complete a turn."""
12
+
13
+
14
+ class ToolsNotSupportedError(AsfopsError):
15
+ """Raised when pydantic-ai function tools are requested on a CopilotModel.
16
+
17
+ The Copilot runtime executes tools inside its own turn and cannot return
18
+ tool calls to pydantic-ai. Use an API-key model (e.g. ``openai:...`` or
19
+ ``anthropic:...``) for agents that need custom tools.
20
+ """
21
+
22
+
23
+ class RoleNotFoundError(AsfopsError):
24
+ """Raised when a role slug is not present in the registry."""
25
+
26
+ def __init__(self, slug: str, known: tuple[str, ...]) -> None:
27
+ self.slug = slug
28
+ self.known = known
29
+ super().__init__(f"Unknown role slug {slug!r}. Known roles: {', '.join(known)}")
30
+
31
+
32
+ class TriageError(AsfopsError):
33
+ """Raised when triage produces no usable role selection."""
@@ -0,0 +1,31 @@
1
+ """Fleet: security-department roles, schemas, and agent construction."""
2
+
3
+ from asfops.fleet.member import build_agent
4
+ from asfops.fleet.roles import REGISTRY, RoleRegistry, RoleSpec
5
+ from asfops.fleet.roster import register_default_roles
6
+ from asfops.fleet.schemas import (
7
+ SEVERITY_ORDER,
8
+ AgentReport,
9
+ Confidence,
10
+ Finding,
11
+ RoleSelection,
12
+ Severity,
13
+ SynthesisSummary,
14
+ TriageDecision,
15
+ )
16
+
17
+ __all__ = [
18
+ "REGISTRY",
19
+ "SEVERITY_ORDER",
20
+ "AgentReport",
21
+ "Confidence",
22
+ "Finding",
23
+ "RoleRegistry",
24
+ "RoleSelection",
25
+ "RoleSpec",
26
+ "Severity",
27
+ "SynthesisSummary",
28
+ "TriageDecision",
29
+ "build_agent",
30
+ "register_default_roles",
31
+ ]