connectonion 0.5.3__tar.gz → 0.5.6__tar.gz

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 (131) hide show
  1. {connectonion-0.5.3 → connectonion-0.5.6}/PKG-INFO +1 -1
  2. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/__init__.py +1 -1
  3. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/asgi.py +47 -1
  4. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/commands/deploy_commands.py +110 -16
  5. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/host.py +101 -81
  6. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/usage.py +10 -5
  7. {connectonion-0.5.3 → connectonion-0.5.6}/pyproject.toml +1 -1
  8. {connectonion-0.5.3 → connectonion-0.5.6}/.gitignore +0 -0
  9. {connectonion-0.5.3 → connectonion-0.5.6}/README.md +0 -0
  10. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/address.py +0 -0
  11. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/agent.py +0 -0
  12. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/announce.py +0 -0
  13. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/auto_debug_exception.py +0 -0
  14. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/__init__.py +0 -0
  15. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/browser_agent/__init__.py +0 -0
  16. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/browser_agent/browser.py +0 -0
  17. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/browser_agent/prompt.md +0 -0
  18. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/commands/__init__.py +0 -0
  19. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/commands/auth_commands.py +0 -0
  20. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/commands/browser_commands.py +0 -0
  21. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/commands/create.py +0 -0
  22. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/commands/doctor_commands.py +0 -0
  23. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/commands/init.py +0 -0
  24. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/commands/project_cmd_lib.py +0 -0
  25. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/commands/reset_commands.py +0 -0
  26. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/commands/status_commands.py +0 -0
  27. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +0 -0
  28. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/docs/connectonion.md +0 -0
  29. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/docs.md +0 -0
  30. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/main.py +0 -0
  31. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/templates/meta-agent/README.md +0 -0
  32. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/templates/meta-agent/agent.py +0 -0
  33. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +0 -0
  34. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +0 -0
  35. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/templates/meta-agent/prompts/metagent.md +0 -0
  36. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/templates/meta-agent/prompts/think_prompt.md +0 -0
  37. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/templates/minimal/README.md +0 -0
  38. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/templates/minimal/agent.py +0 -0
  39. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/templates/playwright/README.md +0 -0
  40. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/templates/playwright/agent.py +0 -0
  41. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/templates/playwright/prompt.md +0 -0
  42. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/templates/playwright/requirements.txt +0 -0
  43. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/cli/templates/web-research/agent.py +0 -0
  44. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/connect.py +0 -0
  45. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/console.py +0 -0
  46. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/debug_agent/__init__.py +0 -0
  47. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/debug_agent/agent.py +0 -0
  48. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/debug_agent/prompts/debug_assistant.md +0 -0
  49. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/debug_agent/runtime_inspector.py +0 -0
  50. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/debug_explainer/__init__.py +0 -0
  51. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/debug_explainer/explain_agent.py +0 -0
  52. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/debug_explainer/explain_context.py +0 -0
  53. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/debug_explainer/explainer_prompt.md +0 -0
  54. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/debug_explainer/root_cause_analysis_prompt.md +0 -0
  55. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/debugger_ui.py +0 -0
  56. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/decorators.py +0 -0
  57. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/events.py +0 -0
  58. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/execution_analyzer/__init__.py +0 -0
  59. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/execution_analyzer/execution_analysis.py +0 -0
  60. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/execution_analyzer/execution_analysis_prompt.md +0 -0
  61. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/interactive_debugger.py +0 -0
  62. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/llm.py +0 -0
  63. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/llm_do.py +0 -0
  64. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/logger.py +0 -0
  65. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/prompt_files/__init__.py +0 -0
  66. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/prompt_files/analyze_contact.md +0 -0
  67. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/prompt_files/eval_expected.md +0 -0
  68. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/prompt_files/react_evaluate.md +0 -0
  69. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/prompt_files/react_plan.md +0 -0
  70. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/prompt_files/reflect.md +0 -0
  71. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/prompts.py +0 -0
  72. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/relay.py +0 -0
  73. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/static/docs.html +0 -0
  74. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/tool_executor.py +0 -0
  75. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/tool_factory.py +0 -0
  76. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/tool_registry.py +0 -0
  77. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/trust.py +0 -0
  78. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/trust_agents.py +0 -0
  79. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/trust_functions.py +0 -0
  80. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/tui/__init__.py +0 -0
  81. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/tui/divider.py +0 -0
  82. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/tui/dropdown.py +0 -0
  83. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/tui/footer.py +0 -0
  84. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/tui/fuzzy.py +0 -0
  85. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/tui/input.py +0 -0
  86. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/tui/keys.py +0 -0
  87. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/tui/pick.py +0 -0
  88. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/tui/providers.py +0 -0
  89. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/tui/status_bar.py +0 -0
  90. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_events_handlers/__init__.py +0 -0
  91. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_events_handlers/reflect.py +0 -0
  92. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_plugins/__init__.py +0 -0
  93. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_plugins/calendar_plugin.py +0 -0
  94. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_plugins/eval.py +0 -0
  95. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_plugins/gmail_plugin.py +0 -0
  96. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_plugins/image_result_formatter.py +0 -0
  97. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_plugins/re_act.py +0 -0
  98. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_plugins/shell_approval.py +0 -0
  99. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_tools/__init__.py +0 -0
  100. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_tools/diff_writer.py +0 -0
  101. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_tools/get_emails.py +0 -0
  102. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_tools/gmail.py +0 -0
  103. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_tools/google_calendar.py +0 -0
  104. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_tools/memory.py +0 -0
  105. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_tools/microsoft_calendar.py +0 -0
  106. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_tools/outlook.py +0 -0
  107. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_tools/send_email.py +0 -0
  108. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_tools/shell.py +0 -0
  109. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_tools/slash_command.py +0 -0
  110. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_tools/terminal.py +0 -0
  111. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_tools/todo_list.py +0 -0
  112. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/useful_tools/web_fetch.py +0 -0
  113. {connectonion-0.5.3 → connectonion-0.5.6}/connectonion/xray.py +0 -0
  114. {connectonion-0.5.3 → connectonion-0.5.6}/docs/README.md +0 -0
  115. {connectonion-0.5.3 → connectonion-0.5.6}/docs/cli/README.md +0 -0
  116. {connectonion-0.5.3 → connectonion-0.5.6}/docs/debug/README.md +0 -0
  117. {connectonion-0.5.3 → connectonion-0.5.6}/docs/integrations/README.md +0 -0
  118. {connectonion-0.5.3 → connectonion-0.5.6}/docs/network/README.md +0 -0
  119. {connectonion-0.5.3 → connectonion-0.5.6}/docs/templates/README.md +0 -0
  120. {connectonion-0.5.3 → connectonion-0.5.6}/docs/tui/README.md +0 -0
  121. {connectonion-0.5.3 → connectonion-0.5.6}/docs/useful_plugins/README.md +0 -0
  122. {connectonion-0.5.3 → connectonion-0.5.6}/docs/useful_tools/README.md +0 -0
  123. {connectonion-0.5.3 → connectonion-0.5.6}/examples/README.md +0 -0
  124. {connectonion-0.5.3 → connectonion-0.5.6}/examples/browser-agent/README.md +0 -0
  125. {connectonion-0.5.3 → connectonion-0.5.6}/examples/email-agent/README.md +0 -0
  126. {connectonion-0.5.3 → connectonion-0.5.6}/examples/simple-agent/README.md +0 -0
  127. {connectonion-0.5.3 → connectonion-0.5.6}/prompts/README.md +0 -0
  128. {connectonion-0.5.3 → connectonion-0.5.6}/prompts/formats/README.md +0 -0
  129. {connectonion-0.5.3 → connectonion-0.5.6}/tests/README.md +0 -0
  130. {connectonion-0.5.3 → connectonion-0.5.6}/tests/cli/README.md +0 -0
  131. {connectonion-0.5.3 → connectonion-0.5.6}/tests/cli/aws/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: connectonion
