@logic-joe/fleek 0.2.0 → 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.
- package/dist/fleek.mjs +1028 -336
- package/package.json +1 -1
- package/service-agents/install-system-service +2 -2
- package/service-agents/knowledge-poll +232 -0
- package/service-agents/run-service +7 -0
package/package.json
CHANGED
|
@@ -10,8 +10,8 @@ broker_uid=$(id -u "$broker")
|
|
|
10
10
|
broker_home=$(getent passwd "$broker" | cut -d: -f6)
|
|
11
11
|
[ "$broker_uid" -gt 0 ] && [ "$broker_home" = /home/fa-review ] || exit 77
|
|
12
12
|
install -d -m 0755 /opt/fleek-agents
|
|
13
|
-
if [ "$source_dir" != /opt/fleek-agents ]; then install -m 0755 "$source_dir/run-job" "$source_dir/unlock-keyring" "$source_dir/run-service" "$source_dir/kit-setup" "$source_dir/broker-run" /opt/fleek-agents/; fi
|
|
14
|
-
chmod 0755 /opt/fleek-agents/run-job /opt/fleek-agents/unlock-keyring /opt/fleek-agents/run-service /opt/fleek-agents/kit-setup /opt/fleek-agents/broker-run
|
|
13
|
+
if [ "$source_dir" != /opt/fleek-agents ]; then install -m 0755 "$source_dir/run-job" "$source_dir/knowledge-poll" "$source_dir/unlock-keyring" "$source_dir/run-service" "$source_dir/kit-setup" "$source_dir/broker-run" /opt/fleek-agents/; fi
|
|
14
|
+
chmod 0755 /opt/fleek-agents/knowledge-poll /opt/fleek-agents/run-job /opt/fleek-agents/unlock-keyring /opt/fleek-agents/run-service /opt/fleek-agents/kit-setup /opt/fleek-agents/broker-run
|
|
15
15
|
# Every level of ~/.config belongs to the broker (install -d sets owner/mode only on the NAMED directories, so naming
|
|
16
16
|
# just .config/systemd/user left .config root-owned and `fleek login` could not create ~/.config/fleek - SVCLOGIN).
|
|
17
17
|
# Re-running the installer repairs an existing host: install -d also re-applies owner/mode to existing directories.
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""E10: one host-side poll, called every 15 minutes by the existing broker service.
|
|
3
|
+
The broker owns STATE_DIRECTORY/repository (an ordinary, clean clone with its own Git read login).
|
|
4
|
+
Only a changed default-branch HEAD runs fleek init; agents see the CLI's allowlisted staging, never the clone or broker keys.
|
|
5
|
+
"""
|
|
6
|
+
import datetime
|
|
7
|
+
import fcntl
|
|
8
|
+
import importlib.machinery
|
|
9
|
+
import importlib.util
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import re
|
|
14
|
+
import selectors
|
|
15
|
+
import signal
|
|
16
|
+
import stat
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import tempfile
|
|
20
|
+
import time
|
|
21
|
+
import uuid
|
|
22
|
+
|
|
23
|
+
loader = importlib.machinery.SourceFileLoader("fleek_starter", str(Path(__file__).with_name("run-job")))
|
|
24
|
+
spec = importlib.util.spec_from_loader(loader.name, loader)
|
|
25
|
+
starter = importlib.util.module_from_spec(spec)
|
|
26
|
+
loader.exec_module(starter)
|
|
27
|
+
SHA = re.compile(r"^[0-9a-f]{40}(?:[0-9a-f]{24})?$")
|
|
28
|
+
FLEEK = "/opt/fleek/bin/fleek"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def git(repo, *args):
|
|
32
|
+
# No checkout hooks, fsmonitor hooks, recursive submodules or repository-selected executable protocols.
|
|
33
|
+
result = subprocess.run(["git", "-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", "-c", "protocol.ext.allow=never",
|
|
34
|
+
"-c", "protocol.file.allow=never", *args], cwd=repo, stdin=subprocess.DEVNULL,
|
|
35
|
+
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=60, check=False)
|
|
36
|
+
if result.returncode or len(result.stdout) > 1024 * 1024:
|
|
37
|
+
raise ValueError("repository_not_readable")
|
|
38
|
+
return result.stdout.decode("utf-8", "strict")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def remote_head(repo):
|
|
42
|
+
lines = git(repo, "ls-remote", "--symref", "origin", "HEAD").splitlines()
|
|
43
|
+
branch = next((line[5:].split("\t", 1)[0] for line in lines if line.startswith("ref: ")), None)
|
|
44
|
+
commit = next((line.split("\t", 1)[0] for line in lines if line.endswith("\tHEAD") and not line.startswith("ref: ")), None)
|
|
45
|
+
if not branch or not re.fullmatch(r"refs/heads/[A-Za-z0-9_./-]{1,200}", branch) or ".." in branch or not commit or not SHA.fullmatch(commit):
|
|
46
|
+
raise ValueError("repository_not_readable")
|
|
47
|
+
return branch, commit
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def analyze(config, state, repo, profile):
|
|
51
|
+
if config.get("provider", "claude") != "claude":
|
|
52
|
+
raise ValueError("init_provider_unavailable")
|
|
53
|
+
poll_quota(state)
|
|
54
|
+
provider = starter.provider_env("claude")
|
|
55
|
+
# No Fleek/Git/keyring credential or repo customization enters this container.
|
|
56
|
+
with tempfile.TemporaryDirectory(prefix="knowledge-", dir=state) as temp:
|
|
57
|
+
job = Path(temp)
|
|
58
|
+
work = job / "work"
|
|
59
|
+
work.mkdir(mode=0o700)
|
|
60
|
+
proc = subprocess.Popen([FLEEK, "init", "--project", config["project"], "--refresh", "--maintenance", "--all-apps",
|
|
61
|
+
"--agent", "manual", "--yes", "--json", "--profile", profile, "--work-dir", str(work)], cwd=repo,
|
|
62
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, start_new_session=True)
|
|
63
|
+
containers = []
|
|
64
|
+
done = False
|
|
65
|
+
try:
|
|
66
|
+
selector = selectors.DefaultSelector()
|
|
67
|
+
selector.register(proc.stdout, selectors.EVENT_READ)
|
|
68
|
+
deadline = time.monotonic() + 3600
|
|
69
|
+
while proc.poll() is None or not done:
|
|
70
|
+
if time.monotonic() > deadline:
|
|
71
|
+
raise ValueError("analysis_timeout")
|
|
72
|
+
if not selector.select(timeout=10):
|
|
73
|
+
if proc.poll() is not None:
|
|
74
|
+
break
|
|
75
|
+
continue
|
|
76
|
+
line = proc.stdout.readline(65537)
|
|
77
|
+
if not line:
|
|
78
|
+
break
|
|
79
|
+
if len(line) > 65536 or not line.endswith(b"\n"):
|
|
80
|
+
raise ValueError("analysis_output_invalid")
|
|
81
|
+
event = json.loads(line)
|
|
82
|
+
if event.get("event") == "staged":
|
|
83
|
+
staging, output = Path(event["stagingDir"]), Path(event["outputFile"])
|
|
84
|
+
if not staging.resolve().is_relative_to(work.resolve()) or not output.parent.resolve().is_relative_to(work.resolve()):
|
|
85
|
+
raise ValueError("analysis_path_invalid")
|
|
86
|
+
if staging.is_symlink() or output.parent.is_symlink():
|
|
87
|
+
raise ValueError("analysis_path_invalid")
|
|
88
|
+
name = "fleek-init-" + uuid.uuid4().hex
|
|
89
|
+
containers.append(name)
|
|
90
|
+
argv = starter.container_args(config["image"], name, job, provider)
|
|
91
|
+
mount = f"type=bind,src={job / 'input'},dst=/job,ro=true"
|
|
92
|
+
argv[argv.index(mount)] = f"type=bind,src={staging},dst=/job,ro=true"
|
|
93
|
+
argv += ["claude", "-p", "Read fleek-input.json and the allowed files. Return ONLY the JSON object requested by FLEEK-INIT.md.",
|
|
94
|
+
"--output-format", "text", "--no-session-persistence", "--tools", "Read,Glob,Grep", "--allowedTools", "Read,Glob,Grep",
|
|
95
|
+
"--permission-mode", "dontAsk", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}',
|
|
96
|
+
"--settings", '{"disableAllHooks":true,"permissions":{"deny":["Read(/proc/**)","Read(/run/**)"]}}', "--setting-sources", "", "--append-system-prompt-file", "/job/FLEEK-INIT.md"]
|
|
97
|
+
# A file avoids retaining provider output/logs in memory or the journal; the service bounds files to 16 MiB.
|
|
98
|
+
target = output.with_suffix(".tmp")
|
|
99
|
+
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
|
100
|
+
with os.fdopen(fd, "wb") as stream:
|
|
101
|
+
agent = subprocess.run(argv, stdin=subprocess.DEVNULL, stdout=stream, stderr=subprocess.DEVNULL,
|
|
102
|
+
env={**os.environ, **provider}, timeout=1800, check=False)
|
|
103
|
+
if agent.returncode or target.stat().st_size > 65536:
|
|
104
|
+
raise ValueError("analysis_failed")
|
|
105
|
+
target.rename(output)
|
|
106
|
+
if event.get("event") == "error":
|
|
107
|
+
raise ValueError("analysis_failed")
|
|
108
|
+
if event.get("event") == "done":
|
|
109
|
+
done = event.get("failed") == 0
|
|
110
|
+
break
|
|
111
|
+
if proc.wait(timeout=30) or not done:
|
|
112
|
+
raise ValueError("analysis_failed")
|
|
113
|
+
finally:
|
|
114
|
+
if proc.poll() is None:
|
|
115
|
+
os.killpg(proc.pid, signal.SIGTERM)
|
|
116
|
+
try:
|
|
117
|
+
proc.wait(timeout=10)
|
|
118
|
+
except subprocess.TimeoutExpired:
|
|
119
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
120
|
+
proc.wait()
|
|
121
|
+
for name in containers:
|
|
122
|
+
subprocess.run(["podman", "rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30, check=False)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def write_state(path, record):
|
|
126
|
+
fd, temp = tempfile.mkstemp(prefix="knowledge-state-", dir=path.parent)
|
|
127
|
+
with os.fdopen(fd, "w") as stream:
|
|
128
|
+
json.dump(record, stream)
|
|
129
|
+
os.replace(temp, path)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def poll_quota(state):
|
|
133
|
+
# This poll never consumes the review worker's quota.json / 50 daily jobs.
|
|
134
|
+
path = state / "knowledge-quota.json"
|
|
135
|
+
today = datetime.datetime.now(datetime.timezone.utc).date().isoformat()
|
|
136
|
+
try:
|
|
137
|
+
data = json.loads(starter.private_file(path).read_text())
|
|
138
|
+
except (OSError, ValueError):
|
|
139
|
+
data = {}
|
|
140
|
+
count = data.get("count", 0) if data.get("date") == today else 0
|
|
141
|
+
if not isinstance(count, int) or count >= 8:
|
|
142
|
+
raise ValueError("knowledge_daily_limit")
|
|
143
|
+
write_state(path, {"date": today, "count": count + 1})
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def verified_profile(config):
|
|
147
|
+
# Profile files contain routing metadata only. Fix the exact configured installation before using a checkout binding.
|
|
148
|
+
directory = Path(os.environ.get("FLEEK_CONFIG_DIR") or Path(os.environ.get("XDG_CONFIG_HOME") or Path.home() / ".config") / "fleek")
|
|
149
|
+
profiles = json.loads(starter.private_file(directory / "profiles.json").read_text()).get("profiles", {})
|
|
150
|
+
matches = [key for key, value in profiles.items() if re.fullmatch(r"[0-9a-f]{32}", key)
|
|
151
|
+
and value.get("workspace", {}).get("id", "").lower() == config["workspaceId"]
|
|
152
|
+
and value.get("holder", {}).get("kind") == "installation"
|
|
153
|
+
and value.get("holder", {}).get("id", "").lower() == config["expectedInstallationId"] and not value.get("needsLogin")]
|
|
154
|
+
if len(matches) != 1:
|
|
155
|
+
raise ValueError("identity_unavailable")
|
|
156
|
+
profile = matches[0]
|
|
157
|
+
identity = starter.command([FLEEK, "whoami", "--profile", profile, "--json"])
|
|
158
|
+
if (identity.get("installation", {}).get("id", "").lower() != config["expectedInstallationId"]
|
|
159
|
+
or identity.get("workspace", {}).get("id", "").lower() != config["workspaceId"]
|
|
160
|
+
or identity.get("principal", {}).get("kind") != "service_account" or identity.get("grantClass") != "G4x"):
|
|
161
|
+
raise ValueError("identity_unavailable")
|
|
162
|
+
return profile
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def poll(config, state, profile=None):
|
|
166
|
+
profile = profile or verified_profile(config)
|
|
167
|
+
# Statements are independent of a merge and are drained even when Git is unchanged/unreadable.
|
|
168
|
+
statements_ok = True
|
|
169
|
+
try:
|
|
170
|
+
starter.command([FLEEK, "service", "knowledge-check", config["project"], "--profile", profile, "--json"])
|
|
171
|
+
except (SystemExit, ValueError, OSError, subprocess.SubprocessError):
|
|
172
|
+
statements_ok = False
|
|
173
|
+
repo = state / "repository"
|
|
174
|
+
info = repo.lstat()
|
|
175
|
+
if not stat.S_ISDIR(info.st_mode) or stat.S_ISLNK(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077:
|
|
176
|
+
raise ValueError("repository_not_readable")
|
|
177
|
+
branch, commit = remote_head(repo)
|
|
178
|
+
previous = state / "knowledge-checked.json"
|
|
179
|
+
try:
|
|
180
|
+
last = json.loads(starter.private_file(previous).read_text())
|
|
181
|
+
except (OSError, ValueError):
|
|
182
|
+
last = {}
|
|
183
|
+
scope = {"project": config["project"], "workspaceId": config.get("workspaceId"), "installationId": config.get("expectedInstallationId")}
|
|
184
|
+
if last.get("commit") == commit and all(last.get(key) == value for key, value in scope.items()):
|
|
185
|
+
return "checked" if statements_ok else "not_checkable"
|
|
186
|
+
failure_path = state / "knowledge-failed.json"
|
|
187
|
+
try:
|
|
188
|
+
failure = json.loads(starter.private_file(failure_path).read_text())
|
|
189
|
+
except (OSError, ValueError):
|
|
190
|
+
failure = {}
|
|
191
|
+
same_failure = failure.get("commit") == commit and all(failure.get(key) == value for key, value in scope.items())
|
|
192
|
+
if same_failure and (failure.get("count", 0) >= 3 or time.time() < failure.get("retryAt", 0)):
|
|
193
|
+
return "not_checkable"
|
|
194
|
+
if git(repo, "status", "--porcelain=v1").strip():
|
|
195
|
+
raise ValueError("repository_dirty")
|
|
196
|
+
git(repo, "fetch", "--no-tags", "--recurse-submodules=no", "origin", branch)
|
|
197
|
+
# Default-branch movement during the fetch is retryable; never record a different commit as checked.
|
|
198
|
+
fetched = git(repo, "rev-parse", "FETCH_HEAD").strip()
|
|
199
|
+
if fetched != commit:
|
|
200
|
+
raise ValueError("repository_changed")
|
|
201
|
+
git(repo, "checkout", "--detach", commit)
|
|
202
|
+
try:
|
|
203
|
+
analyze(config, state, repo, profile)
|
|
204
|
+
except (ValueError, OSError, KeyError, TypeError, AttributeError, subprocess.SubprocessError):
|
|
205
|
+
count = failure.get("count", 0) + 1 if same_failure else 1
|
|
206
|
+
write_state(failure_path, {**scope, "commit": commit, "count": count, "retryAt": time.time() + 6 * 3600})
|
|
207
|
+
raise
|
|
208
|
+
record = {**scope, "commit": commit, "checkedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(), "result": "checked"}
|
|
209
|
+
write_state(previous, record)
|
|
210
|
+
return "checked" if statements_ok else "not_checkable"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def main():
|
|
214
|
+
state = Path(os.environ["STATE_DIRECTORY"])
|
|
215
|
+
config = starter.load_config(sys.argv[1])
|
|
216
|
+
with os.fdopen(os.open(state / "knowledge-poll.lock", os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW, 0o600), "w") as lock:
|
|
217
|
+
try:
|
|
218
|
+
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
219
|
+
except BlockingIOError:
|
|
220
|
+
return 0
|
|
221
|
+
try:
|
|
222
|
+
result = poll(config, state)
|
|
223
|
+
print(json.dumps({"result": result}), flush=True)
|
|
224
|
+
return 0
|
|
225
|
+
except (SystemExit, ValueError, OSError, KeyError, TypeError, AttributeError, subprocess.SubprocessError):
|
|
226
|
+
# A fixed result only: never Git/provider stderr, code, configuration or credentials.
|
|
227
|
+
print('{"result":"not_checkable"}', flush=True)
|
|
228
|
+
return 1
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
sys.exit(main())
|
|
@@ -22,4 +22,11 @@ shift
|
|
|
22
22
|
sleep 60
|
|
23
23
|
done
|
|
24
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
|
+
) &
|
|
25
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
|