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.
- agent_backbone/__init__.py +3 -0
- agent_backbone/api/__init__.py +1 -0
- agent_backbone/api/app.py +261 -0
- agent_backbone/api/auth.py +48 -0
- agent_backbone/api/deps.py +72 -0
- agent_backbone/api/models.py +422 -0
- agent_backbone/api/routes/__init__.py +1 -0
- agent_backbone/api/routes/agents.py +381 -0
- agent_backbone/api/routes/config.py +58 -0
- agent_backbone/api/routes/deliveries.py +69 -0
- agent_backbone/api/routes/events.py +23 -0
- agent_backbone/api/routes/help.py +42 -0
- agent_backbone/api/routes/integrations.py +47 -0
- agent_backbone/api/routes/issues.py +188 -0
- agent_backbone/api/routes/messages.py +58 -0
- agent_backbone/api/routes/plans.py +139 -0
- agent_backbone/api/routes/status.py +136 -0
- agent_backbone/api/routes/swarms.py +87 -0
- agent_backbone/api/routes/webhook.py +91 -0
- agent_backbone/api/session_updates.py +184 -0
- agent_backbone/api/socketio_server.py +348 -0
- agent_backbone/base/__init__.py +6 -0
- agent_backbone/base/lifecycle.py +83 -0
- agent_backbone/base/protocols.py +27 -0
- agent_backbone/cli/__init__.py +261 -0
- agent_backbone/cli/__main__.py +5 -0
- agent_backbone/cli/_common.py +113 -0
- agent_backbone/cli/agents.py +442 -0
- agent_backbone/cli/server.py +218 -0
- agent_backbone/cli/service.py +181 -0
- agent_backbone/cli/setup.py +277 -0
- agent_backbone/cli/swarms.py +127 -0
- agent_backbone/config.py +697 -0
- agent_backbone/fs.py +33 -0
- agent_backbone/git.py +52 -0
- agent_backbone/help/__init__.py +93 -0
- agent_backbone/help/agent-brief.md +39 -0
- agent_backbone/help/docs/README.md +28 -0
- agent_backbone/help/docs/api.md +243 -0
- agent_backbone/help/docs/cli.md +214 -0
- agent_backbone/help/docs/concepts.md +158 -0
- agent_backbone/help/docs/configuration.md +156 -0
- agent_backbone/help/docs/getting-started.md +353 -0
- agent_backbone/help/docs/github-app-setup.md +227 -0
- agent_backbone/help/docs/github.md +180 -0
- agent_backbone/help/docs/how-it-works.md +270 -0
- agent_backbone/help/docs/integrations.md +47 -0
- agent_backbone/help/docs/security.md +152 -0
- agent_backbone/help/docs/status-and-roadmap.md +75 -0
- agent_backbone/help/docs/swarms.md +136 -0
- agent_backbone/help/docs/telegram.md +108 -0
- agent_backbone/help/topics/agents.md +49 -0
- agent_backbone/help/topics/github.md +27 -0
- agent_backbone/help/topics/messaging.md +41 -0
- agent_backbone/help/topics/setup.md +120 -0
- agent_backbone/help/topics/swarms.md +60 -0
- agent_backbone/hooks/__init__.py +10 -0
- agent_backbone/hooks/claude_hook.py +251 -0
- agent_backbone/hooks/install.py +160 -0
- agent_backbone/models.py +201 -0
- agent_backbone/recent.py +61 -0
- agent_backbone/services/__init__.py +1 -0
- agent_backbone/services/agents/__init__.py +53 -0
- agent_backbone/services/agents/_file_reader.py +127 -0
- agent_backbone/services/agents/_inference.py +163 -0
- agent_backbone/services/agents/acknowledgement.py +116 -0
- agent_backbone/services/agents/launch.py +342 -0
- agent_backbone/services/agents/models.py +69 -0
- agent_backbone/services/agents/operations.py +101 -0
- agent_backbone/services/agents/store.py +214 -0
- agent_backbone/services/database/__init__.py +7 -0
- agent_backbone/services/database/_acks_repo.py +49 -0
- agent_backbone/services/database/_agents_repo.py +116 -0
- agent_backbone/services/database/_delivery_repo.py +207 -0
- agent_backbone/services/database/_dependencies_repo.py +57 -0
- agent_backbone/services/database/_events_repo.py +104 -0
- agent_backbone/services/database/_queue_repo.py +150 -0
- agent_backbone/services/database/_repo.py +17 -0
- agent_backbone/services/database/_settings_repo.py +41 -0
- agent_backbone/services/database/_state_repo.py +71 -0
- agent_backbone/services/database/_swarms_repo.py +101 -0
- agent_backbone/services/database/_time.py +20 -0
- agent_backbone/services/database/backbone_db.py +284 -0
- agent_backbone/services/database/base.py +20 -0
- agent_backbone/services/database/engine.py +53 -0
- agent_backbone/services/database/migrations/env.py +83 -0
- agent_backbone/services/database/migrations/script.py.mako +25 -0
- agent_backbone/services/database/migrations/versions/2026_09_02_3fb2fe03898c_initial_schema.py +284 -0
- agent_backbone/services/database/models.py +251 -0
- agent_backbone/services/github/__init__.py +5 -0
- agent_backbone/services/github/interface.py +408 -0
- agent_backbone/services/integrations/__init__.py +11 -0
- agent_backbone/services/integrations/_notify.py +46 -0
- agent_backbone/services/integrations/_registry.py +92 -0
- agent_backbone/services/integrations/base.py +92 -0
- agent_backbone/services/integrations/telegram/__init__.py +5 -0
- agent_backbone/services/integrations/telegram/_commands.py +329 -0
- agent_backbone/services/integrations/telegram/_routing.py +122 -0
- agent_backbone/services/integrations/telegram/_topic_discovery.py +179 -0
- agent_backbone/services/integrations/telegram/_topics.py +118 -0
- agent_backbone/services/integrations/telegram/interface.py +310 -0
- agent_backbone/services/jobs/__init__.py +13 -0
- agent_backbone/services/jobs/copy_mode.py +50 -0
- agent_backbone/services/jobs/escalation.py +243 -0
- agent_backbone/services/jobs/github_poll.py +190 -0
- agent_backbone/services/jobs/monitor.py +133 -0
- agent_backbone/services/jobs/pending.py +140 -0
- agent_backbone/services/jobs/retry.py +173 -0
- agent_backbone/services/routing/__init__.py +42 -0
- agent_backbone/services/routing/_create_notify.py +63 -0
- agent_backbone/services/routing/_dedup.py +32 -0
- agent_backbone/services/routing/_delivery.py +287 -0
- agent_backbone/services/routing/_dependencies.py +98 -0
- agent_backbone/services/routing/_format.py +157 -0
- agent_backbone/services/routing/_ingest.py +138 -0
- agent_backbone/services/routing/_intelligence.py +126 -0
- agent_backbone/services/routing/_lifecycle.py +158 -0
- agent_backbone/services/routing/_priority.py +33 -0
- agent_backbone/services/routing/_resolution.py +39 -0
- agent_backbone/services/routing/_router.py +234 -0
- agent_backbone/services/routing/_targets.py +130 -0
- agent_backbone/services/routing/models.py +50 -0
- agent_backbone/services/runtimes/__init__.py +132 -0
- agent_backbone/services/runtimes/_pane.py +128 -0
- agent_backbone/services/runtimes/aider.py +24 -0
- agent_backbone/services/runtimes/base.py +396 -0
- agent_backbone/services/runtimes/claude.py +117 -0
- agent_backbone/services/runtimes/codex.py +103 -0
- agent_backbone/services/runtimes/deepcode.py +78 -0
- agent_backbone/services/runtimes/gemini.py +55 -0
- agent_backbone/services/runtimes/opencode.py +40 -0
- agent_backbone/services/runtimes/shell.py +30 -0
- agent_backbone/services/scheduler.py +135 -0
- agent_backbone/services/swarm/__init__.py +27 -0
- agent_backbone/services/swarm/_roster.py +86 -0
- agent_backbone/services/swarm/_templates.py +28 -0
- agent_backbone/services/swarm/_worktree.py +81 -0
- agent_backbone/services/swarm/interface.py +350 -0
- agent_backbone/services/swarm/templates/coder.md +16 -0
- agent_backbone/services/swarm/templates/common.md +42 -0
- agent_backbone/services/swarm/templates/coordinator.md +45 -0
- agent_backbone/services/swarm/templates/reviewer.md +19 -0
- agent_backbone/services/swarm/templates/scout.md +17 -0
- agent_backbone/services/swarm/templates/worker.md +12 -0
- agent_backbone/services/terminal/__init__.py +49 -0
- agent_backbone/services/terminal/_copy_mode.py +42 -0
- agent_backbone/services/terminal/_core.py +205 -0
- agent_backbone/services/terminal/_pty.py +337 -0
- agent_backbone/services/terminal/_sessions.py +212 -0
- agent_backbone/templates.py +28 -0
- agent_backbone-2.0.0a0.dist-info/METADATA +183 -0
- agent_backbone-2.0.0a0.dist-info/RECORD +155 -0
- agent_backbone-2.0.0a0.dist-info/WHEEL +4 -0
- agent_backbone-2.0.0a0.dist-info/entry_points.txt +3 -0
- agent_backbone-2.0.0a0.dist-info/licenses/LICENSE +21 -0
|
@@ -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)
|