3
- Version: 0.5.3
3
+ Version: 0.5.6
4
4
  Summary: A simple Python framework for creating AI agents with behavior tracking
5
5
  Project-URL: Homepage, https://github.com/openonion/connectonion
6
6
  Project-URL: Documentation, https://docs.connectonion.com
@@ -1,6 +1,6 @@
1
1
  """ConnectOnion - A simple agent framework with behavior tracking."""
2
2
 
3
- __version__ = "0.5.3"
3
+ __version__ = "0.5.6"
4
4
 
5
5
  # Auto-load .env files for the entire framework
6
6
  from dotenv import load_dotenv
@@ -6,9 +6,24 @@ requests. Separated from host.py for better testing and smaller file size.
6
6
  Design decision: Raw ASGI instead of Starlette/FastAPI for full protocol control.
7
7
  See: docs/design-decisions/022-raw-asgi-implementation.md
8
8
  """
9
+ import hmac
9
10
  import json
11
+ import os
10
12
  from pathlib import Path
11
13
 
14
+ from pydantic import BaseModel
15
+
16
+
17
+ def _json_default(obj):
18
+ """Handle non-serializable objects like Pydantic models.
19
+
20
+ This enables native JSON serialization for Pydantic BaseModel instances
21
+ nested in API response dicts, following FastAPI's pattern.
22
+ """
23
+ if isinstance(obj, BaseModel):
24
+ return obj.model_dump()
25
+ raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
26
+
12
27
 
13
28
  async def read_body(receive) -> bytes:
14
29
  """Read complete request body from ASGI receive."""
@@ -23,7 +38,7 @@ async def read_body(receive) -> bytes:
23
38
 
24
39
  async def send_json(send, data: dict, status: int = 200):
25
40
  """Send JSON response via ASGI send."""
26
- body = json.dumps(data).encode()
41
+ body = json.dumps(data, default=_json_default).encode()
27
42
  await send({"type": "http.response.start", "status": status,
28
43
  "headers": [[b"content-type", b"application/json"]]})
29
44
  await send({"type": "http.response.body", "body": body})
@@ -39,6 +54,13 @@ async def send_html(send, html: bytes, status: int = 200):
39
54
  await send({"type": "http.response.body", "body": html})
40
55
 
41
56
 
57
+ async def send_text(send, text: str, status: int = 200):
58
+ """Send plain text response via ASGI send."""
59
+ await send({"type": "http.response.start", "status": status,
60
+ "headers": [[b"content-type", b"text/plain; charset=utf-8"]]})
61
+ await send({"type": "http.response.body", "body": text.encode()})
62
+
63
+
42
64
  async def handle_http(
43
65
  scope,
44
66
  receive,
@@ -68,6 +90,30 @@ async def handle_http(
68
90
  """
