agent-backbone 2.0.0a0__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.
Files changed (155) hide show
  1. agent_backbone/__init__.py +3 -0
  2. agent_backbone/api/__init__.py +1 -0
  3. agent_backbone/api/app.py +261 -0
  4. agent_backbone/api/auth.py +48 -0
  5. agent_backbone/api/deps.py +72 -0
  6. agent_backbone/api/models.py +422 -0
  7. agent_backbone/api/routes/__init__.py +1 -0
  8. agent_backbone/api/routes/agents.py +381 -0
  9. agent_backbone/api/routes/config.py +58 -0
  10. agent_backbone/api/routes/deliveries.py +69 -0
  11. agent_backbone/api/routes/events.py +23 -0
  12. agent_backbone/api/routes/help.py +42 -0
  13. agent_backbone/api/routes/integrations.py +47 -0
  14. agent_backbone/api/routes/issues.py +188 -0
  15. agent_backbone/api/routes/messages.py +58 -0
  16. agent_backbone/api/routes/plans.py +139 -0
  17. agent_backbone/api/routes/status.py +136 -0
  18. agent_backbone/api/routes/swarms.py +87 -0
  19. agent_backbone/api/routes/webhook.py +91 -0
  20. agent_backbone/api/session_updates.py +184 -0
  21. agent_backbone/api/socketio_server.py +348 -0
  22. agent_backbone/base/__init__.py +6 -0
  23. agent_backbone/base/lifecycle.py +83 -0
  24. agent_backbone/base/protocols.py +27 -0
  25. agent_backbone/cli/__init__.py +261 -0
  26. agent_backbone/cli/__main__.py +5 -0
  27. agent_backbone/cli/_common.py +113 -0
  28. agent_backbone/cli/agents.py +442 -0
  29. agent_backbone/cli/server.py +218 -0
  30. agent_backbone/cli/service.py +181 -0
  31. agent_backbone/cli/setup.py +277 -0
  32. agent_backbone/cli/swarms.py +127 -0
  33. agent_backbone/config.py +697 -0
  34. agent_backbone/fs.py +33 -0
  35. agent_backbone/git.py +52 -0
  36. agent_backbone/help/__init__.py +93 -0
  37. agent_backbone/help/agent-brief.md +39 -0
  38. agent_backbone/help/docs/README.md +28 -0
  39. agent_backbone/help/docs/api.md +243 -0
  40. agent_backbone/help/docs/cli.md +214 -0
  41. agent_backbone/help/docs/concepts.md +158 -0
  42. agent_backbone/help/docs/configuration.md +156 -0
  43. agent_backbone/help/docs/getting-started.md +353 -0
  44. agent_backbone/help/docs/github-app-setup.md +227 -0
  45. agent_backbone/help/docs/github.md +180 -0
  46. agent_backbone/help/docs/how-it-works.md +270 -0
  47. agent_backbone/help/docs/integrations.md +47 -0
  48. agent_backbone/help/docs/security.md +152 -0
  49. agent_backbone/help/docs/status-and-roadmap.md +75 -0
  50. agent_backbone/help/docs/swarms.md +136 -0
  51. agent_backbone/help/docs/telegram.md +108 -0
  52. agent_backbone/help/topics/agents.md +49 -0
  53. agent_backbone/help/topics/github.md +27 -0
  54. agent_backbone/help/topics/messaging.md +41 -0
  55. agent_backbone/help/topics/setup.md +120 -0
  56. agent_backbone/help/topics/swarms.md +60 -0
  57. agent_backbone/hooks/__init__.py +10 -0
  58. agent_backbone/hooks/claude_hook.py +251 -0
  59. agent_backbone/hooks/install.py +160 -0
  60. agent_backbone/models.py +201 -0
  61. agent_backbone/recent.py +61 -0
  62. agent_backbone/services/__init__.py +1 -0
  63. agent_backbone/services/agents/__init__.py +53 -0
  64. agent_backbone/services/agents/_file_reader.py +127 -0
  65. agent_backbone/services/agents/_inference.py +163 -0
  66. agent_backbone/services/agents/acknowledgement.py +116 -0
  67. agent_backbone/services/agents/launch.py +342 -0
  68. agent_backbone/services/agents/models.py +69 -0
  69. agent_backbone/services/agents/operations.py +101 -0
  70. agent_backbone/services/agents/store.py +214 -0
  71. agent_backbone/services/database/__init__.py +7 -0
  72. agent_backbone/services/database/_acks_repo.py +49 -0
  73. agent_backbone/services/database/_agents_repo.py +116 -0
  74. agent_backbone/services/database/_delivery_repo.py +207 -0
  75. agent_backbone/services/database/_dependencies_repo.py +57 -0
  76. agent_backbone/services/database/_events_repo.py +104 -0
  77. agent_backbone/services/database/_queue_repo.py +150 -0
  78. agent_backbone/services/database/_repo.py +17 -0
  79. agent_backbone/services/database/_settings_repo.py +41 -0
  80. agent_backbone/services/database/_state_repo.py +71 -0
  81. agent_backbone/services/database/_swarms_repo.py +101 -0
  82. agent_backbone/services/database/_time.py +20 -0
  83. agent_backbone/services/database/backbone_db.py +284 -0
  84. agent_backbone/services/database/base.py +20 -0
  85. agent_backbone/services/database/engine.py +53 -0
  86. agent_backbone/services/database/migrations/env.py +83 -0
  87. agent_backbone/services/database/migrations/script.py.mako +25 -0
  88. agent_backbone/services/database/migrations/versions/2026_09_02_3fb2fe03898c_initial_schema.py +284 -0
  89. agent_backbone/services/database/models.py +251 -0
  90. agent_backbone/services/github/__init__.py +5 -0
  91. agent_backbone/services/github/interface.py +408 -0
  92. agent_backbone/services/integrations/__init__.py +11 -0
  93. agent_backbone/services/integrations/_notify.py +46 -0
  94. agent_backbone/services/integrations/_registry.py +92 -0
  95. agent_backbone/services/integrations/base.py +92 -0
  96. agent_backbone/services/integrations/telegram/__init__.py +5 -0
  97. agent_backbone/services/integrations/telegram/_commands.py +329 -0
  98. agent_backbone/services/integrations/telegram/_routing.py +122 -0
  99. agent_backbone/services/integrations/telegram/_topic_discovery.py +179 -0
  100. agent_backbone/services/integrations/telegram/_topics.py +118 -0
  101. agent_backbone/services/integrations/telegram/interface.py +310 -0
  102. agent_backbone/services/jobs/__init__.py +13 -0
  103. agent_backbone/services/jobs/copy_mode.py +50 -0
  104. agent_backbone/services/jobs/escalation.py +243 -0
  105. agent_backbone/services/jobs/github_poll.py +190 -0
  106. agent_backbone/services/jobs/monitor.py +133 -0
  107. agent_backbone/services/jobs/pending.py +140 -0
  108. agent_backbone/services/jobs/retry.py +173 -0
  109. agent_backbone/services/routing/__init__.py +42 -0
  110. agent_backbone/services/routing/_create_notify.py +63 -0
  111. agent_backbone/services/routing/_dedup.py +32 -0
  112. agent_backbone/services/routing/_delivery.py +287 -0
  113. agent_backbone/services/routing/_dependencies.py +98 -0
  114. agent_backbone/services/routing/_format.py +157 -0
  115. agent_backbone/services/routing/_ingest.py +138 -0
  116. agent_backbone/services/routing/_intelligence.py +126 -0
  117. agent_backbone/services/routing/_lifecycle.py +158 -0
  118. agent_backbone/services/routing/_priority.py +33 -0
  119. agent_backbone/services/routing/_resolution.py +39 -0
  120. agent_backbone/services/routing/_router.py +234 -0
  121. agent_backbone/services/routing/_targets.py +130 -0
  122. agent_backbone/services/routing/models.py +50 -0
  123. agent_backbone/services/runtimes/__init__.py +132 -0
  124. agent_backbone/services/runtimes/_pane.py +128 -0
  125. agent_backbone/services/runtimes/aider.py +24 -0
  126. agent_backbone/services/runtimes/base.py +396 -0
  127. agent_backbone/services/runtimes/claude.py +117 -0
  128. agent_backbone/services/runtimes/codex.py +103 -0
  129. agent_backbone/services/runtimes/deepcode.py +78 -0
  130. agent_backbone/services/runtimes/gemini.py +55 -0
  131. agent_backbone/services/runtimes/opencode.py +40 -0
  132. agent_backbone/services/runtimes/shell.py +30 -0
  133. agent_backbone/services/scheduler.py +135 -0
  134. agent_backbone/services/swarm/__init__.py +27 -0
  135. agent_backbone/services/swarm/_roster.py +86 -0
  136. agent_backbone/services/swarm/_templates.py +28 -0
  137. agent_backbone/services/swarm/_worktree.py +81 -0
  138. agent_backbone/services/swarm/interface.py +350 -0
  139. agent_backbone/services/swarm/templates/coder.md +16 -0
  140. agent_backbone/services/swarm/templates/common.md +42 -0
  141. agent_backbone/services/swarm/templates/coordinator.md +45 -0
  142. agent_backbone/services/swarm/templates/reviewer.md +19 -0
  143. agent_backbone/services/swarm/templates/scout.md +17 -0
  144. agent_backbone/services/swarm/templates/worker.md +12 -0
  145. agent_backbone/services/terminal/__init__.py +49 -0
  146. agent_backbone/services/terminal/_copy_mode.py +42 -0
  147. agent_backbone/services/terminal/_core.py +205 -0
  148. agent_backbone/services/terminal/_pty.py +337 -0
  149. agent_backbone/services/terminal/_sessions.py +212 -0
  150. agent_backbone/templates.py +28 -0
  151. agent_backbone-2.0.0a0.dist-info/METADATA +183 -0
  152. agent_backbone-2.0.0a0.dist-info/RECORD +155 -0
  153. agent_backbone-2.0.0a0.dist-info/WHEEL +4 -0
  154. agent_backbone-2.0.0a0.dist-info/entry_points.txt +3 -0
  155. agent_backbone-2.0.0a0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ """agent-backbone — a local control plane for terminal AI agents."""
