connectonion 0.5.8__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 (113) hide show
  1. connectonion/__init__.py +78 -0
  2. connectonion/address.py +320 -0
  3. connectonion/agent.py +450 -0
  4. connectonion/announce.py +84 -0
  5. connectonion/asgi.py +287 -0
  6. connectonion/auto_debug_exception.py +181 -0
  7. connectonion/cli/__init__.py +3 -0
  8. connectonion/cli/browser_agent/__init__.py +5 -0
  9. connectonion/cli/browser_agent/browser.py +243 -0
  10. connectonion/cli/browser_agent/prompt.md +107 -0
  11. connectonion/cli/commands/__init__.py +1 -0
  12. connectonion/cli/commands/auth_commands.py +527 -0
  13. connectonion/cli/commands/browser_commands.py +27 -0
  14. connectonion/cli/commands/create.py +511 -0
  15. connectonion/cli/commands/deploy_commands.py +220 -0
  16. connectonion/cli/commands/doctor_commands.py +173 -0
  17. connectonion/cli/commands/init.py +469 -0
  18. connectonion/cli/commands/project_cmd_lib.py +828 -0
  19. connectonion/cli/commands/reset_commands.py +149 -0
  20. connectonion/cli/commands/status_commands.py +168 -0
  21. connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +2010 -0
  22. connectonion/cli/docs/connectonion.md +1256 -0
  23. connectonion/cli/docs.md +123 -0
  24. connectonion/cli/main.py +148 -0
  25. connectonion/cli/templates/meta-agent/README.md +287 -0
  26. connectonion/cli/templates/meta-agent/agent.py +196 -0
  27. connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +9 -0
  28. connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +15 -0
  29. connectonion/cli/templates/meta-agent/prompts/metagent.md +71 -0
  30. connectonion/cli/templates/meta-agent/prompts/think_prompt.md +18 -0
  31. connectonion/cli/templates/minimal/README.md +56 -0
  32. connectonion/cli/templates/minimal/agent.py +40 -0
  33. connectonion/cli/templates/playwright/README.md +118 -0
  34. connectonion/cli/templates/playwright/agent.py +336 -0
  35. connectonion/cli/templates/playwright/prompt.md +102 -0
  36. connectonion/cli/templates/playwright/requirements.txt +3 -0
  37. connectonion/cli/templates/web-research/agent.py +122 -0
  38. connectonion/connect.py +128 -0
  39. connectonion/console.py +539 -0
  40. connectonion/debug_agent/__init__.py +13 -0
  41. connectonion/debug_agent/agent.py +45 -0
  42. connectonion/debug_agent/prompts/debug_assistant.md +72 -0
  43. connectonion/debug_agent/runtime_inspector.py +406 -0
  44. connectonion/debug_explainer/__init__.py +10 -0
  45. connectonion/debug_explainer/explain_agent.py +114 -0
  46. connectonion/debug_explainer/explain_context.py +263 -0
  47. connectonion/debug_explainer/explainer_prompt.md +29 -0
  48. connectonion/debug_explainer/root_cause_analysis_prompt.md +43 -0
  49. connectonion/debugger_ui.py +1039 -0
  50. connectonion/decorators.py +208 -0
  51. connectonion/events.py +248 -0
  52. connectonion/execution_analyzer/__init__.py +9 -0
  53. connectonion/execution_analyzer/execution_analysis.py +93 -0
  54. connectonion/execution_analyzer/execution_analysis_prompt.md +47 -0
  55. connectonion/host.py +579 -0
  56. connectonion/interactive_debugger.py +342 -0
  57. connectonion/llm.py +801 -0
  58. connectonion/llm_do.py +307 -0
  59. connectonion/logger.py +300 -0
  60. connectonion/prompt_files/__init__.py +1 -0
  61. connectonion/prompt_files/analyze_contact.md +62 -0
  62. connectonion/prompt_files/eval_expected.md +12 -0
  63. connectonion/prompt_files/react_evaluate.md +11 -0
  64. connectonion/prompt_files/react_plan.md +16 -0
  65. connectonion/prompt_files/reflect.md +22 -0
  66. connectonion/prompts.py +144 -0
  67. connectonion/relay.py +200 -0
  68. connectonion/static/docs.html +688 -0
  69. connectonion/tool_executor.py +279 -0
  70. connectonion/tool_factory.py +186 -0
  71. connectonion/tool_registry.py +105 -0
  72. connectonion/trust.py +166 -0
  73. connectonion/trust_agents.py +71 -0
  74. connectonion/trust_functions.py +88 -0
  75. connectonion/tui/__init__.py +57 -0
  76. connectonion/tui/divider.py +39 -0
  77. connectonion/tui/dropdown.py +251 -0
  78. connectonion/tui/footer.py +31 -0
  79. connectonion/tui/fuzzy.py +56 -0
  80. connectonion/tui/input.py +278 -0
  81. connectonion/tui/keys.py +35 -0
  82. connectonion/tui/pick.py +130 -0
  83. connectonion/tui/providers.py +155 -0
  84. connectonion/tui/status_bar.py +163 -0
  85. connectonion/usage.py +161 -0
  86. connectonion/useful_events_handlers/__init__.py +16 -0
  87. connectonion/useful_events_handlers/reflect.py +116 -0
  88. connectonion/useful_plugins/__init__.py +20 -0
  89. connectonion/useful_plugins/calendar_plugin.py +163 -0
  90. connectonion/useful_plugins/eval.py +139 -0
  91. connectonion/useful_plugins/gmail_plugin.py +162 -0
  92. connectonion/useful_plugins/image_result_formatter.py +127 -0
  93. connectonion/useful_plugins/re_act.py +78 -0
  94. connectonion/useful_plugins/shell_approval.py +159 -0
  95. connectonion/useful_tools/__init__.py +44 -0
  96. connectonion/useful_tools/diff_writer.py +192 -0
  97. connectonion/useful_tools/get_emails.py +183 -0
  98. connectonion/useful_tools/gmail.py +1596 -0
  99. connectonion/useful_tools/google_calendar.py +613 -0
  100. connectonion/useful_tools/memory.py +380 -0
  101. connectonion/useful_tools/microsoft_calendar.py +604 -0
  102. connectonion/useful_tools/outlook.py +488 -0
  103. connectonion/useful_tools/send_email.py +205 -0
  104. connectonion/useful_tools/shell.py +97 -0
  105. connectonion/useful_tools/slash_command.py +201 -0
  106. connectonion/useful_tools/terminal.py +285 -0
  107. connectonion/useful_tools/todo_list.py +241 -0
  108. connectonion/useful_tools/web_fetch.py +216 -0
  109. connectonion/xray.py +467 -0
  110. connectonion-0.5.8.dist-info/METADATA +741 -0
  111. connectonion-0.5.8.dist-info/RECORD +113 -0
  112. connectonion-0.5.8.dist-info/WHEEL +4 -0
  113. connectonion-0.5.8.dist-info/entry_points.txt +3 -0