69
91
  method, path = scope["method"], scope["path"]
70
92
 
93
+ # Admin endpoints require API key auth
94
+ if path.startswith("/admin"):
95
+ headers = dict(scope.get("headers", []))
96
+ auth = headers.get(b"authorization", b"").decode()
97
+ expected = os.environ.get("OPENONION_API_KEY", "")
98
+ if not expected or not auth.startswith("Bearer ") or not hmac.compare_digest(auth[7:], expected):
99
+ await send_json(send, {"error": "unauthorized"}, 401)
100
+ return
101
+
102
+ if method == "GET" and path == "/admin/logs":
103
+ result = handlers["admin_logs"]()
104
+ if "error" in result:
105
+ await send_json(send, result, 404)
106
+ else:
107
+ await send_text(send, result["content"])
108
+ return
109
+
110
+ if method == "GET" and path == "/admin/sessions":
111
+ await send_json(send, handlers["admin_sessions"]())
112
+ return
113
+
114
+ await send_json(send, {"error": "not found"}, 404)
115
+ return
116
+
71
117
  if method == "POST" and path == "/input":
72
118
  body = await read_body(receive)
73
119
  try:
@@ -5,11 +5,13 @@ LLM-Note:
5
5
  Data flow: handle_deploy() → validates git repo and .co/config.toml → _get_api_key() loads OPENONION_API_KEY → reads config.toml for project name and secrets path → dotenv_values() loads secrets from .env → git archive creates tarball of HEAD → POST to /api/v1/deploy with tarball + project_name + secrets → polls /api/v1/deploy/{id}/status until running/error → displays agent URL
