@1claw/cli 0.57.0 → 0.58.1
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.
- package/README.md +13 -2
- package/dist/bundled-templates/shared/index.html +109 -0
- package/dist/bundled-templates/shared/spawn_chat_ui.py +343 -0
- package/dist/bundled-templates/shared/start-with-chat-ui.sh +33 -0
- package/dist/bundled-templates/templates/agentkit/Dockerfile +3 -0
- package/dist/bundled-templates/templates/agentkit/agent.py +1 -1
- package/dist/bundled-templates/templates/agentkit/entrypoint.sh +2 -2
- package/dist/bundled-templates/templates/agno/Dockerfile +3 -0
- package/dist/bundled-templates/templates/agno/agent.py +1 -1
- package/dist/bundled-templates/templates/agno/entrypoint.sh +2 -2
- package/dist/bundled-templates/templates/agno/requirements.txt +1 -1
- package/dist/bundled-templates/templates/coder/Dockerfile +3 -0
- package/dist/bundled-templates/templates/coder/agent.py +1 -1
- package/dist/bundled-templates/templates/coder/entrypoint.sh +2 -2
- package/dist/bundled-templates/templates/crewai/Dockerfile +3 -0
- package/dist/bundled-templates/templates/crewai/agent.py +1 -1
- package/dist/bundled-templates/templates/crewai/entrypoint.sh +2 -2
- package/dist/bundled-templates/templates/elizaos/Dockerfile +3 -0
- package/dist/bundled-templates/templates/elizaos/agent.ts +1 -1
- package/dist/bundled-templates/templates/elizaos/entrypoint.sh +2 -2
- package/dist/bundled-templates/templates/langchain/Dockerfile +3 -0
- package/dist/bundled-templates/templates/langchain/agent.py +1 -1
- package/dist/bundled-templates/templates/langchain/entrypoint.sh +2 -2
- package/dist/bundled-templates/templates/llamaindex/Dockerfile +3 -0
- package/dist/bundled-templates/templates/llamaindex/agent.py +1 -1
- package/dist/bundled-templates/templates/llamaindex/entrypoint.sh +2 -2
- package/dist/bundled-templates/templates/mastra/Dockerfile +4 -1
- package/dist/bundled-templates/templates/mastra/agent.ts +1 -1
- package/dist/bundled-templates/templates/mastra/entrypoint.sh +2 -2
- package/dist/bundled-templates/templates/openai-agents/Dockerfile +3 -0
- package/dist/bundled-templates/templates/openai-agents/agent.py +1 -1
- package/dist/bundled-templates/templates/openai-agents/entrypoint.sh +2 -2
- package/dist/bundled-templates/templates/pydantic-ai/Dockerfile +3 -0
- package/dist/bundled-templates/templates/pydantic-ai/agent.py +1 -1
- package/dist/bundled-templates/templates/pydantic-ai/entrypoint.sh +2 -2
- package/dist/bundled-templates/templates/smolagents/Dockerfile +3 -0
- package/dist/bundled-templates/templates/smolagents/README.md +1 -1
- package/dist/bundled-templates/templates/smolagents/SECURITY.md +17 -0
- package/dist/bundled-templates/templates/smolagents/agent.py +11 -3
- package/dist/bundled-templates/templates/smolagents/entrypoint.sh +2 -2
- package/dist/bundled-templates/templates/typescript-sdk/Dockerfile +4 -1
- package/dist/bundled-templates/templates/typescript-sdk/README.md +3 -0
- package/dist/bundled-templates/templates/typescript-sdk/agent.ts +164 -15
- package/dist/bundled-templates/templates/typescript-sdk/entrypoint.sh +2 -2
- package/dist/src/commands/agent.js +58 -0
- package/dist/src/commands/agent.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +20 -21
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/platform.js +90 -2
- package/dist/src/commands/platform.js.map +1 -1
- package/dist/src/commands/proxy.d.ts.map +1 -1
- package/dist/src/commands/proxy.js +7 -23
- package/dist/src/commands/proxy.js.map +1 -1
- package/dist/src/commands/spawn.d.ts.map +1 -1
- package/dist/src/commands/spawn.js +39 -19
- package/dist/src/commands/spawn.js.map +1 -1
- package/dist/src/lib/agent-key.d.ts +13 -0
- package/dist/src/lib/agent-key.d.ts.map +1 -0
- package/dist/src/lib/agent-key.js +45 -0
- package/dist/src/lib/agent-key.js.map +1 -0
- package/dist/src/lib/docker-client.js +1 -1
- package/dist/src/lib/docker-client.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @1claw/cli (v0.
|
|
1
|
+
# @1claw/cli (v0.57.0)
|
|
2
2
|
|
|
3
3
|
Command-line interface for [1Claw](https://1claw.xyz).
|
|
4
4
|
|
|
@@ -847,6 +847,15 @@ Create, manage, and trigger automation workflows for agents.
|
|
|
847
847
|
1claw automation delete <id> # Delete automation
|
|
848
848
|
```
|
|
849
849
|
|
|
850
|
+
**Agent-scoped create** (agent JWT only; simple manual/webhook workflows):
|
|
851
|
+
|
|
852
|
+
```bash
|
|
853
|
+
1claw agent automation create <agent-id> my-reminder \
|
|
854
|
+
--workflow '{"steps":[{"type":"log","message":"done"}]}' \
|
|
855
|
+
--api-key ocv_...
|
|
856
|
+
# Or: ONECLAW_AGENT_TOKEN=... 1claw agent automation create <agent-id> ...
|
|
857
|
+
```
|
|
858
|
+
|
|
850
859
|
`workflow_spec` is required on create (`--workflow` JSON or `@file`). For cron
|
|
851
860
|
triggers, `--cron` is required. Dashboard `schedule` is accepted by the API as
|
|
852
861
|
an alias for `cron`.
|
|
@@ -1113,7 +1122,7 @@ conflicts: [] # e.g. [other-tool] to forbid combining
|
|
|
1113
1122
|
|
|
1114
1123
|
## Agent Templates (`spawn`)
|
|
1115
1124
|
|
|
1116
|
-
`1claw spawn` creates a framework-specific AI agent from a pre-built template. Each template ships a Dockerfile, starter code, and entrypoint pre-wired with 1Claw MCP and Shroud. The container follows the same daemon-socket security model as `init --docker` — **the container never sees raw API keys.**
|
|
1125
|
+
`1claw spawn` creates a framework-specific AI agent from a pre-built template. Each template ships a Dockerfile, starter code, and entrypoint pre-wired with 1Claw MCP and Shroud. After spawn, an **interactive browser chat UI** opens at `http://localhost:<port>` (default 3000) so you can send test messages immediately. The container follows the same daemon-socket security model as `init --docker` — **the container never sees raw API keys.**
|
|
1117
1126
|
|
|
1118
1127
|
```bash
|
|
1119
1128
|
1claw spawn langchain # LangChain agent with Shroud LLM routing
|
|
@@ -1123,6 +1132,8 @@ conflicts: [] # e.g. [other-tool] to forbid combining
|
|
|
1123
1132
|
1claw spawn --refresh # Force-refresh templates from GitHub
|
|
1124
1133
|
```
|
|
1125
1134
|
|
|
1135
|
+
When the health check passes, spawn prints the local URL and opens your browser to the shared chat UI. Framework logic runs on an internal port; the UI forwards `POST /chat` to your agent or falls back to Shroud through the host daemon `/proxy`.
|
|
1136
|
+
|
|
1126
1137
|
### Available templates
|
|
1127
1138
|
|
|
1128
1139
|
**Python:**
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>1Claw __FRAMEWORK__</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { color-scheme: dark; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
body {
|
|
11
|
+
margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
12
|
+
background: #0b0d12; color: #e6e9ef; height: 100vh; display: flex; flex-direction: column;
|
|
13
|
+
}
|
|
14
|
+
header {
|
|
15
|
+
padding: 12px 18px; border-bottom: 1px solid #1c2230;
|
|
16
|
+
display: flex; align-items: center; gap: 12px; background: #0e1219;
|
|
17
|
+
}
|
|
18
|
+
header .dot { width: 9px; height: 9px; border-radius: 50%; background: #38d39f; box-shadow: 0 0 8px #38d39f; }
|
|
19
|
+
header h1 { font-size: 15px; margin: 0; font-weight: 600; }
|
|
20
|
+
header .meta { margin-left: auto; font-size: 12px; color: #7c869b; }
|
|
21
|
+
header .badge { background: #1c2230; padding: 2px 8px; border-radius: 999px; margin-left: 6px; }
|
|
22
|
+
#log { flex: 1; overflow-y: auto; padding: 18px; display: flex; flex-direction: column; gap: 12px; }
|
|
23
|
+
.msg { max-width: 80%; padding: 10px 14px; border-radius: 12px; white-space: pre-wrap; line-height: 1.45; font-size: 14px; }
|
|
24
|
+
.user { align-self: flex-end; background: #2563eb; color: #fff; border-bottom-right-radius: 3px; }
|
|
25
|
+
.bot { align-self: flex-start; background: #161b25; border: 1px solid #1c2230; border-bottom-left-radius: 3px; }
|
|
26
|
+
.tool { align-self: flex-start; font-size: 11px; color: #8aa0c2; margin-top: -6px; }
|
|
27
|
+
form { display: flex; gap: 8px; padding: 14px 18px; border-top: 1px solid #1c2230; background: #0e1219; }
|
|
28
|
+
input { flex: 1; background: #0b0d12; border: 1px solid #28303f; color: #e6e9ef; padding: 11px 14px; border-radius: 10px; font-size: 14px; outline: none; }
|
|
29
|
+
input:focus { border-color: #2563eb; }
|
|
30
|
+
button { background: #2563eb; color: #fff; border: none; padding: 0 18px; border-radius: 10px; font-size: 14px; cursor: pointer; font-weight: 600; }
|
|
31
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
32
|
+
</style>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<header>
|
|
36
|
+
<span class="dot"></span>
|
|
37
|
+
<h1>1Claw __FRAMEWORK__</h1>
|
|
38
|
+
<span class="meta" id="meta">connecting…</span>
|
|
39
|
+
</header>
|
|
40
|
+
<div id="log"></div>
|
|
41
|
+
<form id="form">
|
|
42
|
+
<input id="input" placeholder="Type a message or /help" autocomplete="off" autofocus />
|
|
43
|
+
<button id="send" type="submit">Send</button>
|
|
44
|
+
</form>
|
|
45
|
+
<script>
|
|
46
|
+
const log = document.getElementById("log");
|
|
47
|
+
const form = document.getElementById("form");
|
|
48
|
+
const input = document.getElementById("input");
|
|
49
|
+
const send = document.getElementById("send");
|
|
50
|
+
const meta = document.getElementById("meta");
|
|
51
|
+
|
|
52
|
+
function add(role, text, tool) {
|
|
53
|
+
if (tool) {
|
|
54
|
+
const t = document.createElement("div");
|
|
55
|
+
t.className = "tool";
|
|
56
|
+
t.textContent = "⚙ " + tool;
|
|
57
|
+
log.appendChild(t);
|
|
58
|
+
}
|
|
59
|
+
const el = document.createElement("div");
|
|
60
|
+
el.className = "msg " + (role === "user" ? "user" : "bot");
|
|
61
|
+
el.textContent = text;
|
|
62
|
+
log.appendChild(el);
|
|
63
|
+
log.scrollTop = log.scrollHeight;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function loadInfo() {
|
|
67
|
+
try {
|
|
68
|
+
const r = await fetch("/api/info");
|
|
69
|
+
const info = await r.json();
|
|
70
|
+
const parts = [];
|
|
71
|
+
if (info.framework) parts.push(info.framework);
|
|
72
|
+
parts.push("vault=" + info.mode);
|
|
73
|
+
if (info.agentId) parts.push("agent=" + info.agentId.slice(0, 8) + "…");
|
|
74
|
+
parts.push(info.daemonReachable ? "daemon ✓" : "daemon —");
|
|
75
|
+
meta.innerHTML = parts.map((p) => '<span class="badge">' + p + "</span>").join("");
|
|
76
|
+
} catch {
|
|
77
|
+
meta.textContent = "offline";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
form.addEventListener("submit", async (e) => {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
const message = input.value.trim();
|
|
84
|
+
if (!message) return;
|
|
85
|
+
add("user", message);
|
|
86
|
+
input.value = "";
|
|
87
|
+
send.disabled = true;
|
|
88
|
+
try {
|
|
89
|
+
const r = await fetch("/api/chat", {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "Content-Type": "application/json" },
|
|
92
|
+
body: JSON.stringify({ message }),
|
|
93
|
+
});
|
|
94
|
+
const data = await r.json();
|
|
95
|
+
add("bot", data.reply || "(no reply)", data.tool);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
add("bot", "Error: " + err.message);
|
|
98
|
+
} finally {
|
|
99
|
+
send.disabled = false;
|
|
100
|
+
input.focus();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
add("bot", "1Claw agent ready. Credentials stay in the host daemon — this container never sees secret values. Type /help to begin.");
|
|
105
|
+
loadInfo();
|
|
106
|
+
setInterval(loadInfo, 15000);
|
|
107
|
+
</script>
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Minimal browser chat UI for `1claw spawn` templates.
|
|
4
|
+
|
|
5
|
+
Listens on CHAT_UI_PORT (default 3000). Forwards chat to the framework agent on
|
|
6
|
+
AGENT_INTERNAL_PORT (default 3001) when available, otherwise falls back to Shroud
|
|
7
|
+
via the host daemon /proxy (credentials never enter the container).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import socket
|
|
15
|
+
import sys
|
|
16
|
+
import threading
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
from urllib.error import HTTPError, URLError
|
|
21
|
+
from urllib.request import Request, urlopen
|
|
22
|
+
|
|
23
|
+
PORT = int(os.environ.get("CHAT_UI_PORT", "3000"))
|
|
24
|
+
AGENT_PORT = int(os.environ.get("AGENT_INTERNAL_PORT", "3001"))
|
|
25
|
+
SOCKET_PATH = os.environ.get("ONECLAW_DAEMON_SOCKET", "/run/1claw/daemon.sock")
|
|
26
|
+
SECRET_PREFIX = os.environ.get("ONECLAW_SECRET_PREFIX", "")
|
|
27
|
+
AGENT_ID = os.environ.get("ONECLAW_AGENT_ID", "")
|
|
28
|
+
FRAMEWORK = os.environ.get("ONECLAW_FRAMEWORK", "agent")
|
|
29
|
+
MODE = os.environ.get(
|
|
30
|
+
"ONECLAW_MODE",
|
|
31
|
+
"local" if os.environ.get("ONECLAW_LOCAL_VAULT") == "true" else "cloud",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
LLM_VIA_SHROUD = os.environ.get("ONECLAW_LLM_VIA_SHROUD", "").lower() == "true"
|
|
35
|
+
SHROUD_URL = (os.environ.get("ONECLAW_SHROUD_URL") or "https://shroud.1claw.xyz").rstrip("/")
|
|
36
|
+
SHROUD_SECRET = os.environ.get("ONECLAW_SHROUD_SECRET", "")
|
|
37
|
+
SHROUD_API_KEY_SECRET = os.environ.get("ONECLAW_SHROUD_API_KEY_SECRET", "")
|
|
38
|
+
SHROUD_PROVIDER = os.environ.get("ONECLAW_SHROUD_PROVIDER", "openai")
|
|
39
|
+
SHROUD_MODEL = os.environ.get("ONECLAW_SHROUD_MODEL", "gpt-4o-mini")
|
|
40
|
+
SYSTEM_PROMPT = os.environ.get(
|
|
41
|
+
"ONECLAW_SHROUD_SYSTEM_PROMPT",
|
|
42
|
+
"You are a helpful AI agent running inside a 1Claw secure container. "
|
|
43
|
+
"Your credentials are held by the host daemon and never exposed to you.",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
HISTORY_MAX = 20
|
|
47
|
+
_conversation: list[dict[str, str]] = []
|
|
48
|
+
_lock = threading.Lock()
|
|
49
|
+
|
|
50
|
+
_INDEX_HTML = Path(__file__).with_name("index.html")
|
|
51
|
+
if _INDEX_HTML.is_file():
|
|
52
|
+
INDEX_HTML = _INDEX_HTML.read_text(encoding="utf-8")
|
|
53
|
+
else:
|
|
54
|
+
INDEX_HTML = "<h1>1Claw Agent</h1>"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _daemon_request(method: str, path: str, body: dict[str, Any] | None = None) -> tuple[int, dict[str, Any] | None]:
|
|
58
|
+
payload = json.dumps(body).encode("utf-8") if body is not None else None
|
|
59
|
+
headers = [
|
|
60
|
+
f"{method} {path} HTTP/1.1",
|
|
61
|
+
"Host: localhost",
|
|
62
|
+
"Content-Type: application/json",
|
|
63
|
+
"Connection: close",
|
|
64
|
+
]
|
|
65
|
+
if SECRET_PREFIX:
|
|
66
|
+
headers.append(f"X-Secret-Prefix: {SECRET_PREFIX}")
|
|
67
|
+
if payload is not None:
|
|
68
|
+
headers.append(f"Content-Length: {len(payload)}")
|
|
69
|
+
headers.append("")
|
|
70
|
+
headers.append("")
|
|
71
|
+
raw = "\r\n".join(headers).encode("utf-8") + (payload or b"")
|
|
72
|
+
|
|
73
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
74
|
+
sock.settimeout(120)
|
|
75
|
+
try:
|
|
76
|
+
sock.connect(SOCKET_PATH)
|
|
77
|
+
sock.sendall(raw)
|
|
78
|
+
chunks: list[bytes] = []
|
|
79
|
+
while True:
|
|
80
|
+
part = sock.recv(65536)
|
|
81
|
+
if not part:
|
|
82
|
+
break
|
|
83
|
+
chunks.append(part)
|
|
84
|
+
finally:
|
|
85
|
+
sock.close()
|
|
86
|
+
|
|
87
|
+
text = b"".join(chunks).decode("utf-8", errors="replace")
|
|
88
|
+
if "\r\n\r\n" not in text:
|
|
89
|
+
return 0, None
|
|
90
|
+
_, body_text = text.split("\r\n\r\n", 1)
|
|
91
|
+
status_line = text.split("\r\n", 1)[0]
|
|
92
|
+
try:
|
|
93
|
+
status = int(status_line.split()[1])
|
|
94
|
+
except (IndexError, ValueError):
|
|
95
|
+
status = 0
|
|
96
|
+
try:
|
|
97
|
+
parsed = json.loads(body_text) if body_text.strip() else None
|
|
98
|
+
except json.JSONDecodeError:
|
|
99
|
+
parsed = {"raw": body_text}
|
|
100
|
+
return status, parsed
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _daemon_reachable() -> bool:
|
|
104
|
+
try:
|
|
105
|
+
status, _ = _daemon_request("GET", "/health")
|
|
106
|
+
return status == 200
|
|
107
|
+
except OSError:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _shroud_chat(user_text: str) -> tuple[bool, str]:
|
|
112
|
+
messages = [{"role": "system", "content": SYSTEM_PROMPT}, *_conversation, {"role": "user", "content": user_text}]
|
|
113
|
+
proxy_body: dict[str, Any] = {
|
|
114
|
+
"secretName": SHROUD_SECRET,
|
|
115
|
+
"url": f"{SHROUD_URL}/v1/chat/completions",
|
|
116
|
+
"method": "POST",
|
|
117
|
+
"headers": {
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
"X-Shroud-Provider": SHROUD_PROVIDER,
|
|
120
|
+
},
|
|
121
|
+
"body": json.dumps({"model": SHROUD_MODEL, "messages": messages}),
|
|
122
|
+
}
|
|
123
|
+
if SHROUD_API_KEY_SECRET:
|
|
124
|
+
proxy_body["injectSecrets"] = [SHROUD_API_KEY_SECRET]
|
|
125
|
+
|
|
126
|
+
status, body = _daemon_request("POST", "/proxy", proxy_body)
|
|
127
|
+
if status != 200 or not body:
|
|
128
|
+
detail = (body or {}).get("error") if body else "unknown daemon error"
|
|
129
|
+
return False, f"Daemon refused the LLM call ({status}): {detail}"
|
|
130
|
+
|
|
131
|
+
upstream_status = body.get("status", 502)
|
|
132
|
+
raw_upstream = body.get("body", "")
|
|
133
|
+
try:
|
|
134
|
+
upstream = json.loads(raw_upstream) if isinstance(raw_upstream, str) else raw_upstream
|
|
135
|
+
except json.JSONDecodeError:
|
|
136
|
+
upstream = {"raw": raw_upstream}
|
|
137
|
+
|
|
138
|
+
if upstream_status != 200:
|
|
139
|
+
err = upstream.get("error") if isinstance(upstream, dict) else upstream
|
|
140
|
+
if isinstance(err, dict):
|
|
141
|
+
msg = err.get("message") or str(err)
|
|
142
|
+
else:
|
|
143
|
+
msg = str(err or upstream)
|
|
144
|
+
return False, f"Shroud/upstream returned {upstream_status}: {msg}"
|
|
145
|
+
|
|
146
|
+
content = (
|
|
147
|
+
upstream.get("choices", [{}])[0]
|
|
148
|
+
.get("message", {})
|
|
149
|
+
.get("content")
|
|
150
|
+
if isinstance(upstream, dict)
|
|
151
|
+
else None
|
|
152
|
+
)
|
|
153
|
+
if not content:
|
|
154
|
+
return False, f"No content in model response: {str(upstream)[:400]}"
|
|
155
|
+
return True, str(content)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _agent_chat(user_text: str) -> dict[str, Any] | None:
|
|
159
|
+
req = Request(
|
|
160
|
+
f"http://127.0.0.1:{AGENT_PORT}/chat",
|
|
161
|
+
data=json.dumps({"message": user_text}).encode("utf-8"),
|
|
162
|
+
headers={"Content-Type": "application/json"},
|
|
163
|
+
method="POST",
|
|
164
|
+
)
|
|
165
|
+
try:
|
|
166
|
+
with urlopen(req, timeout=120) as resp:
|
|
167
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
168
|
+
except (HTTPError, URLError, TimeoutError, json.JSONDecodeError, OSError):
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
if isinstance(data, dict) and data.get("response"):
|
|
172
|
+
return {
|
|
173
|
+
"reply": str(data["response"]),
|
|
174
|
+
"tool": data.get("framework") or FRAMEWORK,
|
|
175
|
+
}
|
|
176
|
+
if isinstance(data, dict) and data.get("error"):
|
|
177
|
+
return {"reply": str(data["error"]), "tool": "agent_error"}
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def handle_chat(message: str) -> dict[str, Any]:
|
|
182
|
+
text = (message or "").strip()
|
|
183
|
+
|
|
184
|
+
if text in ("", "/help"):
|
|
185
|
+
llm_line = (
|
|
186
|
+
f"LLM is wired via Shroud ({SHROUD_PROVIDER}/{SHROUD_MODEL}) — type a message to chat.\n"
|
|
187
|
+
if LLM_VIA_SHROUD and SHROUD_SECRET
|
|
188
|
+
else "Framework agent on port "
|
|
189
|
+
f"{AGENT_PORT} handles chat when running; Shroud fallback uses the daemon.\n"
|
|
190
|
+
)
|
|
191
|
+
return {
|
|
192
|
+
"reply": (
|
|
193
|
+
f"1Claw {FRAMEWORK} agent ready.\n"
|
|
194
|
+
f"{llm_line}"
|
|
195
|
+
"Commands:\n"
|
|
196
|
+
" /secrets — list secret names available via the daemon\n"
|
|
197
|
+
" /info — show runtime info\n"
|
|
198
|
+
"Secret values never enter this container."
|
|
199
|
+
),
|
|
200
|
+
"tool": None,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if text == "/info":
|
|
204
|
+
llm = f" · llm=shroud:{SHROUD_PROVIDER}/{SHROUD_MODEL}" if LLM_VIA_SHROUD else " · llm=agent"
|
|
205
|
+
return {
|
|
206
|
+
"reply": (
|
|
207
|
+
f"Agent {AGENT_ID or '(local)'} · framework={FRAMEWORK} · "
|
|
208
|
+
f"vault={MODE} · chat_ui=:{PORT} · agent=:{AGENT_PORT}{llm}"
|
|
209
|
+
),
|
|
210
|
+
"tool": "info",
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if text == "/secrets":
|
|
214
|
+
path = f"/secrets?prefix={SECRET_PREFIX}" if SECRET_PREFIX else "/secrets"
|
|
215
|
+
try:
|
|
216
|
+
status, body = _daemon_request("GET", path)
|
|
217
|
+
if status != 200:
|
|
218
|
+
return {"reply": f"Daemon returned {status}", "tool": "list_secrets"}
|
|
219
|
+
names = [s.get("name", "") for s in (body or {}).get("secrets", [])]
|
|
220
|
+
names = [n for n in names if n]
|
|
221
|
+
reply = (
|
|
222
|
+
"Available secrets:\n" + "\n".join(f" • {n}" for n in names)
|
|
223
|
+
if names
|
|
224
|
+
else "No secrets available to this agent."
|
|
225
|
+
)
|
|
226
|
+
return {"reply": reply, "tool": "list_secrets"}
|
|
227
|
+
except OSError as exc:
|
|
228
|
+
return {"reply": f"Could not reach daemon: {exc}", "tool": "list_secrets"}
|
|
229
|
+
|
|
230
|
+
agent_out = _agent_chat(text)
|
|
231
|
+
if agent_out is not None:
|
|
232
|
+
return agent_out
|
|
233
|
+
|
|
234
|
+
if LLM_VIA_SHROUD and SHROUD_SECRET:
|
|
235
|
+
try:
|
|
236
|
+
ok, reply = _shroud_chat(text)
|
|
237
|
+
if ok:
|
|
238
|
+
with _lock:
|
|
239
|
+
_conversation.append({"role": "user", "content": text})
|
|
240
|
+
_conversation.append({"role": "assistant", "content": reply})
|
|
241
|
+
while len(_conversation) > HISTORY_MAX:
|
|
242
|
+
_conversation.pop(0)
|
|
243
|
+
return {"reply": reply, "tool": "shroud_llm"}
|
|
244
|
+
except OSError as exc:
|
|
245
|
+
return {"reply": f"Could not reach the daemon for LLM call: {exc}", "tool": "shroud_llm"}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
"reply": (
|
|
249
|
+
"No LLM configured. Re-run spawn with --agent-key ocv_... "
|
|
250
|
+
"and optional --llm-api-key. The host daemon injects Shroud credentials."
|
|
251
|
+
),
|
|
252
|
+
"tool": None,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class Handler(BaseHTTPRequestHandler):
|
|
257
|
+
def log_message(self, fmt: str, *args: Any) -> None:
|
|
258
|
+
sys.stderr.write(f"[spawn-chat-ui] {self.address_string()} - {fmt % args}\n")
|
|
259
|
+
|
|
260
|
+
def _json(self, status: int, obj: dict[str, Any]) -> None:
|
|
261
|
+
body = json.dumps(obj).encode("utf-8")
|
|
262
|
+
self.send_response(status)
|
|
263
|
+
self.send_header("Content-Type", "application/json")
|
|
264
|
+
self.send_header("Content-Length", str(len(body)))
|
|
265
|
+
self.end_headers()
|
|
266
|
+
self.wfile.write(body)
|
|
267
|
+
|
|
268
|
+
def do_GET(self) -> None:
|
|
269
|
+
path = self.path.split("?", 1)[0]
|
|
270
|
+
if path == "/health":
|
|
271
|
+
self._json(
|
|
272
|
+
200,
|
|
273
|
+
{
|
|
274
|
+
"status": "ok",
|
|
275
|
+
"framework": FRAMEWORK,
|
|
276
|
+
"agent_port": AGENT_PORT,
|
|
277
|
+
"llm_wired": LLM_VIA_SHROUD and bool(SHROUD_SECRET),
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
return
|
|
281
|
+
if path == "/api/info":
|
|
282
|
+
self._json(
|
|
283
|
+
200,
|
|
284
|
+
{
|
|
285
|
+
"agentId": AGENT_ID or None,
|
|
286
|
+
"framework": FRAMEWORK,
|
|
287
|
+
"runtime": "docker",
|
|
288
|
+
"mode": MODE,
|
|
289
|
+
"agentPort": AGENT_PORT,
|
|
290
|
+
"llm": (
|
|
291
|
+
{
|
|
292
|
+
"via": "shroud",
|
|
293
|
+
"provider": SHROUD_PROVIDER,
|
|
294
|
+
"model": SHROUD_MODEL,
|
|
295
|
+
}
|
|
296
|
+
if LLM_VIA_SHROUD
|
|
297
|
+
else None
|
|
298
|
+
),
|
|
299
|
+
"daemonReachable": _daemon_reachable(),
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
return
|
|
303
|
+
if path in ("/", "/index.html"):
|
|
304
|
+
html = INDEX_HTML.replace("__FRAMEWORK__", FRAMEWORK)
|
|
305
|
+
body = html.encode("utf-8")
|
|
306
|
+
self.send_response(200)
|
|
307
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
308
|
+
self.send_header("Content-Length", str(len(body)))
|
|
309
|
+
self.end_headers()
|
|
310
|
+
self.wfile.write(body)
|
|
311
|
+
return
|
|
312
|
+
self._json(404, {"error": "not found"})
|
|
313
|
+
|
|
314
|
+
def do_POST(self) -> None:
|
|
315
|
+
path = self.path.split("?", 1)[0]
|
|
316
|
+
if path != "/api/chat":
|
|
317
|
+
self._json(404, {"error": "not found"})
|
|
318
|
+
return
|
|
319
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
320
|
+
if length > 1024 * 1024:
|
|
321
|
+
self._json(413, {"error": "body too large"})
|
|
322
|
+
return
|
|
323
|
+
try:
|
|
324
|
+
payload = json.loads(self.rfile.read(length).decode("utf-8") or "{}")
|
|
325
|
+
except json.JSONDecodeError:
|
|
326
|
+
self._json(400, {"error": "invalid JSON"})
|
|
327
|
+
return
|
|
328
|
+
result = handle_chat(str(payload.get("message", "")))
|
|
329
|
+
self._json(200, result)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def main() -> None:
|
|
333
|
+
server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
|
|
334
|
+
print(
|
|
335
|
+
f"[spawn-chat-ui] listening on :{PORT} "
|
|
336
|
+
f"(framework={FRAMEWORK}, agent=:{AGENT_PORT}, mode={MODE})",
|
|
337
|
+
flush=True,
|
|
338
|
+
)
|
|
339
|
+
server.serve_forever()
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
if __name__ == "__main__":
|
|
343
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Start the framework agent on AGENT_INTERNAL_PORT, then run the shared chat UI
|
|
3
|
+
# on CHAT_UI_PORT (spawn publishes this port to localhost).
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
AGENT_INTERNAL_PORT="${AGENT_INTERNAL_PORT:-3001}"
|
|
7
|
+
CHAT_UI_PORT="${CHAT_UI_PORT:-3000}"
|
|
8
|
+
export AGENT_INTERNAL_PORT CHAT_UI_PORT
|
|
9
|
+
|
|
10
|
+
if [ $# -lt 1 ]; then
|
|
11
|
+
echo "Usage: start-with-chat-ui.sh <agent command...>" >&2
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
"$@" &
|
|
16
|
+
AGENT_PID=$!
|
|
17
|
+
|
|
18
|
+
cleanup() {
|
|
19
|
+
kill "$AGENT_PID" 2>/dev/null || true
|
|
20
|
+
}
|
|
21
|
+
trap cleanup EXIT INT TERM
|
|
22
|
+
|
|
23
|
+
i=0
|
|
24
|
+
while [ "$i" -lt 30 ]; do
|
|
25
|
+
if curl -sf "http://127.0.0.1:${AGENT_INTERNAL_PORT}/health" >/dev/null 2>&1; then
|
|
26
|
+
break
|
|
27
|
+
fi
|
|
28
|
+
i=$((i + 1))
|
|
29
|
+
sleep 0.5
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
echo "Ready: http://0.0.0.0:${CHAT_UI_PORT} (chat UI → agent on :${AGENT_INTERNAL_PORT})"
|
|
33
|
+
exec python3 /app/shared/spawn_chat_ui.py
|
|
@@ -78,6 +78,6 @@ Credentials stay in the host daemon — this container never sees secret values.
|
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
if __name__ == "__main__":
|
|
81
|
-
port = int(os.environ.get("
|
|
81
|
+
port = int(os.environ.get("AGENT_INTERNAL_PORT", "3001"))
|
|
82
82
|
print(f"AgentKit agent listening on http://0.0.0.0:{port}")
|
|
83
83
|
app.run(host="0.0.0.0", port=port)
|
|
@@ -81,6 +81,6 @@ Credentials stay in the host daemon — this container never sees secret values.
|
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
if __name__ == "__main__":
|
|
84
|
-
port = int(os.environ.get("
|
|
84
|
+
port = int(os.environ.get("AGENT_INTERNAL_PORT", "3001"))
|
|
85
85
|
print(f"Agno agent listening on http://0.0.0.0:{port}")
|
|
86
86
|
app.run(host="0.0.0.0", port=port)
|
|
@@ -114,6 +114,6 @@ Credentials stay in the host daemon — this container never sees secret values.
|
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
if __name__ == "__main__":
|
|
117
|
-
port = int(os.environ.get("
|
|
117
|
+
port = int(os.environ.get("AGENT_INTERNAL_PORT", "3001"))
|
|
118
118
|
print(f"Coder agent listening on http://0.0.0.0:{port}")
|
|
119
119
|
app.run(host="0.0.0.0", port=port)
|
|
@@ -104,6 +104,6 @@ Credentials stay in the host daemon — this container never sees secret values.
|
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
if __name__ == "__main__":
|
|
107
|
-
port = int(os.environ.get("
|
|
107
|
+
port = int(os.environ.get("AGENT_INTERNAL_PORT", "3001"))
|
|
108
108
|
print(f"CrewAI agent listening on http://0.0.0.0:{port}")
|
|
109
109
|
app.run(host="0.0.0.0", port=port)
|
|
@@ -11,7 +11,7 @@ const SHROUD_URL = process.env.ONECLAW_SHROUD_URL ?? "https://shroud.1claw.xyz";
|
|
|
11
11
|
const LLM_VIA_SHROUD = process.env.ONECLAW_LLM_VIA_SHROUD === "true";
|
|
12
12
|
const PROVIDER = process.env.ONECLAW_SHROUD_PROVIDER ?? "openai";
|
|
13
13
|
const MODEL = process.env.ONECLAW_SHROUD_MODEL ?? "gpt-4o-mini";
|
|
14
|
-
const PORT = parseInt(process.env.
|
|
14
|
+
const PORT = parseInt(process.env.AGENT_INTERNAL_PORT ?? "3001", 10);
|
|
15
15
|
|
|
16
16
|
const conversationHistory: { role: "user" | "assistant"; content: string }[] = [];
|
|
17
17
|
|