connectonion/host.py ADDED
@@ -0,0 +1,579 @@
1
+ """Host an agent over HTTP/WebSocket.
2
+
3
+ Trust enforcement happens at the host level, not in the Agent.
4
+ This provides clean separation: Agent does work, host controls access.
5
+
6
+ Trust parameter accepts three forms:
7
+ 1. Level (string): "open", "careful", "strict"
8
+ 2. Policy (string): Natural language or file path
9
+ 3. Agent: Custom Agent instance for verification
10
+
11
+ All forms create a trust agent behind the scenes.
12
+ """
13
+ import hashlib
14
+ import json
15
+ import logging
16
+ import os
17
+ import time
18
+ import uuid
19
+ from pathlib import Path
20
+ from typing import Optional, Union
21
+
22
+ from pydantic import BaseModel
23
+ from rich.console import Console
24
+ from rich.panel import Panel
25
+
26
+ from .asgi import create_app as asgi_create_app
27
+ from .trust import create_trust_agent, get_default_trust_level, TRUST_LEVELS
28
+
29
+
30
+ def get_default_trust() -> str:
31
+ """Get default trust based on environment.
32
+
33
+ Returns:
34
+ Trust level based on CONNECTONION_ENV, defaults to 'careful'
35
+ """
36
+ return get_default_trust_level() or "careful"
37
+
38
+
39
+ # === Types ===
40
+
41
+ class Session(BaseModel):
42
+ """Session record for tracking agent requests.
43
+
44
+ Uses Pydantic BaseModel for:
45
+ - Native JSON serialization via .model_dump()
46
+ - Type validation
47
+ - API response compatibility
48
+ """
49
+ session_id: str
50
+ status: str
51
+ prompt: str
52
+ result: Optional[str] = None
53
+ created: Optional[float] = None
54
+ expires: Optional[float] = None
55
+ duration_ms: Optional[int] = None
56
+
57
+
58
+ # === Storage ===
59
+
60
+ class SessionStorage:
61
+ """JSONL file storage. Append-only, last entry wins."""
62
+
63
+ def __init__(self, path: str = ".co/session_results.jsonl"):
64
+ self.path = Path(path)
65
+ self.path.parent.mkdir(exist_ok=True)
66
+
67
+ def save(self, session: Session):
68
+ with open(self.path, "a") as f:
69
+ f.write(session.model_dump_json() + "\n")
70
+
71
+ def get(self, session_id: str) -> Session | None:
72
+ if not self.path.exists():
73
+ return None
74
+ now = time.time()
75
+ with open(self.path) as f:
76
+ lines = f.readlines()
77
+ for line in reversed(lines):
78
+ data = json.loads(line)
79
+ if data["session_id"] == session_id:
80
+ session = Session(**data)
81
+ # Return if running or not expired
82
+ if session.status == "running" or not session.expires or session.expires > now:
83
+ return session
84
+ return None # Expired
85
+ return None
86
+
87
+ def list(self) -> list[Session]:
88
+ if not self.path.exists():
89
+ return []
90
+ sessions = {}
91
+ now = time.time()
92
+ with open(self.path) as f:
93
+ for line in f:
94
+ data = json.loads(line)
95
+ sessions[data["session_id"]] = Session(**data)
96
+ # Filter out expired non-running sessions
97
+ valid = [s for s in sessions.values()
98
+ if s.status == "running" or not s.expires or s.expires > now]
99
+ # Sort by created desc (newest first)
100
+ return sorted(valid, key=lambda s: s.created or 0, reverse=True)
101
+
102
+
103
+ # === Handlers (pure functions) ===
104
+
105
+ def input_handler(agent, storage: SessionStorage, prompt: str, result_ttl: int,
106
+ session: dict | None = None) -> dict:
107
+ """POST /input
108
+
109
+ Args:
110
+ agent: The agent to process the request
111
+ storage: SessionStorage for persisting results
112
+ prompt: The user's prompt
113
+ result_ttl: How long to keep the result on server
114
+ session: Optional conversation session for continuation
115
+ """
116
+ now = time.time()
117
+
118
+ # Get or generate session_id
119
+ session_id = session.get('session_id') if session else None
120
+ if not session_id:
121
+ session_id = str(uuid.uuid4())
122
+ # If session was provided but missing session_id, add it
123
+ if session:
124
+ session['session_id'] = session_id
125
+
126
+ # Create storage record
127
+ record = Session(
128
+ session_id=session_id,
129
+ status="running",
130
+ prompt=prompt,
131
+ created=now,
132
+ expires=now + result_ttl,
133
+ )
134
+ storage.save(record)
135
+
136
+ start = time.time()
137
+ result = agent.input(prompt, session=session)
138
+ duration_ms = int((time.time() - start) * 1000)
139
+
140
+ record.status = "done"
141
+ record.result = result
142
+ record.duration_ms = duration_ms
143
+ storage.save(record)
144
+
145
+ # Return result with updated session for client to continue conversation
146
+ return {
147
+ "session_id": session_id,
148
+ "status": "done",
149
+ "result": result,
150
+ "duration_ms": duration_ms,
151
+ "session": agent.current_session
152
+ }
153
+
154
+
155
+ def session_handler(storage: SessionStorage, session_id: str) -> dict | None:
156
+ """GET /sessions/{id}"""
157
+ session = storage.get(session_id)
158
+ return session.model_dump() if session else None
159
+
160
+
161
+ def sessions_handler(storage: SessionStorage) -> dict:
162
+ """GET /sessions"""
163
+ return {"sessions": [s.model_dump() for s in storage.list()]}
164
+
165
+
166
+ def health_handler(agent, start_time: float) -> dict:
167
+ """GET /health"""
168
+ return {"status": "healthy", "agent": agent.name, "uptime": int(time.time() - start_time)}
169
+
170
+
171
+ def info_handler(agent, trust: str) -> dict:
172
+ """GET /info"""
173
+ from . import __version__
174
+ tools = agent.tools.list_names() if hasattr(agent.tools, "list_names") else []
175
+ return {
176
+ "name": agent.name,
177
+ "address": get_agent_address(agent),
178
+ "tools": tools,
179
+ "trust": trust,
180
+ "version": __version__,
181
+ }
182
+
183
+
184
+ # === Auth Helpers ===
185
+
186
+ # Signature expiry window (5 minutes)
187
+ SIGNATURE_EXPIRY_SECONDS = 300
188
+
189
+
190
+ def verify_signature(payload: dict, signature: str, public_key: str) -> bool:
191
+ """Verify Ed25519 signature.
192
+
193
+ Args:
194
+ payload: The payload that was signed
195
+ signature: Hex-encoded signature (with or without 0x prefix)
196
+ public_key: Hex-encoded public key (with or without 0x prefix)
197
+
198
+ Returns:
199
+ True if signature is valid, False otherwise
200
+ """
201
+ from nacl.signing import VerifyKey
202
+ from nacl.exceptions import BadSignatureError
203
+
204
+ # Remove 0x prefix if present
205
+ sig_hex = signature[2:] if signature.startswith("0x") else signature
206
+ key_hex = public_key[2:] if public_key.startswith("0x") else public_key
207
+
208
+ # Canonicalize payload (deterministic JSON)
209
+ canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
210
+
211
+ try:
212
+ verify_key = VerifyKey(bytes.fromhex(key_hex))
213
+ verify_key.verify(canonical.encode(), bytes.fromhex(sig_hex))
214
+ return True
215
+ except (BadSignatureError, ValueError):
216
+ # BadSignatureError: invalid signature
217
+ # ValueError: invalid hex encoding
218
+ return False
219
+
220
+
221
+ def extract_and_authenticate(data: dict, trust, *, blacklist=None, whitelist=None, agent_address=None):
222
+ """Extract prompt and authenticate request.
223
+
224
+ ALL requests must be signed - this is a protocol requirement.
225
+
226
+ Required format (Ed25519 signed):
227
+ {
228
+ "payload": {"prompt": "...", "to": "0xAgentAddress", "timestamp": 123},
229
+ "from": "0xCallerPublicKey",
230
+ "signature": "0xEd25519Signature..."
231
+ }
232
+
233
+ Trust levels control additional policies AFTER signature verification:
234
+ - "open": Any valid signer allowed
235
+ - "careful": Warnings for unknown signers (default)
236
+ - "strict": Whitelist only
237
+ - Custom policy/Agent: LLM evaluation
238
+
239
+ Returns: (prompt, identity, sig_valid, error)
240
+ """
241
+ # Protocol requirement: ALL requests must be signed
242
+ if "payload" not in data or "signature" not in data:
243
+ return None, None, False, "unauthorized: signed request required"
244
+
245
+ # Verify signature (protocol level - always required)
246
+ prompt, identity, error = _authenticate_signed(
247
+ data, blacklist=blacklist, whitelist=whitelist, agent_address=agent_address
248
+ )
249
+ if error:
250
+ return prompt, identity, False, error
251
+
252
+ # Trust level: additional policies AFTER signature verification
253
+ if trust == "strict" and whitelist and identity not in whitelist:
254
+ return None, identity, True, "forbidden: not in whitelist"
255
+
256
+ # Custom trust policy/agent evaluation
257
+ if is_custom_trust(trust):
258
+ trust_agent = create_trust_agent(trust)
259
+ accepted, reason = evaluate_with_trust_agent(trust_agent, prompt, identity, True)
260
+ if not accepted:
261
+ return None, identity, True, f"rejected: {reason}"
262
+
263
+ return prompt, identity, True, None
264
+
265
+
266
+ def _authenticate_signed(data: dict, *, blacklist=None, whitelist=None, agent_address=None):
267
+ """Authenticate signed request with Ed25519 - ALWAYS REQUIRED.
268
+
269
+ Protocol-level signature verification. All requests must be signed.
270
+
271
+ Returns: (prompt, identity, error) - error is None on success
272
+ """
273
+ payload = data.get("payload", {})
274
+ identity = data.get("from")
275
+ signature = data.get("signature")
276
+
277
+ prompt = payload.get("prompt", "")
278
+ timestamp = payload.get("timestamp")
279
+ to_address = payload.get("to")
280
+
281
+ # Check blacklist first
282
+ if blacklist and identity in blacklist:
283
+ return None, identity, "forbidden: blacklisted"
284
+
285
+ # Check whitelist (bypass signature check - trusted caller)
286
+ if whitelist and identity in whitelist:
287
+ return prompt, identity, None
288
+
289
+ # Validate required fields
290
+ if not identity:
291
+ return None, None, "unauthorized: 'from' field required"
292
+ if not signature:
293
+ return None, identity, "unauthorized: signature required"
294
+ if not timestamp:
295
+ return None, identity, "unauthorized: timestamp required in payload"
296
+
297
+ # Check timestamp expiry (5 minute window)
298
+ now = time.time()
299
+ if abs(now - timestamp) > SIGNATURE_EXPIRY_SECONDS:
300
+ return None, identity, "unauthorized: signature expired"
301
+
302
+ # Optionally verify 'to' matches agent address
303
+ if agent_address and to_address and to_address != agent_address:
304
+ return None, identity, "unauthorized: wrong recipient"
305
+
306
+ # Verify signature
307
+ if not verify_signature(payload, signature, identity):
308
+ return None, identity, "unauthorized: invalid signature"
309
+
310
+ return prompt, identity, None
311
+
312
+
313
+ def get_agent_address(agent) -> str:
314
+ """Generate deterministic address from agent name."""
315
+ h = hashlib.sha256(agent.name.encode()).hexdigest()
316
+ return f"0x{h[:40]}"
317
+
318
+
319
+ def evaluate_with_trust_agent(trust_agent, prompt: str, identity: str, sig_valid: bool) -> tuple[bool, str]:
320
+ """Evaluate request using a custom trust agent (policy or Agent).
321
+
322
+ Only called when trust is a policy string or custom Agent - NOT for simple levels.
323
+
324
+ Args:
325
+ trust_agent: The trust agent created from policy or custom Agent
326
+ prompt: The request prompt
327
+ identity: The requester's identity/address
328
+ sig_valid: Whether the signature is valid
329
+
330
+ Returns:
331
+ (accepted, reason) tuple
332
+ """
333
+ from pydantic import BaseModel
334
+ from .llm_do import llm_do
335
+
336
+ class TrustDecision(BaseModel):
337
+ accept: bool
338
+ reason: str
339
+
340
+ request_info = f"""Evaluate this request:
341
+ - prompt: {prompt[:200]}{'...' if len(prompt) > 200 else ''}
342
+ - identity: {identity or 'anonymous'}
343
+ - signature_valid: {sig_valid}"""
344
+
345
+ decision = llm_do(
346
+ request_info,
347
+ output=TrustDecision,
348
+ system_prompt=trust_agent.system_prompt,
349
+ )
350
+ return decision.accept, decision.reason
351
+
352
+
353
+ def is_custom_trust(trust) -> bool:
354
+ """Check if trust needs a custom agent (policy or Agent, not a level)."""
355
+ if not isinstance(trust, str):
356
+ return True # It's an Agent
357
+ return trust not in TRUST_LEVELS # It's a policy string
358
+
359
+
360
+ # === Admin Handlers ===
361
+
362
+ def admin_logs_handler(agent_name: str) -> dict:
363
+ """GET /admin/logs - return plain text activity log file."""
364
+ log_path = Path(f".co/logs/{agent_name}.log")
365
+ if log_path.exists():
366
+ return {"content": log_path.read_text()}
367
+ return {"error": "No logs found"}
368
+
369
+
370
+ def admin_sessions_handler() -> dict:
371
+ """GET /admin/sessions - return all activity sessions as JSON array."""
372
+ import yaml
373
+ sessions_dir = Path(".co/sessions")
374
+ if not sessions_dir.exists():
375
+ return {"sessions": []}
376
+
377
+ sessions = []
378
+ for session_file in sessions_dir.glob("*.yaml"):
379
+ with open(session_file) as f:
380
+ session_data = yaml.safe_load(f)
381
+ if session_data:
382
+ sessions.append(session_data)
383
+
384
+ # Sort by created date descending (newest first)
385
+ sessions.sort(key=lambda s: s.get("created", ""), reverse=True)
386
+ return {"sessions": sessions}
387
+
388
+
389
+ # === Entry Point ===
390
+
391
+ def _create_handlers(agent, result_ttl: int):
392
+ """Create handler dict for ASGI app."""
393
+ return {
394
+ "input": lambda storage, prompt, ttl, session=None: input_handler(agent, storage, prompt, ttl, session),
395
+ "session": session_handler,
396
+ "sessions": sessions_handler,
397
+ "health": lambda start_time: health_handler(agent, start_time),
398
+ "info": lambda trust: info_handler(agent, trust),
399
+ "auth": extract_and_authenticate,
400
+ "ws_input": agent.input,
401
+ # Admin endpoints (auth required via OPENONION_API_KEY)
402
+ "admin_logs": lambda: admin_logs_handler(agent.name),
403
+ "admin_sessions": admin_sessions_handler,
404
+ }
405
+
406
+
407
+ def _start_relay_background(agent, relay_url: str, addr_data: dict):
408
+ """Start relay connection in background thread.
409
+
410
+ The relay connection runs alongside the HTTP server, allowing the agent
411
+ to be discovered via P2P network while also serving HTTP requests.
412
+ """
413
+ import asyncio
414
+ import threading
415
+ from . import announce, relay
416
+
417
+ # Create ANNOUNCE message
418
+ summary = agent.system_prompt[:1000] if agent.system_prompt else f"{agent.name} agent"
419
+ announce_msg = announce.create_announce_message(addr_data, summary, endpoints=[])
420
+
421
+ # Task handler that routes to agent.input()
422
+ async def task_handler(prompt: str) -> str:
423
+ return agent.input(prompt)
424
+
425
+ async def relay_loop():
426
+ ws = await relay.connect(relay_url)
427
+ await relay.serve_loop(ws, announce_msg, task_handler)
428
+
429
+ def run():
430
+ asyncio.run(relay_loop())
431
+
432
+ thread = threading.Thread(target=run, daemon=True, name="relay-connection")
433
+ thread.start()
434
+ return thread
435
+
436
+
437
+ def host(
438
+ agent,
439
+ port: int = None,
440
+ trust: Union[str, "Agent"] = "careful",
441
+ result_ttl: int = 86400,
442
+ workers: int = 1,
443
+ reload: bool = False,
444
+ *,
445
+ relay_url: str = "wss://oo.openonion.ai/ws/announce",
446
+ blacklist: list | None = None,
447
+ whitelist: list | None = None,
448
+ ):
449
+ """
450
+ Host an agent over HTTP/WebSocket with optional P2P relay discovery.
451
+
452
+ Args:
453
+ agent: Agent to host
454
+ port: HTTP port (default: PORT env var or 8000)
455
+ trust: Trust level, policy, or Agent:
456
+ - Level: "open", "careful", "strict"
457
+ - Policy: Natural language or file path
458
+ - Agent: Custom trust agent
459
+ result_ttl: How long to keep results on server in seconds (default 24h)
460
+ workers: Number of worker processes
461
+ reload: Auto-reload on code changes
462
+ relay_url: P2P relay URL (default: production relay)
463
+ - Set to None to disable relay
464
+ blacklist: Blocked identities
465
+ whitelist: Allowed identities
466
+
467
+ Endpoints:
468
+ POST /input - Submit prompt, get result
469
+ GET /sessions/{id} - Get session by ID
470
+ GET /sessions - List all sessions
471
+ GET /health - Health check
472
+ GET /info - Agent info
473
+ WS /ws - WebSocket
474
+ GET /logs - Activity log (requires OPENONION_API_KEY)
475
+ GET /logs/sessions - Activity sessions (requires OPENONION_API_KEY)
476
+ """
477
+ import uvicorn
478
+ from . import address
479
+
480
+ # Use PORT env var if port not specified (for container deployments)
481
+ if port is None:
482
+ port = int(os.environ.get("PORT", 8000))
483
+
484
+ # Load or generate agent identity
485
+ co_dir = Path.cwd() / '.co'
486
+ addr_data = address.load(co_dir)
487
+
488
+ if addr_data is None:
489
+ addr_data = address.generate()
490
+ address.save(addr_data, co_dir)
491
+
492
+ agent_address = addr_data['address']
493
+
494
+ storage = SessionStorage()
495
+ handlers = _create_handlers(agent, result_ttl)
496
+ app = asgi_create_app(
497
+ handlers=handlers,
498
+ storage=storage,
499
+ trust=trust,
500
+ result_ttl=result_ttl,
501
+ blacklist=blacklist,
502
+ whitelist=whitelist,
503
+ )
504
+
505
+ # Start relay connection in background (if enabled)
506
+ if relay_url:
507
+ _start_relay_background(agent, relay_url, addr_data)
508
+
509
+ # Display startup info
510
+ relay_status = f"[green]✓[/] {relay_url}" if relay_url else "[dim]disabled[/]"
511
+ Console().print(Panel(
512
+ f"[bold]POST[/] http://localhost:{port}/input\n"
513
+ f"[dim]GET /sessions/{{id}} · /sessions · /health · /info[/]\n"
514
+ f"[dim]WS ws://localhost:{port}/ws\n"
515
+ f"[dim]UI http://localhost:{port}/docs[/]\n\n"
516
+ f"[bold]Address:[/] {agent_address}\n"
517
+ f"[bold]Relay:[/] {relay_status}",
518
+ title=f"[green]Agent '{agent.name}'[/]"
519
+ ))
520
+
521
+ uvicorn.run(app, host="0.0.0.0", port=port, workers=workers, reload=reload, log_level="warning")
522
+
523
+
524
+ # === Export ASGI App ===
525
+
526
+ def _make_app(agent, trust: Union[str, "Agent"] = "careful", result_ttl=86400, *, blacklist=None, whitelist=None):
527
+ """Create ASGI app for external uvicorn/gunicorn usage.
528
+
529
+ Args:
530
+ agent: Agent to host
531
+ trust: Trust level, policy, or Agent
532
+ result_ttl: How long to keep results on server in seconds
533
+ blacklist: Blocked identities
534
+ whitelist: Allowed identities
535
+
536
+ Usage:
537
+ # myagent.py
538
+ from connectonion import Agent, host
539
+ agent = Agent("translator", tools=[translate])
540
+ app = host.app(agent)
541
+
542
+ # Then run with:
543
+ # uvicorn myagent:app --workers 4
544
+ """
545
+ storage = SessionStorage()
546
+ handlers = _create_handlers(agent, result_ttl)
547
+ return asgi_create_app(
548
+ handlers=handlers,
549
+ storage=storage,
550
+ trust=trust,
551
+ result_ttl=result_ttl,
552
+ blacklist=blacklist,
553
+ whitelist=whitelist,
554
+ )
555
+
556
+
557
+ # Attach app factory to host function
558
+ host.app = _make_app
559
+
560
+
561
+ # Backward-compatible create_app (use host.app() for new code)
562
+ def create_app_compat(agent, storage, trust="careful", result_ttl=86400, *, blacklist=None, whitelist=None):
563
+ """Create ASGI app (backward-compatible wrapper).
564
+
565
+ Prefer using host.app(agent) for new code.
566
+ """
567
+ handlers = _create_handlers(agent, result_ttl)
568
+ return asgi_create_app(
569
+ handlers=handlers,
570
+ storage=storage,
571
+ trust=trust,
572
+ result_ttl=result_ttl,
573
+ blacklist=blacklist,
574
+ whitelist=whitelist,
575
+ )
576
+
577
+
578
+ # Re-export for backward compatibility
579
+ create_app = create_app_compat