6
6
  State/Effects: creates temporary tarball file in tempdir | reads .co/config.toml, .env files | makes network POST request | prints progress to stdout via rich.Console | does not modify project files
7
7
  Integration: exposes handle_deploy() for CLI | expects git repo with .co/config.toml containing project.name, project.secrets, deploy.entrypoint | uses Bearer token auth | returns void (prints results)
8
- Performance: git archive is fast | network timeout 60s for upload, 10s for status checks | polls every 3s up to 100 times (~5 min)
8
+ Performance: git archive is fast | network timeout 600s for upload+build, 10s for status checks | polls every 3s up to 100 times (~5 min)
9
9
  Errors: fails if not git repo | fails if not ConnectOnion project (.co/config.toml missing) | fails if no API key | prints backend error messages
10
10
  """
11
11
 
12
+ import json
12
13
  import os
14
+ import re
13
15
  import subprocess
14
16
  import tempfile
15
17
  import time
@@ -24,6 +26,30 @@ console = Console()
24
26
  API_BASE = "https://oo.openonion.ai"
25
27
 
26
28
 
29
+ def _check_host_export(entrypoint: str) -> bool:
30
+ """Check if entrypoint file exports an ASGI app via host().
31
+
32
+ Looks for patterns like:
33
+ - host(agent)
34
+ - host(my_agent)
35
+ - from connectonion import host
36
+ """
37
+ entrypoint_path = Path(entrypoint)
38
+ if not entrypoint_path.exists():
39
+ return False
40
+
41
+ content = entrypoint_path.read_text()
42
+
43
+ # Check for host() call pattern
44
+ # Matches: host(agent), host(my_agent), host( agent ), etc.
45
+ host_call_pattern = r'\bhost\s*\([^)]+\)'
46
+
47
+ if re.search(host_call_pattern, content):
48
+ return True
49
+
50
+ return False
51
+
52
+
27
53
  def _get_api_key() -> str:
28
54
  """Get OPENONION_API_KEY from env or .env files."""
29
55
  if api_key := os.getenv("OPENONION_API_KEY"):
@@ -65,6 +91,26 @@ def handle_deploy():
65
91
  secrets_path = config.get("project", {}).get("secrets", ".env")
66
92
  entrypoint = config.get("deploy", {}).get("entrypoint", "agent.py")
67
93
 
94
+ # Validate entrypoint exists
95
+ if not Path(entrypoint).exists():
96
+ console.print(f"[red]Entrypoint not found: {entrypoint}[/red]")
97
+ console.print("[dim]Set entrypoint in .co/config.toml under [deploy][/dim]")
98
+ return
99
+
100
+ # Validate entrypoint exports ASGI app via host()
101
+ if not _check_host_export(entrypoint):
102
+ console.print(f"[red]Entrypoint '{entrypoint}' does not export an ASGI app.[/red]")
103
+ console.print()
104
+ console.print("[yellow]To deploy, your agent must call host():[/yellow]")
105
+ console.print()
106
+ console.print(" [cyan]from connectonion import Agent, host[/cyan]")
107
+ console.print()
108
+ console.print(" [cyan]agent = Agent('my-agent', ...)[/cyan]")
109
+ console.print(" [cyan]host(agent) # Starts HTTP server[/cyan]")
110
+ console.print()
111
+ console.print("[dim]See: https://docs.connectonion.com/deploy[/dim]")
112
+ return
113
+
68
114
  # Load secrets from .env
69
115
  secrets = dotenv_values(secrets_path) if Path(secrets_path).exists() else {}
70
116
 
@@ -76,7 +122,17 @@ def handle_deploy():
76
122
  check=True,
77
123
  )
78
124
 
125
+ # Show package size
126
+ tarball_size = tarball_path.stat().st_size
127
+ if tarball_size < 1024:
128
+ size_str = f"{tarball_size} B"
129
+ elif tarball_size < 1024 * 1024:
130
+ size_str = f"{tarball_size / 1024:.1f} KB"
131
+ else:
132
+ size_str = f"{tarball_size / (1024 * 1024):.2f} MB"
133
+
79
134
  console.print(f" Project: {project_name}")
135
+ console.print(f" Package: {size_str}")
80
136
  console.print(f" Secrets: {len(secrets)} keys")
81
137
  console.print()
82
138
 
@@ -88,39 +144,77 @@ def handle_deploy():
88
144
  files={"package": ("agent.tar.gz", f, "application/gzip")},
89
145
  data={
90
146
  "project_name": project_name,
91
- "secrets": str(secrets),
147
+ "secrets": json.dumps(secrets),
92
148
  "entrypoint": entrypoint,
93
149
  },
94
150
  headers={"Authorization": f"Bearer {api_key}"},
95
- timeout=60,
151
+ timeout=600, # 10 minutes for docker build
96
152
  )
97
153
 
98
154
  if response.status_code != 200:
99
155
  console.print(f"[red]Deploy failed: {response.text}[/red]")
100
156
  return
101
157
 
102
- deployment_id = response.json().get("id")
158
+ result = response.json()
159
+
160
+ # Check for error response (backend returns 200 with error dict)
161
+ if "error" in result:
162
+ console.print(f"[red]Deploy failed: {result['error']}[/red]")
163
+ return
164
+
165
+ deployment_id = result.get("id")
166
+ url = result.get("url", "")
103
167
 
104
168
  # Wait for deployment
105
169
  console.print("Building...")
170
+ deploy_success = False
171
+ final_status = "unknown"
172
+ timeout_count = 0
173
+
106
174
  for _ in range(100):
107
- status_resp = requests.get(
108
- f"{API_BASE}/api/v1/deploy/{deployment_id}/status",
109
- headers={"Authorization": f"Bearer {api_key}"},
110
- timeout=10,
111
- )
175
+ try:
176
+ status_resp = requests.get(
177
+ f"{API_BASE}/api/v1/deploy/{deployment_id}/status",
178
+ headers={"Authorization": f"Bearer {api_key}"},
179
+ timeout=30, # Increased timeout for slow SSH
180
+ )
181
+ except requests.exceptions.Timeout:
182
+ timeout_count += 1
183
+ if timeout_count >= 3:
184
+ console.print("[yellow]Status checks timing out, but deploy may still succeed.[/yellow]")
185
+ break
186
+ time.sleep(3)
187
+ continue
188
+ except requests.exceptions.RequestException as e:
189
+ console.print(f"[yellow]Network error: {e}[/yellow]")
190
+ time.sleep(3)
191
+ continue
192
+
112
193
  if status_resp.status_code != 200:
194
+ console.print(f"[red]Status check failed: {status_resp.status_code}[/red]")
113
195
  break
114
- status = status_resp.json().get("status")
115
- if status == "running":
196
+
197
+ result = status_resp.json()
198
+ final_status = result.get("status", "unknown")
199
+
200
+ if final_status == "running":
201
+ deploy_success = True
202
+ # Update URL from status response (may be more up-to-date)
203
+ url = result.get("url") or url
116
204
  break
117
- if status == "error":
118
- console.print(f"[red]{status_resp.json().get('error_message')}[/red]")
205
+ if final_status in ("error", "failed"):
206
+ console.print(f"[red]Deploy failed: {result.get('error_message', 'Unknown error')}[/red]")
119
207
  return
120
208
  time.sleep(3)
121
209
 
122
- url = response.json().get("url", "")
123
210
  console.print()
124
- console.print("[bold green]Deployed![/bold green]")
125
- console.print(f"Agent URL: {url}")
211
+ if deploy_success:
212
+ console.print("[bold green]Deployed![/bold green]")
213
+ else:
214
+ console.print(f"[yellow]Deploy status: {final_status}[/yellow]")
215
+ console.print("[yellow]Check status with 'co deployments' or try again.[/yellow]")
216
+
217
+ # Always show URL if we have one
218
+ if url:
219
+ console.print(f"Agent URL: {url}")
126
220
  console.print()
@@ -16,10 +16,10 @@ import logging
16
16
  import os
17
17
  import time
18
18
  import uuid
19
- from dataclasses import dataclass, asdict
20
19
  from pathlib import Path
21
- from typing import Union
20
+ from typing import Optional, Union
22
21
 
22
+ from pydantic import BaseModel
23
23
  from rich.console import Console
24
24
  from rich.panel import Panel
25
25
 
@@ -38,15 +38,21 @@ def get_default_trust() -> str:
38
38
 
39
39
  # === Types ===
40
40
 
41
- @dataclass
42
- class Session:
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
+ """
43
49
  session_id: str
