connectonion 0.5.5__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.5 → connectonion-0.5.6}/PKG-INFO +1 -1
  2. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/__init__.py +1 -1
  3. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/asgi.py +33 -0
  4. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/commands/deploy_commands.py +110 -16
  5. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/host.py +84 -70
  6. {connectonion-0.5.5 → connectonion-0.5.6}/pyproject.toml +1 -1
  7. {connectonion-0.5.5 → connectonion-0.5.6}/.gitignore +0 -0
  8. {connectonion-0.5.5 → connectonion-0.5.6}/README.md +0 -0
  9. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/address.py +0 -0
  10. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/agent.py +0 -0
  11. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/announce.py +0 -0
  12. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/auto_debug_exception.py +0 -0
  13. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/__init__.py +0 -0
  14. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/browser_agent/__init__.py +0 -0
  15. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/browser_agent/browser.py +0 -0
  16. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/browser_agent/prompt.md +0 -0
  17. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/commands/__init__.py +0 -0
  18. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/commands/auth_commands.py +0 -0
  19. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/commands/browser_commands.py +0 -0
  20. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/commands/create.py +0 -0
  21. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/commands/doctor_commands.py +0 -0
  22. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/commands/init.py +0 -0
  23. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/commands/project_cmd_lib.py +0 -0
  24. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/commands/reset_commands.py +0 -0
  25. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/commands/status_commands.py +0 -0
  26. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +0 -0
  27. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/docs/connectonion.md +0 -0
  28. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/docs.md +0 -0
  29. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/main.py +0 -0
  30. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/templates/meta-agent/README.md +0 -0
  31. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/templates/meta-agent/agent.py +0 -0
  32. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +0 -0
  33. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +0 -0
  34. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/templates/meta-agent/prompts/metagent.md +0 -0
  35. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/templates/meta-agent/prompts/think_prompt.md +0 -0
  36. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/templates/minimal/README.md +0 -0
  37. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/templates/minimal/agent.py +0 -0
  38. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/templates/playwright/README.md +0 -0
  39. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/templates/playwright/agent.py +0 -0
  40. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/templates/playwright/prompt.md +0 -0
  41. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/templates/playwright/requirements.txt +0 -0
  42. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/cli/templates/web-research/agent.py +0 -0
  43. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/connect.py +0 -0
  44. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/console.py +0 -0
  45. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/debug_agent/__init__.py +0 -0
  46. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/debug_agent/agent.py +0 -0
  47. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/debug_agent/prompts/debug_assistant.md +0 -0
  48. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/debug_agent/runtime_inspector.py +0 -0
  49. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/debug_explainer/__init__.py +0 -0
  50. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/debug_explainer/explain_agent.py +0 -0
  51. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/debug_explainer/explain_context.py +0 -0
  52. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/debug_explainer/explainer_prompt.md +0 -0
  53. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/debug_explainer/root_cause_analysis_prompt.md +0 -0
  54. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/debugger_ui.py +0 -0
  55. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/decorators.py +0 -0
  56. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/events.py +0 -0
  57. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/execution_analyzer/__init__.py +0 -0
  58. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/execution_analyzer/execution_analysis.py +0 -0
  59. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/execution_analyzer/execution_analysis_prompt.md +0 -0
  60. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/interactive_debugger.py +0 -0
  61. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/llm.py +0 -0
  62. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/llm_do.py +0 -0
  63. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/logger.py +0 -0
  64. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/prompt_files/__init__.py +0 -0
  65. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/prompt_files/analyze_contact.md +0 -0
  66. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/prompt_files/eval_expected.md +0 -0
  67. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/prompt_files/react_evaluate.md +0 -0
  68. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/prompt_files/react_plan.md +0 -0
  69. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/prompt_files/reflect.md +0 -0
  70. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/prompts.py +0 -0
  71. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/relay.py +0 -0
  72. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/static/docs.html +0 -0
  73. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/tool_executor.py +0 -0
  74. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/tool_factory.py +0 -0
  75. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/tool_registry.py +0 -0
  76. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/trust.py +0 -0
  77. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/trust_agents.py +0 -0
  78. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/trust_functions.py +0 -0
  79. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/tui/__init__.py +0 -0
  80. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/tui/divider.py +0 -0
  81. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/tui/dropdown.py +0 -0
  82. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/tui/footer.py +0 -0
  83. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/tui/fuzzy.py +0 -0
  84. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/tui/input.py +0 -0
  85. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/tui/keys.py +0 -0
  86. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/tui/pick.py +0 -0
  87. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/tui/providers.py +0 -0
  88. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/tui/status_bar.py +0 -0
  89. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/usage.py +0 -0
  90. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_events_handlers/__init__.py +0 -0
  91. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_events_handlers/reflect.py +0 -0
  92. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_plugins/__init__.py +0 -0
  93. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_plugins/calendar_plugin.py +0 -0
  94. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_plugins/eval.py +0 -0
  95. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_plugins/gmail_plugin.py +0 -0
  96. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_plugins/image_result_formatter.py +0 -0
  97. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_plugins/re_act.py +0 -0
  98. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_plugins/shell_approval.py +0 -0
  99. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_tools/__init__.py +0 -0
  100. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_tools/diff_writer.py +0 -0
  101. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_tools/get_emails.py +0 -0
  102. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_tools/gmail.py +0 -0
  103. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_tools/google_calendar.py +0 -0
  104. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_tools/memory.py +0 -0
  105. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_tools/microsoft_calendar.py +0 -0
  106. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_tools/outlook.py +0 -0
  107. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_tools/send_email.py +0 -0
  108. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_tools/shell.py +0 -0
  109. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_tools/slash_command.py +0 -0
  110. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_tools/terminal.py +0 -0
  111. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_tools/todo_list.py +0 -0
  112. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/useful_tools/web_fetch.py +0 -0
  113. {connectonion-0.5.5 → connectonion-0.5.6}/connectonion/xray.py +0 -0
  114. {connectonion-0.5.5 → connectonion-0.5.6}/docs/README.md +0 -0
  115. {connectonion-0.5.5 → connectonion-0.5.6}/docs/cli/README.md +0 -0
  116. {connectonion-0.5.5 → connectonion-0.5.6}/docs/debug/README.md +0 -0
  117. {connectonion-0.5.5 → connectonion-0.5.6}/docs/integrations/README.md +0 -0
  118. {connectonion-0.5.5 → connectonion-0.5.6}/docs/network/README.md +0 -0
  119. {connectonion-0.5.5 → connectonion-0.5.6}/docs/templates/README.md +0 -0
  120. {connectonion-0.5.5 → connectonion-0.5.6}/docs/tui/README.md +0 -0
  121. {connectonion-0.5.5 → connectonion-0.5.6}/docs/useful_plugins/README.md +0 -0
  122. {connectonion-0.5.5 → connectonion-0.5.6}/docs/useful_tools/README.md +0 -0
  123. {connectonion-0.5.5 → connectonion-0.5.6}/examples/README.md +0 -0
  124. {connectonion-0.5.5 → connectonion-0.5.6}/examples/browser-agent/README.md +0 -0
  125. {connectonion-0.5.5 → connectonion-0.5.6}/examples/email-agent/README.md +0 -0
  126. {connectonion-0.5.5 → connectonion-0.5.6}/examples/simple-agent/README.md +0 -0
  127. {connectonion-0.5.5 → connectonion-0.5.6}/prompts/README.md +0 -0
  128. {connectonion-0.5.5 → connectonion-0.5.6}/prompts/formats/README.md +0 -0
  129. {connectonion-0.5.5 → connectonion-0.5.6}/tests/README.md +0 -0
  130. {connectonion-0.5.5 → connectonion-0.5.6}/tests/cli/README.md +0 -0
  131. {connectonion-0.5.5 → 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.5
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.5"
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,7 +6,9 @@ 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
 
12
14
  from pydantic import BaseModel
@@ -52,6 +54,13 @@ async def send_html(send, html: bytes, status: int = 200):
52
54
  await send({"type": "http.response.body", "body": html})
53
55
 
54
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
+
55
64
  async def handle_http(
56
65
  scope,
57
66
  receive,
@@ -81,6 +90,30 @@ async def handle_http(
81
90
  """
82
91
  method, path = scope["method"], scope["path"]
83
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
+
84
117
  if method == "POST" and path == "/input":
85
118
  body = await read_body(receive)
86
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()
@@ -221,79 +221,55 @@ def verify_signature(payload: dict, signature: str, public_key: str) -> bool:
221
221
  def extract_and_authenticate(data: dict, trust, *, blacklist=None, whitelist=None, agent_address=None):
222
222
  """Extract prompt and authenticate request.
223
223
 
224
- Supports two request formats:
224
+ ALL requests must be signed - this is a protocol requirement.
225
225
 
226
- Simple (unsigned):
227
- {"prompt": "...", "from": "0x..."}
228
-
229
- Signed (Ed25519):
226
+ Required format (Ed25519 signed):
230
227
  {
231
- "payload": {"prompt": "...", "to": "0x...", "timestamp": 123},
232
- "from": "0xClientPublicKey",
233
- "signature": "0x..."
228
+ "payload": {"prompt": "...", "to": "0xAgentAddress", "timestamp": 123},
229
+ "from": "0xCallerPublicKey",
230
+ "signature": "0xEd25519Signature..."
234
231
  }
235
232
 
236
- Trust can be:
237
- - Level: "open", "careful", "strict" (code-based checks)
238
- - Policy: Natural language string (LLM evaluation)
239
- - 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
240
238
 
241
239
  Returns: (prompt, identity, sig_valid, error)
242
240
  """
243
- # Determine trust level for basic auth
244
- if isinstance(trust, str) and trust not in TRUST_LEVELS:
245
- logging.warning(f"Unknown trust level '{trust}', defaulting to 'careful'. Valid: {TRUST_LEVELS}")
246
- trust_level = trust if isinstance(trust, str) and trust in TRUST_LEVELS else "careful"
247
-
248
- # Check for signed request format
249
- if "payload" in data and "signature" in data:
250
- prompt, identity, sig_valid, error = _authenticate_signed(
251
- data, trust_level, blacklist=blacklist, whitelist=whitelist, agent_address=agent_address
252
- )
253
- else:
254
- prompt, identity, sig_valid, error = _authenticate_simple(
255
- data, trust_level, blacklist=blacklist, whitelist=whitelist
256
- )
257
-
258
- # 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
+ )
259
249
  if error:
260
- return prompt, identity, sig_valid, error
250
+ return prompt, identity, False, error
261
251
 
262
- # If trust is a policy or custom agent, evaluate with LLM
263
- # Use original trust for custom check (Agent or policy string), but skip if it was a typo'd level
264
- if is_custom_trust(trust) and not (isinstance(trust, str) and trust not in TRUST_LEVELS and trust_level in TRUST_LEVELS):
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):
265
258
  trust_agent = create_trust_agent(trust)
266
- accepted, reason = evaluate_with_trust_agent(trust_agent, prompt, identity, sig_valid)
259
+ accepted, reason = evaluate_with_trust_agent(trust_agent, prompt, identity, True)
267
260
  if not accepted:
268
- return None, identity, sig_valid, f"rejected: {reason}"
269
-
270
- return prompt, identity, sig_valid, None
271
-
261
+ return None, identity, True, f"rejected: {reason}"
272
262
 
273
- def _authenticate_simple(data: dict, trust: str, *, blacklist=None, whitelist=None):
274
- """Authenticate simple unsigned request."""
275
- prompt = data.get("prompt", "")
276
- identity = data.get("from")
277
-
278
- # Check blacklist
279
- if blacklist and identity in blacklist:
280
- return None, identity, False, "forbidden: blacklisted"
281
-
282
- # Check whitelist (bypass other checks)
283
- if whitelist and identity in whitelist:
284
- return prompt, identity, True, None
263
+ return prompt, identity, True, None
285
264
 
286
- # Trust level enforcement
287
- if trust == "strict" and not identity:
288
- return None, None, False, "unauthorized: identity required"
289
265
 
290
- # For careful/strict without signature, mark sig_valid=False
291
- sig_valid = trust == "open"
292
- 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.
293
268
 
269
+ Protocol-level signature verification. All requests must be signed.
294
270
 
295
- def _authenticate_signed(data: dict, trust: str, *, blacklist=None, whitelist=None, agent_address=None):
296
- """Authenticate signed request with Ed25519."""
271
+ Returns: (prompt, identity, error) - error is None on success
272
+ """
297
273
  payload = data.get("payload", {})
298
274
  identity = data.get("from")
299
275
  signature = data.get("signature")
@@ -304,34 +280,34 @@ def _authenticate_signed(data: dict, trust: str, *, blacklist=None, whitelist=No
304
280
 
305
281
  # Check blacklist first
306
282
  if blacklist and identity in blacklist:
307
- return None, identity, False, "forbidden: blacklisted"
283
+ return None, identity, "forbidden: blacklisted"
308
284
 
309
- # Check whitelist (bypass signature check)
285
+ # Check whitelist (bypass signature check - trusted caller)
310
286
  if whitelist and identity in whitelist:
311
- return prompt, identity, True, None
287
+ return prompt, identity, None
312
288
 
313
- # Validate required fields for signed request
289
+ # Validate required fields
314
290
  if not identity:
315
- return None, None, False, "unauthorized: 'from' field required for signed request"
291
+ return None, None, "unauthorized: 'from' field required"
316
292
  if not signature:
317
- return None, identity, False, "unauthorized: signature required"
293
+ return None, identity, "unauthorized: signature required"
318
294
  if not timestamp:
319
- return None, identity, False, "unauthorized: timestamp required in payload"
295
+ return None, identity, "unauthorized: timestamp required in payload"
320
296
 
321
297
  # Check timestamp expiry (5 minute window)
322
298
  now = time.time()
323
299
  if abs(now - timestamp) > SIGNATURE_EXPIRY_SECONDS:
324
- return None, identity, False, "unauthorized: signature expired"
300
+ return None, identity, "unauthorized: signature expired"
325
301
 
326
302
  # Optionally verify 'to' matches agent address
327
303
  if agent_address and to_address and to_address != agent_address:
328
- return None, identity, False, "unauthorized: wrong recipient"
304
+ return None, identity, "unauthorized: wrong recipient"
329
305
 
330
306
  # Verify signature
331
307
  if not verify_signature(payload, signature, identity):
332
- return None, identity, False, "unauthorized: invalid signature"
308
+ return None, identity, "unauthorized: invalid signature"
333
309
 
334
- return prompt, identity, True, None
310
+ return prompt, identity, None
335
311
 
336
312
 
337
313
  def get_agent_address(agent) -> str:
@@ -381,6 +357,35 @@ def is_custom_trust(trust) -> bool:
381
357
  return trust not in TRUST_LEVELS # It's a policy string
382
358
 
383
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
+
384
389
  # === Entry Point ===
385
390
 
386
391
  def _create_handlers(agent, result_ttl: int):
@@ -393,6 +398,9 @@ def _create_handlers(agent, result_ttl: int):
393
398
  "info": lambda trust: info_handler(agent, trust),
394
399
  "auth": extract_and_authenticate,
395
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,
396
404
  }
397
405
 
398
406
 
@@ -428,7 +436,7 @@ def _start_relay_background(agent, relay_url: str, addr_data: dict):
428
436
 
429
437
  def host(
430
438
  agent,
431
- port: int = 8000,
439
+ port: int = None,
432
440
  trust: Union[str, "Agent"] = "careful",
433
441
  result_ttl: int = 86400,
434
442
  workers: int = 1,
@@ -443,7 +451,7 @@ def host(
443
451
 
444
452
  Args:
445
453
  agent: Agent to host
446
- port: HTTP port (default 8000)
454
+ port: HTTP port (default: PORT env var or 8000)
447
455
  trust: Trust level, policy, or Agent:
448
456
  - Level: "open", "careful", "strict"
449
457
  - Policy: Natural language or file path
@@ -463,10 +471,16 @@ def host(
463
471
  GET /health - Health check
464
472
  GET /info - Agent info
465
473
  WS /ws - WebSocket
474
+ GET /logs - Activity log (requires OPENONION_API_KEY)
475
+ GET /logs/sessions - Activity sessions (requires OPENONION_API_KEY)
466
476
  """
467
477
  import uvicorn
468
478
  from . import address
469
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
+
470
484
  # Load or generate agent identity
471
485
  co_dir = Path.cwd() / '.co'
472
486
  addr_data = address.load(co_dir)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "connectonion"
7
- version = "0.5.5"
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