2
+
3
+ __version__ = "2.0.0a0"
@@ -0,0 +1 @@
1
+ """FastAPI application for the agent-backbone REST API."""
@@ -0,0 +1,261 @@
1
+ """FastAPI application factory.
2
+
3
+ One process hosts everything: the REST API, the Socket.IO feed, the periodic
4
+ scheduler (monitor, delivery retry, GitHub intake), the integrations (the
5
+ Telegram bot) and the GitHub client. Configuration comes from the database: the lifespan opens the
6
+ data directory, starts the database, loads settings and agents, and only then
7
+ wires the remaining services against that snapshot.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from contextlib import asynccontextmanager
14
+
15
+ import httpx
16
+ import socketio
17
+ from fastapi import Depends, FastAPI, Request
18
+ from fastapi.middleware.cors import CORSMiddleware
19
+ from fastapi.responses import JSONResponse
20
+
21
+ from agent_backbone.base import LifecycleManager
22
+ from agent_backbone.config import BackboneConfig, bootstrap_config
23
+
24
+ log = logging.getLogger(__name__)
25
+
26
+ API_VERSION = "2.0.0"
27
+
28
+
29
+ def _register_jobs(app: FastAPI):
30
+ """Wire the periodic jobs. Each job reads ``app.state.config`` at run time so
31
+ setting changes and newly discovered agents are picked up without a restart."""
32
+ from agent_backbone.services.agents import rotate_action_log
33
+ from agent_backbone.services.jobs import GitHubPoller, delivery_retry, monitor_agents
34
+ from agent_backbone.services.scheduler import PeriodicScheduler
35
+
36
+ scheduler = PeriodicScheduler()
37
+ state = app.state
38
+ config: BackboneConfig = state.config
39
+
40
+ async def _broadcast():
41
+ # Reconcile out-of-band session churn to live Socket.IO subscribers.
42
+ await state.feed.emit(only_if_changed=True)
43
+
44
+ async def _monitor():
45
+ await state.agent_store.refresh()
46
+ return await monitor_agents(state.config, state.db, state.github, on_change=_broadcast)
47
+
48
+ async def _retry():
49
+ return await delivery_retry(state.config, state.db, state.github)
50
+
51
+ async def _prune():
52
+ days = state.config.timing.delivery_retention_days
53
+ return {
54
+ "deliveries": await state.db.deliveries.prune(days),
55
+ "events": await state.db.events.prune(days),
56
+ "action_log_lines": rotate_action_log(state.config.action_log_path),
57
+ }
58
+
59
+ # Immediately at startup: this is what syncs agent state after a restart.
60
+ scheduler.add(
61
+ "agent-monitor", config.timing.monitor_interval_seconds, _monitor, run_immediately=True
62
+ )
63
+ scheduler.add("delivery-retry", config.timing.retry_interval_seconds, _retry)
64
+ scheduler.add("prune", 6 * 3600, _prune)
65
+ # Integrations re-provision their per-agent surfaces (Telegram topics):
66
+ # a config publish triggers it immediately, this catches everything else
67
+ # (a group discovered from a message, a transient Telegram error).
68
+ scheduler.add("integrations-sync", 300, state.integrations.sync_agents)
69
+
70
+ if state.github is not None:
71
+ poller = GitHubPoller(
72
+ lambda: state.config,
73
+ state.db,
74
+ state.github,
75
+ issue_closed_hooks=state.issue_closed_hooks,
76
+ )
77
+ if config.github_intake == "poll":
78
+ scheduler.add(
79
+ "github-poll", config.github.poll_interval_seconds, poller.run, run_immediately=True
80
+ )
81
+ elif config.github_intake == "webhook" and config.github.backfill_on_start:
82
+ scheduler.add("github-backfill", 24 * 3600, poller.run, run_immediately=True)
83
+ return scheduler
84
+
85
+
86
+ @asynccontextmanager
87
+ async def lifespan(app: FastAPI):
88
+ """Open the data directory, start the database, load config, wire services."""
89
+ from agent_backbone.api.session_updates import SessionFeed
90
+ from agent_backbone.api.socketio_server import configure_pty_manager, get_pty_manager
91
+ from agent_backbone.services.agents import AgentStore
92
+ from agent_backbone.services.database import BackboneDB
93
+ from agent_backbone.services.github import GitHubClient
94
+ from agent_backbone.services.integrations import build_integrations
95
+ from agent_backbone.services.swarm import teardown_for_issue
96
+
97
+ boot: BackboneConfig = getattr(app.state, "config", None) or bootstrap_config()
98
+ data_dir = boot.data_dir
99
+ data_dir.mkdir(parents=True, exist_ok=True)
100
+ app.state.config = boot
101
+ configure_pty_manager(data_dir)
102
+
103
+ lifecycle = LifecycleManager()
104
+ app.state.lifecycle = lifecycle
105
+
106
+ # Stage 1: the database, then the configuration stored in it.
107
+ app.state.db = BackboneDB(boot.database_url)
108
+ lifecycle.register("database", app.state.db)
109
+ await lifecycle.start_all()
110
+
111
+ def _publish(new_config: BackboneConfig) -> None:
112
+ app.state.config = new_config
113
+ # The set of agents may have changed: integrations re-provision their
114
+ # per-agent surfaces (Telegram topics) against the new snapshot.
115
+ integrations = getattr(app.state, "integrations", None)
116
+ if integrations is not None:
117
+ integrations.schedule_sync()
118
+
119
+ app.state.agent_store = AgentStore(app.state.db, data_dir, on_change=_publish)
120
+ lifecycle.register("agents", app.state.agent_store)
121
+ await lifecycle.start_all()
122
+ config: BackboneConfig = app.state.config
123
+
124
+ if config.github.intake == "webhook" and not config.webhook_secret:
125
+ log.warning(
126
+ "github.intake=webhook but GITHUB_WEBHOOK_SECRET is not set — "
127
+ "webhooks would all be rejected, falling back to polling"
128
+ )
129
+
130
+ # Stage 2: everything else, against the loaded snapshot.
131
+ app.state.github = GitHubClient(config) if config.github_ready else None
132
+ if app.state.github is not None:
133
+ lifecycle.register("github", app.state.github)
134
+ app.state.feed = SessionFeed(lambda: app.state.config, getattr(app.state, "sio", None))
135
+ app.state.integrations = build_integrations(lambda: app.state.config, db=app.state.db)
136
+ for integration in app.state.integrations:
137
+ lifecycle.register(integration.name, integration)
138
+
139
+ # A closed issue ends the swarm that was working it (PR merged -> issue
140
+ # closed via "Closes #N" -> teardown). Handed to ingest as a hook so
141
+ # routing never imports the packages above it.
142
+ async def _swarm_teardown(repo: str, issue_number: int) -> None:
143
+ name = await teardown_for_issue(
144
+ app.state.config, app.state.db, app.state.agent_store, repo, issue_number
145
+ )
146
+ if name:
147
+ log.info("Swarm '%s' completed with %s#%s", name, repo, issue_number)
148
+
149
+ app.state.issue_closed_hooks = (_swarm_teardown,)
150
+ app.state.scheduler = _register_jobs(app)
151
+ lifecycle.register("scheduler", app.state.scheduler)
152
+
153
+ try:
154
+ await lifecycle.start_all()
155
+ log.info(
156
+ "agent-backbone %s on http://%s:%d — data %s, %d agent(s), github=%s, integrations=%s",
157
+ API_VERSION,
158
+ config.backbone.host,
159
+ config.backbone.port,
160
+ data_dir,
161
+ len(config.agents),
162
+ config.github_intake,
163
+ ", ".join(i.name for i in app.state.integrations.enabled) or "none",
164
+ )
165
+ yield
166
+ finally:
167
+ await lifecycle.stop_all()
168
+ await get_pty_manager().cleanup_all()
169
+ log.info("agent-backbone shutting down")
170
+
171
+
172
+ def create_app(config: BackboneConfig | None = None) -> socketio.ASGIApp:
173
+ """Build and return the ASGI application (Socket.IO wrapping FastAPI)."""
174
+ config = config or bootstrap_config()
175
+ app = FastAPI(
176
+ title="agent-backbone",
177
+ description="Local control plane for terminal AI agents",
178
+ version=API_VERSION,
179
+ lifespan=lifespan,
180
+ )
181
+ app.state.config = config
182
+
183
+ cors_origins = list(config.backbone.cors_origins)
184
+ if cors_origins:
185
+ app.add_middleware(
186
+ CORSMiddleware,
187
+ allow_origins=cors_origins,
188
+ allow_credentials=True,
189
+ allow_methods=["*"],
190
+ allow_headers=["*"],
191
+ )
192
+
193
+ @app.exception_handler(httpx.HTTPError)
194
+ async def httpx_error_handler(request: Request, exc: httpx.HTTPError):
195
+ log.warning(
196
+ "External API error on %s %s: %s: %s",
197
+ request.method,
198
+ request.url.path,
199
+ type(exc).__name__,
200
+ exc,
201
+ )
202
+ return JSONResponse(
203
+ status_code=502,
204
+ content={"detail": f"Upstream service error: {type(exc).__name__}"},
205
+ )
206
+
207
+ @app.exception_handler(Exception)
208
+ async def unhandled_exception_handler(request: Request, exc: Exception):
209
+ log.error(
210
+ "Unhandled exception on %s %s: %s",
211
+ request.method,
212
+ request.url.path,
213
+ exc,
214
+ exc_info=True,
215
+ )
216
+ return JSONResponse(status_code=500, content={"detail": "Internal server error"})
217
+
218
+ @app.get("/health")
219
+ async def health(request: Request):
220
+ lifecycle: LifecycleManager | None = getattr(request.app.state, "lifecycle", None)
221
+ if lifecycle is None:
222
+ return {"healthy": False, "components": {}}
223
+ return await lifecycle.health()
224
+
225
+ from agent_backbone.api.auth import require_api_key
226
+ from agent_backbone.api.routes.agents import router as agents_router
227
+ from agent_backbone.api.routes.config import router as config_router
228
+ from agent_backbone.api.routes.deliveries import router as deliveries_router
229
+ from agent_backbone.api.routes.events import router as events_router
230
+ from agent_backbone.api.routes.help import router as help_router
231
+ from agent_backbone.api.routes.integrations import router as integrations_router
232
+ from agent_backbone.api.routes.issues import router as issues_router
233
+ from agent_backbone.api.routes.messages import router as messages_router
234
+ from agent_backbone.api.routes.plans import router as plans_router
235
+ from agent_backbone.api.routes.status import router as status_router
236
+ from agent_backbone.api.routes.swarms import router as swarms_router
237
+ from agent_backbone.api.routes.webhook import router as webhook_router
238
+
239
+ app.include_router(webhook_router)
240
+
241
+ for router in (
242
+ agents_router,
243
+ status_router, # before config_router: /api/config/agents vs /api/config/{key}
244
+ config_router,
245
+ deliveries_router,
246
+ events_router,
247
+ help_router,
248
+ integrations_router,
249
+ issues_router,
250
+ messages_router,
251
+ plans_router,
252
+ swarms_router,
253
+ ):
254
+ app.include_router(router, dependencies=[Depends(require_api_key)])
255
+
256
+ from agent_backbone.api.socketio_server import create_sio
257
+
258
+ sio = create_sio(cors_origins=cors_origins or [])
259
+ sio.fastapi_app = app
260
+ app.state.sio = sio
261
+ return socketio.ASGIApp(sio, app)
@@ -0,0 +1,48 @@
1
+ """API key authentication dependency."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hmac
6
+ import logging
7
+
8
+ from fastapi import HTTPException, Request
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+ _warned_unauthenticated = False
13
+
14
+
15
+ def _configured_api_key(request: Request) -> tuple[str, bool]:
16
+ """Return (api_key, allow_unauthenticated) from the app config."""
17
+ config = getattr(request.app.state, "config", None)
18
+ if config is None:
19
+ return "", False
20
+ return config.api_key, config.security.allow_unauthenticated
21
+
22
+
23
+ def api_key_valid(candidate: str | None, api_key: str) -> bool:
24
+ return bool(candidate) and hmac.compare_digest(candidate, api_key)
25
+
26
+
27
+ async def require_api_key(request: Request) -> None:
28
+ """FastAPI dependency that validates a Bearer token against the configured API key.
29
+
30
+ Without an API key the API refuses requests unless
31
+ ``[security] allow_unauthenticated = true`` (or ``BACKBONE_ALLOW_UNAUTHENTICATED=1``).
32
+ """
33
+ global _warned_unauthenticated
34
+ api_key, allow_unauthenticated = _configured_api_key(request)
35
+ if not api_key:
36
+ if allow_unauthenticated:
37
+ if not _warned_unauthenticated:
38
+ log.warning("API authentication DISABLED (allow_unauthenticated is set)")
39
+ _warned_unauthenticated = True
40
+ return
41
+ raise HTTPException(
42
+ status_code=401,
43
+ detail="No API key configured. Set BACKBONE_API_KEY (run `backbone init`).",
44
+ )
45
+ auth = request.headers.get("Authorization", "")
46
+ token = auth[7:] if auth.startswith("Bearer ") else None
47
+ if not api_key_valid(token, api_key):
48
+ raise HTTPException(status_code=401, detail="Invalid or missing API key")
@@ -0,0 +1,72 @@
1
+ """FastAPI dependency injection — service lookups from app.state."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ from fastapi import HTTPException, Request
8
+
9
+ from agent_backbone.api.session_updates import SessionFeed
10
+ from agent_backbone.config import AgentSpec, BackboneConfig
11
+ from agent_backbone.services.agents import AgentStore
12
+ from agent_backbone.services.database import BackboneDB
13
+ from agent_backbone.services.github import GitHubClient
14
+ from agent_backbone.services.integrations import Integrations
15
+ from agent_backbone.services.routing import IssueClosedHook
16
+ from agent_backbone.services.scheduler import PeriodicScheduler
17
+
18
+
19
+ def get_config(request: Request) -> BackboneConfig:
20
+ return request.app.state.config
21
+
22
+
23
+ def get_db(request: Request) -> BackboneDB:
24
+ return request.app.state.db
25
+
26
+
27
+ def registered_agent_or_404(config: BackboneConfig, name: str) -> AgentSpec:
28
+ """The backbone reads from and types into its *registered* agents only.
29
+
30
+ Other tmux sessions of the same OS user are out of the API's reach even
31
+ though the process could technically capture or type into them: the API
32
+ key is a backbone credential, not a shell on the machine.
33
+ """
34
+ spec = config.agents.get(name)
35
+ if spec is None:
36
+ raise HTTPException(status_code=404, detail=f"'{name}' is not a registered agent")
37
+ return spec
38
+
39
+
40
+ def get_agent_store(request: Request) -> AgentStore:
41
+ return request.app.state.agent_store
42
+
43
+
44
+ def get_github(request: Request) -> GitHubClient:
45
+ """The GitHub client, or 503 when GitHub is not configured."""
46
+ gh = getattr(request.app.state, "github", None)
47
+ if gh is None:
48
+ raise HTTPException(
49
+ status_code=503,
50
+ detail="GitHub is not configured — set GITHUB_TOKEN (or GitHub App credentials)",
51
+ )
52
+ return gh
53
+
54
+
55
+ def get_optional_github(request: Request) -> GitHubClient | None:
56
+ return getattr(request.app.state, "github", None)
57
+
58
+
59
+ def get_feed(request: Request) -> SessionFeed:
60
+ return request.app.state.feed
61
+
62
+
63
+ def get_issue_closed_hooks(request: Request) -> Sequence[IssueClosedHook]:
64
+ return getattr(request.app.state, "issue_closed_hooks", ())
65
+
66
+
67
+ def get_scheduler(request: Request) -> PeriodicScheduler | None:
68
+ return getattr(request.app.state, "scheduler", None)
69
+
70
+
71
+ def get_integrations(request: Request) -> Integrations | None:
72
+ return getattr(request.app.state, "integrations", None)