44
50
  status: str
45
51
  prompt: str
46
- result: str = None
47
- created: float = None
48
- expires: float = None
49
- duration_ms: int = None
52
+ result: Optional[str] = None
53
+ created: Optional[float] = None
54
+ expires: Optional[float] = None
55
+ duration_ms: Optional[int] = None
50
56
 
51
57
 
52
58
  # === Storage ===
@@ -60,7 +66,7 @@ class SessionStorage:
60
66
 
61
67
  def save(self, session: Session):
62
68
  with open(self.path, "a") as f:
63
- f.write(json.dumps(asdict(session)) + "\n")
69
+ f.write(session.model_dump_json() + "\n")
64
70
 
65
71
  def get(self, session_id: str) -> Session | None:
66
72
  if not self.path.exists():
@@ -149,12 +155,12 @@ def input_handler(agent, storage: SessionStorage, prompt: str, result_ttl: int,
149
155
  def session_handler(storage: SessionStorage, session_id: str) -> dict | None:
150
156
  """GET /sessions/{id}"""
151
157
  session = storage.get(session_id)
152
- return asdict(session) if session else None
158
+ return session.model_dump() if session else None
153
159
 
154
160
 
155
161
  def sessions_handler(storage: SessionStorage) -> dict:
156
162
  """GET /sessions"""
157
- return {"sessions": [asdict(s) for s in storage.list()]}
163
+ return {"sessions": [s.model_dump() for s in storage.list()]}
158
164
 
159
165
 
160
166
  def health_handler(agent, start_time: float) -> dict:
@@ -215,79 +221,55 @@ def verify_signature(payload: dict, signature: str, public_key: str) -> bool:
215
221
  def extract_and_authenticate(data: dict, trust, *, blacklist=None, whitelist=None, agent_address=None):
216
222
  """Extract prompt and authenticate request.
217
223
 
218
- Supports two request formats:
219
-
220
- Simple (unsigned):
221
- {"prompt": "...", "from": "0x..."}
224
+ ALL requests must be signed - this is a protocol requirement.
222
225
 
223
- Signed (Ed25519):
226
+ Required format (Ed25519 signed):
224
227
  {
225
- "payload": {"prompt": "...", "to": "0x...", "timestamp": 123},
226
- "from": "0xClientPublicKey",
227
- "signature": "0x..."
228
+ "payload": {"prompt": "...", "to": "0xAgentAddress", "timestamp": 123},
229
+ "from": "0xCallerPublicKey",
230
+ "signature": "0xEd25519Signature..."
228
231
  }
229
232
 
230
- Trust can be:
231
- - Level: "open", "careful", "strict" (code-based checks)
232
- - Policy: Natural language string (LLM evaluation)
233
- - Agent: Custom Agent instance (LLM evaluation)
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
234
238
 
235
239
  Returns: (prompt, identity, sig_valid, error)
236
240
  """
237
- # Determine trust level for basic auth
238
- if isinstance(trust, str) and trust not in TRUST_LEVELS:
239
- logging.warning(f"Unknown trust level '{trust}', defaulting to 'careful'. Valid: {TRUST_LEVELS}")
240
- trust_level = trust if isinstance(trust, str) and trust in TRUST_LEVELS else "careful"
241
-
242
- # Check for signed request format
243
- if "payload" in data and "signature" in data:
244
- prompt, identity, sig_valid, error = _authenticate_signed(
245
- data, trust_level, blacklist=blacklist, whitelist=whitelist, agent_address=agent_address
246
- )
247
- else:
248
- prompt, identity, sig_valid, error = _authenticate_simple(
249
- data, trust_level, blacklist=blacklist, whitelist=whitelist
250
- )
251
-
252
- # If basic auth failed, return error
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
+ )
253
249
  if error:
