@logic-joe/fleek 0.1.3 → 0.2.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.
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env python3
2
+ """One ordinary headless CLI in a rootless container; never an agent implementation."""
3
+ import datetime
4
+ import fcntl
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import re
9
+ import shutil
10
+ import signal
11
+ import stat
12
+ import subprocess
13
+ import sys
14
+ import tempfile
15
+ import time
16
+ import uuid
17
+
18
+ DENIED = 77
19
+ LOGIN = 4
20
+ UUID = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
21
+
22
+
23
+ def private_file(path):
24
+ value = Path(path)
25
+ info = value.lstat()
26
+ if not stat.S_ISREG(info.st_mode) or info.st_mode & 0o077 or info.st_uid not in (0, os.getuid()):
27
+ raise ValueError("private file required")
28
+ return value
29
+
30
+
31
+ def load_config(path):
32
+ config = json.loads(private_file(path).read_text())
33
+ for field in ("expectedInstallationId", "workspaceId"):
34
+ if not UUID.fullmatch(config[field]):
35
+ raise ValueError("invalid identity")
36
+ config[field] = config[field].lower()
37
+ if not re.fullmatch(r"[A-Z0-9][A-Z0-9_]{0,19}", config["project"]):
38
+ raise ValueError("invalid project")
39
+ if config.get("provider", "claude") not in ("claude", "codex"):
40
+ raise ValueError("invalid provider")
41
+ if not re.fullmatch(r"[a-zA-Z0-9./:_-]+@sha256:[a-f0-9]{64}", config["image"]):
42
+ raise ValueError("image must be digest-pinned")
43
+ return config
44
+
45
+
46
+ def command(argv, cwd=None):
47
+ result = subprocess.run(argv, cwd=cwd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
48
+ stderr=subprocess.DEVNULL, timeout=60, check=False)
49
+ if result.returncode:
50
+ try:
51
+ code = json.loads(result.stdout).get("error")
52
+ except (ValueError, AttributeError):
53
+ code = None
54
+ # A kit change the CLI refuses without a terminal (first kit, new kit / MCP entry / files: CONFIRMATION_REQUIRED) needs
55
+ # the operator's one interactive confirmation (`kit-setup`); stop like a denial so the event is redelivered after it.
56
+ # USAGE: `--accept-approved` on a broker without a service-account login - an operator repair (fleek service setup)
57
+ raise SystemExit(DENIED if code in ("ACCESS_DENIED", "CONFIRMATION_REQUIRED", "DECLINED", "USAGE") else LOGIN if code in ("UNAUTHENTICATED", "NEEDS_LOGIN") else result.returncode if result.returncode in (DENIED, LOGIN) else 1)
58
+ if len(result.stdout) > 1024 * 1024:
59
+ raise ValueError("oversized broker output")
60
+ return json.loads(result.stdout)
61
+
62
+
63
+ def quota(root):
64
+ # One listener plus a file lock keeps the 50/day default across restarts and accidental duplicate services.
65
+ path = root / "quota.json"
66
+ with os.fdopen(os.open(path, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600), "r+") as stream:
67
+ os.fchmod(stream.fileno(), 0o600)
68
+ fcntl.flock(stream, fcntl.LOCK_EX)
69
+ stream.seek(0)
70
+ today = datetime.datetime.now(datetime.timezone.utc).date().isoformat()
71
+ raw = stream.read()
72
+ data = json.loads(raw) if raw else {"date": today, "count": 0}
73
+ count = data["count"] if data["date"] == today else 0
74
+ if count >= 50:
75
+ raise ValueError("daily limit")
76
+ stream.seek(0)
77
+ json.dump({"date": today, "count": count + 1}, stream)
78
+ stream.truncate()
79
+ stream.flush()
80
+ os.fsync(stream.fileno())
81
+
82
+
83
+ def provider_env(provider):
84
+ credentials = Path(os.environ["CREDENTIALS_DIRECTORY"])
85
+ names = ([("claude-oauth", "CLAUDE_CODE_OAUTH_TOKEN"), ("anthropic-key", "ANTHROPIC_API_KEY")]
86
+ if provider == "claude" else [("openai-key", "CODEX_API_KEY")])
87
+ for filename, variable in names:
88
+ path = credentials / filename
89
+ if path.exists():
90
+ value = private_file(path).read_text().strip()
91
+ if not value or "\n" in value or len(value) > 16384:
92
+ raise ValueError("invalid provider credential")
93
+ return {variable: value}
94
+ raise ValueError("provider credential missing")
95
+
96
+
97
+ def container_args(image, name, job, provider):
98
+ args = ["podman", "--cgroup-manager=cgroupfs", "run", "--rm", "--name", name, "--pull=never", "--read-only", "--userns=keep-id", "--cgroups=split",
99
+ f"--user={os.getuid()}:{os.getgid()}", "--cap-drop=ALL", "--security-opt=no-new-privileges", "--pids-limit=128", "--memory=1g", "--cpus=1",
100
+ "--log-driver=none", "--tmpfs=/home/agent:rw,nosuid,nodev,noexec,size=64m,mode=1777", "--tmpfs=/tmp:rw,nosuid,nodev,noexec,size=64m",
101
+ "--mount", f"type=bind,src={job / 'input'},dst=/job,ro=true", "--workdir=/job",
102
+ "--env=HOME=/home/agent/home", "--env=CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1", "--env=DISABLE_AUTOUPDATER=1",
103
+ "--env=DISABLE_TELEMETRY=1", "--env=CODEX_HOME=/home/agent/home/.codex"]
104
+ for variable in provider:
105
+ args.append(f"--env={variable}") # value inherited only by the captured container process, never argv
106
+ args.append(image)
107
+ return args
108
+
109
+
110
+ def agent_args(provider, event_id, issue_id):
111
+ prompt = f"Job review; eventId={event_id}; issueId={issue_id}."
112
+ if provider == "claude":
113
+ return ["claude", "-p", prompt, "--output-format", "json", "--max-turns", "30", "--tools", "",
114
+ "--allowedTools", "mcp__fleek__*", "--permission-mode", "dontAsk", "--strict-mcp-config",
115
+ "--mcp-config", "/job/mcp.json", "--settings", "/job/settings.json", "--setting-sources", "",
116
+ "--append-system-prompt-file", "/job/kit.md"]
117
+ return ["codex", "exec", "--json", "--skip-git-repo-check", "--sandbox", "read-only", "--ephemeral",
118
+ "-c", 'forced_login_method="api"', "-c", 'web_search="disabled"', "-c", "features.shell_tool=false",
119
+ "-c", "features.apply_patch_freeform=false", "-c", "features.multi_agent=false", "-c", "features.multi_agent_v2=false", "-c", "features.js_repl=false",
120
+ "-c", "features.code_mode=false", "-c", "features.code_mode_host=false", "-c", 'project_doc_max_bytes=0',
121
+ "-c", 'mcp_servers.fleek.command="socat"', "-c", 'mcp_servers.fleek.args=["STDIO","UNIX-CONNECT:/job/mcp.sock"]',
122
+ "-c", 'model_instructions_file="/job/kit.md"', prompt]
123
+
124
+
125
+ def kit_version(pulled):
126
+ kits = [kit for kit in pulled.get("kits", []) if isinstance(kit, dict)] if isinstance(pulled, dict) else []
127
+ main = next((kit for kit in kits if kit.get("kind") != "supplement"), kits[0] if kits else None)
128
+ version = main.get("version") if main else None
129
+ return str(version) if isinstance(version, int) and not isinstance(version, bool) and 0 < version < 10**9 else None
130
+
131
+
132
+ PROVIDER_AUTH_FAILED = re.compile(rb"(?i)(oauth token has expired|authentication_error|invalid api key|invalid bearer token|\b401\b)")
133
+
134
+
135
+ def provider_state(state):
136
+ # ok | expired: the outcome of the last agent run's provider sign-in (file written by run-job; nothing else in it)
137
+ try:
138
+ value = (state / "provider-auth").read_text().strip()
139
+ except OSError:
140
+ return "ok"
141
+ return value if value in ("ok", "expired") else "ok"
142
+
143
+
144
+ def host_report(fleek, provider_auth, kit=None, last_job_at=None, timeout=20):
145
+ # WZ-CLI-SVC: the host state for Fleek's "Alles grün" (serviceHosts.report). Best effort: never changes the job's outcome.
146
+ argv = [fleek, "service", "host-report", "--provider-auth", provider_auth, "--json"]
147
+ if kit:
148
+ argv += ["--kit-version", kit]
149
+ if last_job_at:
150
+ argv += ["--last-job-at", last_job_at]
151
+ try:
152
+ subprocess.run(argv, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=timeout, check=False)
153
+ except (OSError, subprocess.SubprocessError):
154
+ pass
155
+
156
+
157
+ def kit_text(fleek, project, cwd):
158
+ # WZ-CLI-SVC (founder 25.10.2026): new kit versions an admin released, signed and pinned to this project are taken over
159
+ # without a root terminal (--accept-approved: service-account logins only; kit MCP entries are only noted - the container
160
+ # below still gets nothing but the Fleek socket).
161
+ pulled = command([fleek, "kit", "pull", project, "--target", "claude", "--yes", "--accept-approved", "--json"], cwd)
162
+ host_report(fleek, provider_state(Path(os.environ["STATE_DIRECTORY"])), kit_version(pulled))
163
+ launch = command([fleek, "kit", "run", "claude", "--project", project, "--dry-run", "--json"], cwd)
164
+ args = launch["args"]
165
+ parts = []
166
+ for index, arg in enumerate(args):
167
+ if arg == "--append-system-prompt":
168
+ parts.append(args[index + 1])
169
+ elif arg == "--plugin-dir":
170
+ plugin = Path(args[index + 1]).resolve(strict=True)
171
+ # Only plain Markdown enters the container; no plugin hooks, scripts, MCP entries or broker paths.
172
+ for file in sorted(plugin.rglob("*.md")):
173
+ if file.is_symlink() or not file.resolve().is_relative_to(plugin):
174
+ raise ValueError("unsafe kit")
175
+ parts.append(file.read_text())
176
+ text = "\n\n".join(parts)
177
+ if not text or len(text.encode()) > 120000:
178
+ raise ValueError("invalid kit")
179
+ return text
180
+
181
+
182
+ def run(config_path):
183
+ if os.getuid() == 0:
184
+ return DENIED
185
+ config = load_config(config_path)
186
+ raw = sys.stdin.buffer.read(65537)
187
+ if len(raw) > 65536:
188
+ raise ValueError("oversized event")
189
+ event = json.loads(raw)
190
+ event_id, issue_id = event.get("eventId", ""), event.get("aggregateId", "")
191
+ # AUTO-AGENT: "Agent startet automatisch" - In Prüfung keeps its own event, every other agent step wakes with agent_work_requested
192
+ if event.get("eventType") not in ("issue.intake_review_requested", "issue.agent_work_requested") or event.get("aggregateType") != "issue" or not UUID.fullmatch(event_id) or not UUID.fullmatch(issue_id):
193
+ return 64 # listener makes the durable metadata-only dead letter; no unknown event is acknowledged
194
+ event_id, issue_id = event_id.lower(), issue_id.lower()
195
+ fleek = config.get("fleek", "/opt/fleek/bin/fleek")
196
+ identity = command([fleek, "whoami", "--json"])
197
+ if identity.get("installation", {}).get("id", "").lower() != config["expectedInstallationId"] or identity.get("workspace", {}).get("id", "").lower() != config["workspaceId"]:
198
+ return DENIED
199
+ context = command([fleek, "context", issue_id, "--json"])
200
+ subject = context.get("subject", {})
201
+ if subject.get("id", "").lower() != issue_id or not re.fullmatch(re.escape(config["project"]) + r"-[1-9][0-9]*", subject.get("identifier", "")):
202
+ return 64
203
+ state = Path(os.environ["STATE_DIRECTORY"])
204
+ if not state.is_dir() or state.is_symlink() or state.stat().st_uid != os.getuid() or state.stat().st_mode & 0o077:
205
+ raise ValueError("private state required")
206
+ quota(state)
207
+ # Per-broker serialization covers a second listener started by mistake as well.
208
+ with (state / "job.lock").open("a") as lock:
209
+ os.chmod(state / "job.lock", 0o600)
210
+ fcntl.flock(lock, fcntl.LOCK_EX)
211
+ job = Path(tempfile.mkdtemp(prefix="job-", dir=os.environ["RUNTIME_DIRECTORY"]))
212
+ proxy = agent = None
213
+ name = "fleek-" + uuid.uuid4().hex
214
+ exit_code = 1
215
+ def outcome(code):
216
+ nonlocal exit_code
217
+ exit_code = code
218
+ return code
219
+ started = time.monotonic()
220
+ provider = config.get("provider", "claude")
221
+ container_env = {key: os.environ[key] for key in ("PATH", "HOME", "XDG_RUNTIME_DIR") if key in os.environ}
222
+ try:
223
+ inputs = job / "input"
224
+ inputs.mkdir(mode=0o700)
225
+ (inputs / "kit.md").write_text(kit_text(fleek, config["project"], job))
226
+ (inputs / "mcp.json").write_text(json.dumps({"mcpServers": {"fleek": {"command": "socat", "args": ["STDIO", "UNIX-CONNECT:/job/mcp.sock"]}}}))
227
+ (inputs / "settings.json").write_text(json.dumps({"permissions": {"deny": ["Read(/proc/**)", "Read(/run/**)"]}}))
228
+ with (job / "proxy.log").open("wb") as proxy_log, (job / "agent.stdout").open("wb") as stdout, (job / "agent.stderr").open("wb") as stderr:
229
+ proxy = subprocess.Popen([fleek, "mcp", "serve", "--socket", str(inputs / "mcp.sock"), "--job", event_id], stdout=proxy_log, stderr=proxy_log)
230
+ deadline = time.monotonic() + 10
231
+ while not (inputs / "mcp.sock").exists():
232
+ if proxy.poll() is not None:
233
+ return outcome(proxy.returncode)
234
+ if time.monotonic() >= deadline:
235
+ return outcome(1)
236
+ time.sleep(0.05)
237
+ container_env.update(provider_env(provider))
238
+ argv = container_args(config["image"], name, job, {key: value for key, value in container_env.items() if key in ("CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "CODEX_API_KEY")})
239
+ agent = subprocess.Popen(argv + agent_args(provider, event_id, issue_id), env=container_env, stdin=subprocess.DEVNULL, stdout=stdout, stderr=stderr)
240
+ while agent.poll() is None:
241
+ if proxy.poll() is not None:
242
+ return outcome(proxy.returncode if proxy.returncode in (DENIED, LOGIN) else 1)
243
+ if time.monotonic() - started >= 600:
244
+ return outcome(75)
245
+ time.sleep(0.1)
246
+ # A just-denied proxy may still be exiting when the CLI returns. Never acknowledge that race.
247
+ try:
248
+ proxy.wait(timeout=1)
249
+ except subprocess.TimeoutExpired:
250
+ pass
251
+ exit_code = proxy.returncode if proxy.poll() in (DENIED, LOGIN) else 0 if agent.returncode == 0 else 1
252
+ # provider sign-in outcome for the host report (only "ok"/"expired" is stored, never agent text)
253
+ if agent.returncode == 0 or exit_code in (DENIED, LOGIN):
254
+ auth = "ok" if agent.returncode == 0 else None
255
+ else:
256
+ with (job / "agent.stdout").open("rb") as out, (job / "agent.stderr").open("rb") as err:
257
+ auth = "expired" if PROVIDER_AUTH_FAILED.search(out.read(1024 * 1024) + err.read(1024 * 1024)) else None
258
+ if auth:
259
+ fd = os.open(state / "provider-auth", os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW, 0o600)
260
+ with os.fdopen(fd, "w") as stream:
261
+ stream.write(auth)
262
+ return exit_code
263
+ except SystemExit as error:
264
+ exit_code = error.code if isinstance(error.code, int) else 1
265
+ raise
266
+ finally:
267
+ # systemd and the listener can both send TERM; teardown must not be interrupted twice.
268
+ for sig in (signal.SIGTERM, signal.SIGINT):
269
+ signal.signal(sig, signal.SIG_IGN)
270
+ # Container removal is required even when Podman's client process already exited or was signaled.
271
+ cleanup_failed = False
272
+ try:
273
+ result = subprocess.run(["podman", "--cgroup-manager=cgroupfs", "rm", "--force", "--time=0", "--ignore", name], env=container_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5, check=False)
274
+ cleanup_failed = result.returncode != 0
275
+ except (OSError, subprocess.SubprocessError):
276
+ cleanup_failed = True
277
+ finally:
278
+ for child in (agent, proxy):
279
+ if child and child.poll() is None:
280
+ child.terminate()
281
+ try:
282
+ child.wait(timeout=2)
283
+ except subprocess.TimeoutExpired:
284
+ child.kill()
285
+ child.wait(timeout=2)
286
+ shutil.rmtree(job)
287
+ if cleanup_failed:
288
+ exit_code = DENIED
289
+ stamp = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
290
+ try:
291
+ last = Path(os.environ["STATE_DIRECTORY"]) / "last-job-at"
292
+ fd = os.open(last, os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW, 0o600)
293
+ with os.fdopen(fd, "w") as stream:
294
+ stream.write(stamp)
295
+ except (OSError, KeyError):
296
+ pass
297
+ host_report(fleek, provider_state(state), None, stamp, timeout=5)
298
+ # Only validated IDs and numeric metadata ever reach journald. No agent-derived text/statistics.
299
+ print(json.dumps({"eventId": event_id, "kind": "review", "exit": exit_code, "durationMs": int((time.monotonic() - started) * 1000), "cleanupFailed": cleanup_failed}), flush=True)
300
+ if cleanup_failed:
301
+ raise SystemExit(DENIED)
302
+
303
+
304
+ def interrupted(_signal, _frame):
305
+ raise SystemExit(75)
306
+
307
+
308
+ if __name__ == "__main__":
309
+ os.umask(0o077)
310
+ for sig in (signal.SIGTERM, signal.SIGINT):
311
+ signal.signal(sig, interrupted)
312
+ try:
313
+ result = run(sys.argv[1])
314
+ except (ValueError, KeyError, TypeError, OSError, subprocess.SubprocessError):
315
+ result = 1 # exception text can contain credentials or event content; never log it
316
+ sys.exit(result)
@@ -0,0 +1,32 @@
1
+ #!/bin/sh
2
+ # Both system and user units keep D-Bus + the unlocked OS keyring in the broker's own service cgroup.
3
+ set -eu
4
+ export GNOME_KEYRING_CONTROL="$RUNTIME_DIRECTORY/keyring"
5
+ mkdir -p -m 0700 "$GNOME_KEYRING_CONTROL"
6
+ if [ "${1:-}" != --inside-private-bus ]; then
7
+ exec dbus-run-session -- "$0" --inside-private-bus "$@"
8
+ fi
9
+ shift
10
+ [ "${1:-}" = review ] && [ "${2:-}" = /etc/fleek-agents/review.json ] || exit 2
11
+ /opt/fleek-agents/unlock-keyring
12
+ # WZ-CLI-SVC: host state for Fleek every minute (contract: "Alles grün" needs a report younger than 5 minutes). Same unit, same keyring;
13
+ # KillMode=control-group ends it with the service. The unit only starts with its provider credential: "ok" unless the last
14
+ # job recorded a failed provider sign-in ("expired", file written by run-job).
15
+ (
16
+ while :; do
17
+ auth=$(cat "$STATE_DIRECTORY/provider-auth" 2>/dev/null || true)
18
+ case $auth in expired) ;; *) auth=ok ;; esac
19
+ last=$(cat "$STATE_DIRECTORY/last-job-at" 2>/dev/null || true)
20
+ case $last in [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]Z) set -- --last-job-at "$last" ;; *) set -- ;; esac
21
+ /opt/fleek/bin/fleek service host-report --provider-auth "$auth" "$@" --json >/dev/null 2>&1 || true
22
+ sleep 60
23
+ done
24
+ ) &
25
+ # E10: the broker's existing private bus/keyring/cgroup; no server job or per-drift notification.
26
+ (
27
+ while :; do
28
+ /opt/fleek-agents/knowledge-poll /etc/fleek-agents/review.json || true
29
+ sleep 900
30
+ done
31
+ ) &
32
+ exec /opt/fleek/bin/fleek listen --name review --concurrency 1 --timeout 660 --quiet-children --exec /opt/fleek-agents/run-job /etc/fleek-agents/review.json
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ set -eu
3
+ # No shell expansion of credential values, argv, files or terminal output.
4
+ exec gnome-keyring-daemon --unlock --components=secrets --control-directory="$GNOME_KEYRING_CONTROL" < "$CREDENTIALS_DIRECTORY/keyring-unlock" > /dev/null 2>&1