254
- return prompt, identity, sig_valid, 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
255
 
256
- # If trust is a policy or custom agent, evaluate with LLM
257
- # Use original trust for custom check (Agent or policy string), but skip if it was a typo'd level
258
- if is_custom_trust(trust) and not (isinstance(trust, str) and trust not in TRUST_LEVELS and trust_level in TRUST_LEVELS):
256
+ # Custom trust policy/agent evaluation
257
+ if is_custom_trust(trust):
259
258
  trust_agent = create_trust_agent(trust)
260
- accepted, reason = evaluate_with_trust_agent(trust_agent, prompt, identity, sig_valid)
259
+ accepted, reason = evaluate_with_trust_agent(trust_agent, prompt, identity, True)
261
260
  if not accepted:
262
- return None, identity, sig_valid, f"rejected: {reason}"
263
-
264
- return prompt, identity, sig_valid, None
265
-
266
-
267
- def _authenticate_simple(data: dict, trust: str, *, blacklist=None, whitelist=None):
268
- """Authenticate simple unsigned request."""
269
- prompt = data.get("prompt", "")
270
- identity = data.get("from")
271
-
272
- # Check blacklist
273
- if blacklist and identity in blacklist:
274
- return None, identity, False, "forbidden: blacklisted"
261
+ return None, identity, True, f"rejected: {reason}"
275
262
 
276
- # Check whitelist (bypass other checks)
277
- if whitelist and identity in whitelist:
278
- return prompt, identity, True, None
263
+ return prompt, identity, True, None
279
264
 
280
- # Trust level enforcement
281
- if trust == "strict" and not identity:
282
- return None, None, False, "unauthorized: identity required"
283
265
 
284
- # For careful/strict without signature, mark sig_valid=False
285
- sig_valid = trust == "open"
286
- return prompt, identity, sig_valid, None
266
+ def _authenticate_signed(data: dict, *, blacklist=None, whitelist=None, agent_address=None):
267
+ """Authenticate signed request with Ed25519 - ALWAYS REQUIRED.
287
268
 
269
+ Protocol-level signature verification. All requests must be signed.
288
270
 
289
- def _authenticate_signed(data: dict, trust: str, *, blacklist=None, whitelist=None, agent_address=None):
290
- """Authenticate signed request with Ed25519."""
271
+ Returns: (prompt, identity, error) - error is None on success
272
+ """
291
273
  payload = data.get("payload", {})
292
274
  identity = data.get("from")
293
275
  signature = data.get("signature")
@@ -298,34 +280,34 @@ def _authenticate_signed(data: dict, trust: str, *, blacklist=None, whitelist=No
298
280
 
299
281
  # Check blacklist first
300
282
  if blacklist and identity in blacklist:
301
- return None, identity, False, "forbidden: blacklisted"
283
+ return None, identity, "forbidden: blacklisted"
302
284
 
303
- # Check whitelist (bypass signature check)
285
+ # Check whitelist (bypass signature check - trusted caller)
304
286
  if whitelist and identity in whitelist:
305
- return prompt, identity, True, None
287
+ return prompt, identity, None
306
288
 
307
- # Validate required fields for signed request
289
+ # Validate required fields
308
290
  if not identity:
309
- return None, None, False, "unauthorized: 'from' field required for signed request"
291
+ return None, None, "unauthorized: 'from' field required"
310
292
  if not signature:
311
- return None, identity, False, "unauthorized: signature required"
293
+ return None, identity, "unauthorized: signature required"
312
294
  if not timestamp:
313
- return None, identity, False, "unauthorized: timestamp required in payload"
295
+ return None, identity, "unauthorized: timestamp required in payload"
314
296
 
315
297
  # Check timestamp expiry (5 minute window)
316
298
  now = time.time()
317
299
  if abs(now - timestamp) > SIGNATURE_EXPIRY_SECONDS:
318
- return None, identity, False, "unauthorized: signature expired"
300
+ return None, identity, "unauthorized: signature expired"
319
301
 
320
302
  # Optionally verify 'to' matches agent address
321
303
  if agent_address and to_address and to_address != agent_address:
322
- return None, identity, False, "unauthorized: wrong recipient"
304
+ return None, identity, "unauthorized: wrong recipient"
323
305
 
324
306
  # Verify signature
325
307
  if not verify_signature(payload, signature, identity):
326
- return None, identity, False, "unauthorized: invalid signature"
308
+ return None, identity, "unauthorized: invalid signature"
327
309
 
328
- return prompt, identity, True, None
310
+ return prompt, identity, None
329
311
 
330
312
 
331
313
  def get_agent_address(agent) -> str:
@@ -375,6 +357,35 @@ def is_custom_trust(trust) -> bool:
375
357
  return trust not in TRUST_LEVELS # It's a policy string
376
358
 
377
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
+
378
389
  # === Entry Point ===
379
390
 
380
391
  def _create_handlers(agent, result_ttl: int):
@@ -387,6 +398,9 @@ def _create_handlers(agent, result_ttl: int):
387
398
  "info": lambda trust: info_handler(agent, trust),
388
399
  "auth": extract_and_authenticate,
389
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,
390
404
  }
391
405
 
392
406
 
@@ -422,7 +436,7 @@ def _start_relay_background(agent, relay_url: str, addr_data: dict):
422
436
 
423
437
  def host(
424
438
  agent,
425
- port: int = 8000,
439
+ port: int = None,
426
440
  trust: Union[str, "Agent"] = "careful",
427
441
  result_ttl: int = 86400,
428
442
  workers: int = 1,
@@ -437,7 +451,7 @@ def host(
437
451
 
438
452
  Args:
439
453
  agent: Agent to host
440
- port: HTTP port (default 8000)
454
+ port: HTTP port (default: PORT env var or 8000)
441
455
  trust: Trust level, policy, or Agent:
442
456
  - Level: "open", "careful", "strict"
443
457
  - Policy: Natural language or file path
@@ -457,10 +471,16 @@ def host(
457
471
  GET /health - Health check
458
472
  GET /info - Agent info
459
473
  WS /ws - WebSocket
474
+ GET /logs - Activity log (requires OPENONION_API_KEY)
475
+ GET /logs/sessions - Activity sessions (requires OPENONION_API_KEY)
460
476
  """
461
477
  import uvicorn
462
478
  from . import address
463
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
+
464
484
  # Load or generate agent identity
465
485
  co_dir = Path.cwd() / '.co'
466
486
  addr_data = address.load(co_dir)
@@ -1,17 +1,22 @@
1
1
  """
2
2
  Purpose: Token usage tracking and cost calculation for LLM calls
3
3
  LLM-Note:
4
- Dependencies: none | imported by [llm.py, agent.py]
4
+ Dependencies: pydantic | imported by [llm.py, agent.py]
5
5
  Data flow: receives model name + token counts → returns cost in USD
6
6
  Integration: exposes TokenUsage, MODEL_PRICING, MODEL_CONTEXT_LIMITS, calculate_cost(), get_context_limit()
7
7
  """
8
8
 
9
- from dataclasses import dataclass
9
+ from pydantic import BaseModel
10
10
 
11
11
 
12
- @dataclass
13
- class TokenUsage:
14
- """Token usage from a single LLM call."""
12
+ class TokenUsage(BaseModel):
13
+ """Token usage from a single LLM call.
14
+
15
+ Uses Pydantic BaseModel for:
16
+ - Native JSON serialization via .model_dump()
17
+ - Type validation at runtime
18
+ - Future-proof API response compatibility
19
+ """
15
20
  input_tokens: int = 0
16
21
  output_tokens: int = 0
17
22
  cached_tokens: int = 0 # Tokens read from cache (subset of input_tokens)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "connectonion"
7
- version = "0.5.3"
7
+ version = "0.5.6"
8
8
  description = "A simple Python framework for creating AI agents with behavior tracking"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes