@logic-joe/fleek 0.1.2 → 0.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logic-joe/fleek",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "fleek - command line client for the Fleek agent hub (MCP over OAuth, OS keychain, signed role/project kits)",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist/fleek.mjs",
12
+ "service-agents",
12
13
  "LICENSE",
13
14
  "THIRD_PARTY_NOTICES.txt"
14
15
  ],
@@ -0,0 +1,13 @@
1
+ # Build on fleek-agents after choosing audited base/image digests. Never build on the coordinator Mac.
2
+ ARG BASE_IMAGE
3
+ FROM ${BASE_IMAGE}
4
+ USER root
5
+ RUN apt-get update && apt-get install -y --no-install-recommends socat ca-certificates \
6
+ && rm -rf /var/lib/apt/lists/*
7
+ # Versions match packages/fleek-cli/src/kit-capabilities.ts; reverify tool restrictions on any upgrade.
8
+ RUN npm install -g --allow-scripts=@anthropic-ai/claude-code @anthropic-ai/claude-code@2.1.303 @openai/codex@0.161.0
9
+ COPY container-entrypoint /usr/local/bin/fleek-agent-entrypoint
10
+ RUN chmod 0755 /usr/local/bin/fleek-agent-entrypoint
11
+ USER node
12
+ WORKDIR /job
13
+ ENTRYPOINT ["/usr/local/bin/fleek-agent-entrypoint"]
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ # WZ-CLI-SVC: runs ONE `fleek` command as the service broker - its own profile (HOME), its OS keyring unlocked from the service's
3
+ # encrypted credential on a private D-Bus, working directory / (outside every git checkout). Same arrangement as kit-setup and
4
+ # run-service; `fleek service setup|status|renew` (root) use it for the broker's login, whoami, claim, reports and kit pull.
5
+ # Usage (root): broker-run review pty|pipe <purpose> [FLEEK_SETUP_CODE] -- <fleek arguments>
6
+ # pty = a person at the terminal (device login); pipe = captured output (--json documents)
7
+ # FLEEK_SETUP_CODE = take the wizard's setup code from the caller's environment and hand it to the inner fleek as a unit
8
+ # CREDENTIAL (root-only 0600 file, LoadCredential): never argv, never a unit property (`systemctl show` is readable by every
9
+ # user; -E would put it there), never the journal.
10
+ set -euo pipefail
11
+ if [ "${1:-}" != --as-broker ]; then
12
+ [ "$(id -u)" = 0 ] || { echo 'root required' >&2; exit 77; }
13
+ kind=${1:-}; mode=${2:-}; purpose=${3:-}
14
+ [ "$kind" = review ] || { echo 'only the review starter is enabled' >&2; exit 64; }
15
+ case $mode in pty|pipe) ;; *) exit 64 ;; esac
16
+ [[ "$purpose" =~ ^[a-z][a-z-]{0,23}$ ]] || exit 64
17
+ shift 3
18
+ credential=()
19
+ if [ "${1:-}" = FLEEK_SETUP_CODE ]; then
20
+ [[ "${FLEEK_SETUP_CODE:-}" =~ ^[BCDFGHJKLMNPQRSTVWXZ]{8}$ ]] || exit 64
21
+ secret_dir=$(mktemp -d /run/fleek-setup-code-XXXXXX)
22
+ trap 'rm -rf "$secret_dir"' EXIT
23
+ printf '%s' "$FLEEK_SETUP_CODE" > "$secret_dir/setup-code"
24
+ chmod 0600 "$secret_dir/setup-code"
25
+ credential=(-p "LoadCredential=setup-code:$secret_dir/setup-code")
26
+ shift
27
+ fi
28
+ [ "${1:-}" = -- ] || exit 64
29
+ shift
30
+ broker=fa-$kind
31
+ broker_uid=$(id -u "$broker")
32
+ broker_home=$(getent passwd "$broker" | cut -d: -f6)
33
+ [ "$broker_uid" -gt 0 ] && [ "$broker_home" = "/home/$broker" ] || exit 77
34
+ # not exec: the credential file is removed when the unit ended (EXIT trap)
35
+ systemd-run --quiet "--$mode" --wait --collect --uid="$broker" --gid="$broker" --working-directory=/ \
36
+ --description="fleek $purpose" -p UMask=0077 -p "RuntimeDirectory=fleek-$purpose-$kind-$$" -p RuntimeDirectoryMode=0700 \
37
+ -p "LoadCredentialEncrypted=keyring-unlock:/etc/credstore.encrypted/fleek-$kind-keyring-unlock" \
38
+ -E "HOME=$broker_home" -E PATH=/opt/fleek/bin:/usr/local/bin:/usr/bin:/bin -E "XDG_RUNTIME_DIR=/run/user/$broker_uid" \
39
+ -E "LANG=${LANG:-C.UTF-8}" "${credential[@]}" \
40
+ /opt/fleek-agents/broker-run --as-broker "$@"
41
+ exit $?
42
+ fi
43
+ shift
44
+ if [ "${1:-}" != --inside-private-bus ]; then
45
+ export GNOME_KEYRING_CONTROL="$RUNTIME_DIRECTORY/keyring"
46
+ mkdir -p -m 0700 "$GNOME_KEYRING_CONTROL"
47
+ exec dbus-run-session -- "$0" --as-broker --inside-private-bus "$@"
48
+ fi
49
+ shift
50
+ /opt/fleek-agents/unlock-keyring
51
+ if [ -r "${CREDENTIALS_DIRECTORY:-/nonexistent}/setup-code" ]; then
52
+ FLEEK_SETUP_CODE=$(cat "$CREDENTIALS_DIRECTORY/setup-code")
53
+ export FLEEK_SETUP_CODE
54
+ fi
55
+ exec /opt/fleek/bin/fleek "$@"
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+ set -eu
3
+ umask 077
4
+ mkdir -p -m 0700 "$HOME"
5
+ chmod 0700 "$HOME"
6
+ if [ -n "${CODEX_HOME:-}" ]; then
7
+ mkdir -p -m 0700 "$CODEX_HOME"
8
+ chmod 0700 "$CODEX_HOME"
9
+ fi
10
+ exec "$@"
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env python3
2
+ """Root-owned broker egress: fixed approved DNS names, current public addresses, no private/link-local routes."""
3
+ import ipaddress
4
+ import os
5
+ import pwd
6
+ import socket
7
+ import subprocess
8
+
9
+ if os.getuid() != 0:
10
+ raise SystemExit(77)
11
+ # platform.claude.com: token exchange of `claude setup-token` (WZ-CLI-SVC provider sign-in in the agent image, pre-check 25.10.2026)
12
+ hosts = ("api.onfleek.app", "mcp.onfleek.app", "app.onfleek.app", "api.anthropic.com", "claude.ai", "platform.claude.com", "api.openai.com", "github.com")
13
+ addresses = {4: set(), 6: set()}
14
+ for host in hosts:
15
+ for family, _type, _proto, _name, endpoint in socket.getaddrinfo(host, 443, type=socket.SOCK_STREAM):
16
+ address = ipaddress.ip_address(endpoint[0])
17
+ if not address.is_global:
18
+ raise SystemExit("refusing non-public egress address")
19
+ addresses[address.version].add(str(address))
20
+ uid = pwd.getpwnam("fa-review").pw_uid
21
+ if uid == 0 or not addresses[4]:
22
+ raise SystemExit(77)
23
+ subprocess.run(["nft", "add", "table", "inet", "fleek_agents"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
24
+ rules = f"""delete table inet fleek_agents
25
+ table inet fleek_agents {{
26
+ set allow4 {{ type ipv4_addr; elements = {{ {', '.join(sorted(addresses[4]))} }} }}
27
+ set allow6 {{ type ipv6_addr; elements = {{ {', '.join(sorted(addresses[6]))} }} }}
28
+ chain output {{ type filter hook output priority -10; policy accept;
29
+ meta skuid {uid} jump broker
30
+ }}
31
+ chain broker {{
32
+ ct state established,related accept
33
+ ip daddr 127.0.0.53 udp dport 53 accept
34
+ ip daddr 127.0.0.53 tcp dport 53 accept
35
+ ip daddr {{ 0.0.0.0/8,10.0.0.0/8,100.64.0.0/10,127.0.0.0/8,169.254.0.0/16,172.16.0.0/12,192.168.0.0/16 }} reject
36
+ ip6 daddr {{ ::1/128,fc00::/7,fe80::/10 }} reject
37
+ ip daddr @allow4 tcp dport 443 accept
38
+ ip6 daddr @allow6 tcp dport 443 accept
39
+ reject with icmpx type admin-prohibited
40
+ }}
41
+ }}
42
+ """
43
+ subprocess.run(["nft", "-f", "-"], input=rules, text=True, check=True)
44
+ print(f"fleek egress updated: uid={uid} hosts={len(hosts)} IPv4={len(addresses[4])} IPv6={len(addresses[6])}")
@@ -0,0 +1,32 @@
1
+ # Install as a USER service under the dedicated fa-<kind> broker (loginctl enable-linger).
2
+ [Unit]
3
+ Description=Fleek service agent %i
4
+ StartLimitIntervalSec=300
5
+ StartLimitBurst=3
6
+
7
+ [Service]
8
+ Type=simple
9
+ UMask=0077
10
+ Environment=PATH=/opt/fleek/bin:/usr/local/bin:/usr/bin:/bin
11
+ StateDirectory=fleek-agent-%i
12
+ StateDirectoryMode=0700
13
+ RuntimeDirectory=fleek-agent-%i
14
+ RuntimeDirectoryMode=0700
15
+ # Encrypted files are created on this machine. Configure ONE provider (OAuth preferred for Claude).
16
+ LoadCredentialEncrypted=claude-oauth:%h/.config/fleek-agent/claude-oauth.cred
17
+ # Alternative: replace the line above with anthropic-key or openai-key (not all three).
18
+ LoadCredentialEncrypted=keyring-unlock:%h/.config/fleek-agent/keyring-unlock.cred
19
+ ExecStart=/opt/fleek-agents/run-service %i /etc/fleek-agents/%i.json
20
+ Restart=on-failure
21
+ RestartSec=30
22
+ RestartPreventExitStatus=4 77
23
+ KillMode=control-group
24
+ TimeoutStopSec=10
25
+ LimitFSIZE=16M
26
+ # Rootless Podman needs user namespaces/cgroup delegation; do not use PrivateUsers/NoNewPrivileges here.
27
+ Delegate=yes
28
+ StandardOutput=journal
29
+ StandardError=journal
30
+
31
+ [Install]
32
+ WantedBy=default.target
@@ -0,0 +1,8 @@
1
+ [Unit]
2
+ Description=Refresh approved Fleek broker egress addresses
3
+ Wants=network-online.target
4
+ After=network-online.target
5
+
6
+ [Service]
7
+ Type=oneshot
8
+ ExecStart=/opt/fleek-agents/egress-allowlist
@@ -0,0 +1,10 @@
1
+ [Unit]
2
+ Description=Keep Fleek broker egress in sync with approved DNS names
3
+
4
+ [Timer]
5
+ OnBootSec=30
6
+ OnUnitActiveSec=300
7
+ Unit=fleek-egress.service
8
+
9
+ [Install]
10
+ WantedBy=timers.target
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # Root-only installer for systemd255 hosts. Encrypted credentials remain host-managed; the process runs as fa-review.
3
+ set -euo pipefail
4
+ [ "$(id -u)" = 0 ] || { echo 'root required' >&2; exit 77; }
5
+ kind=${1:-review}
6
+ [ "$kind" = review ] || { echo 'only the review starter is enabled' >&2; exit 64; }
7
+ source_dir=$(cd "$(dirname "$0")" && pwd)
8
+ broker=fa-$kind
9
+ broker_uid=$(id -u "$broker")
10
+ broker_home=$(getent passwd "$broker" | cut -d: -f6)
11
+ [ "$broker_uid" -gt 0 ] && [ "$broker_home" = /home/fa-review ] || exit 77
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
15
+ # Every level of ~/.config belongs to the broker (install -d sets owner/mode only on the NAMED directories, so naming
16
+ # just .config/systemd/user left .config root-owned and `fleek login` could not create ~/.config/fleek - SVCLOGIN).
17
+ # Re-running the installer repairs an existing host: install -d also re-applies owner/mode to existing directories.
18
+ install -d -m 0700 -o "$broker" -g "$broker" "$broker_home/.config" "$broker_home/.config/systemd" "$broker_home/.config/systemd/user"
19
+ install -d -m 0750 -o root -g "$broker" /etc/fleek-agents
20
+ install -d -m 0700 /etc/credstore.encrypted
21
+ if [ "$source_dir" != /opt/fleek-agents ]; then install -m 0755 "$source_dir/egress-allowlist" /opt/fleek-agents/; fi
22
+ install -m 0644 "$source_dir/fleek-egress.service" "$source_dir/fleek-egress.timer" /etc/systemd/system/
23
+ cat > "/etc/systemd/system/fleek-agent-$kind.service" <<UNIT
24
+ [Unit]
25
+ Description=Fleek review service agent
26
+ StartLimitIntervalSec=300
27
+ StartLimitBurst=3
28
+ Requires=user@$broker_uid.service
29
+ After=network-online.target user@$broker_uid.service fleek-egress.service
30
+ Requires=fleek-egress.service
31
+ Wants=network-online.target
32
+
33
+ [Service]
34
+ Type=simple
35
+ User=$broker
36
+ Group=$broker
37
+ UMask=0077
38
+ Environment=HOME=$broker_home
39
+ Environment=PATH=/opt/fleek/bin:/usr/local/bin:/usr/bin:/bin
40
+ Environment=XDG_RUNTIME_DIR=/run/user/$broker_uid
41
+ RuntimeDirectory=fleek-agent-$kind
42
+ RuntimeDirectoryMode=0700
43
+ StateDirectory=fleek-agent-$kind
44
+ StateDirectoryMode=0700
45
+ LoadCredentialEncrypted=claude-oauth:/etc/credstore.encrypted/fleek-$kind-claude-oauth
46
+ LoadCredentialEncrypted=keyring-unlock:/etc/credstore.encrypted/fleek-$kind-keyring-unlock
47
+ ExecStart=/opt/fleek-agents/run-service $kind /etc/fleek-agents/$kind.json
48
+ Restart=on-failure
49
+ RestartSec=30
50
+ RestartPreventExitStatus=4 77
51
+ KillMode=control-group
52
+ TimeoutStopSec=10
53
+ LimitFSIZE=16M
54
+ Delegate=yes
55
+ StandardOutput=journal
56
+ StandardError=journal
57
+
58
+ [Install]
59
+ WantedBy=multi-user.target
60
+ UNIT
61
+ loginctl enable-linger "$broker"
62
+ systemctl start "user@$broker_uid.service"
63
+ systemctl daemon-reload
64
+ systemctl enable --now fleek-egress.timer
65
+ systemctl start fleek-egress.service
66
+ # Deliberately not enabled/started: identity, image, provider credential and host acceptance must be ready first
67
+ # (`fleek service setup` enables it once they are). Re-running this on a running host keeps the unit's state.
68
+ printf '%s\n' "Installed disabled fleek-agent-$kind.service for $broker (uid $broker_uid)"
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env bash
2
+ # KIT-PROVISION for a service broker (root, at a terminal): the ONE interactive kit confirmation an operator gives.
3
+ # run-job pulls kits unattended (`fleek kit pull <project> --target claude --yes --json`, stdin /dev/null). The CLI refuses a
4
+ # first kit, a new kit, a new MCP entry or new kit files without a terminal (CONFIRMATION_REQUIRED, run-job exits 77) - that
5
+ # boundary stays. This runs `fleek kit setup` once as the broker with the service's profile (HOME), its OS keyring (private
6
+ # D-Bus, unlocked from the service's own encrypted credential) and a working directory outside every git checkout (like
7
+ # run-job's job directory), then repeats run-job's exact pull without a terminal to prove it needs no confirmation now.
8
+ # Usage: sudo /opt/fleek-agents/kit-setup review (then: systemctl restart fleek-agent-review.service)
9
+ set -euo pipefail
10
+ if [ "${1:-}" != --as-broker ]; then
11
+ [ "$(id -u)" = 0 ] || { echo 'root required' >&2; exit 77; }
12
+ kind=${1:-review}
13
+ [ "$kind" = review ] || { echo 'only the review starter is enabled' >&2; exit 64; }
14
+ [ -t 0 ] && [ -t 1 ] || { echo 'an interactive terminal is required (the confirmation is a person at a terminal)' >&2; exit 3; }
15
+ broker=fa-$kind
16
+ broker_uid=$(id -u "$broker")
17
+ broker_home=$(getent passwd "$broker" | cut -d: -f6)
18
+ [ "$broker_uid" -gt 0 ] && [ "$broker_home" = "/home/$broker" ] || exit 77
19
+ project=$(python3 -c 'import json, re, sys
20
+ p = json.load(open(sys.argv[1]))["project"]
21
+ if not re.fullmatch(r"[A-Z0-9][A-Z0-9_]{0,19}", p): sys.exit(2)
22
+ print(p)' "/etc/fleek-agents/$kind.json")
23
+ exec systemd-run --quiet --pty --wait --collect --uid="$broker" --gid="$broker" --working-directory=/ \
24
+ -p UMask=0077 -p "RuntimeDirectory=fleek-kit-setup-$kind" -p RuntimeDirectoryMode=0700 \
25
+ -p "LoadCredentialEncrypted=keyring-unlock:/etc/credstore.encrypted/fleek-$kind-keyring-unlock" \
26
+ -E "HOME=$broker_home" -E PATH=/opt/fleek/bin:/usr/local/bin:/usr/bin:/bin -E "XDG_RUNTIME_DIR=/run/user/$broker_uid" \
27
+ -E "LANG=${LANG:-C.UTF-8}" \
28
+ /opt/fleek-agents/kit-setup --as-broker "$project"
29
+ fi
30
+ shift
31
+ # Same D-Bus + keyring arrangement as run-service (private session bus in this unit, keyring control below RuntimeDirectory)
32
+ if [ "${1:-}" != --inside-private-bus ]; then
33
+ export GNOME_KEYRING_CONTROL="$RUNTIME_DIRECTORY/keyring"
34
+ mkdir -p -m 0700 "$GNOME_KEYRING_CONTROL"
35
+ exec dbus-run-session -- "$0" --as-broker --inside-private-bus "$@"
36
+ fi
37
+ shift
38
+ project=${1:-}
39
+ [[ "$project" =~ ^[A-Z0-9][A-Z0-9_]{0,19}$ ]] || exit 2
40
+ /opt/fleek-agents/unlock-keyring
41
+ fleek whoami
42
+ # The confirmation (TTY): same profile, project key and target as run-job. Other MCP proposals: answer N - the job
43
+ # container only ever gets the Fleek MCP socket, never the host's client configuration.
44
+ set +e
45
+ fleek kit setup "$project" --target claude
46
+ setup=$?
47
+ set -e
48
+ case $setup in 0|13) ;; *) echo "kit setup ended with exit $setup - nothing confirmed" >&2; exit "$setup" ;; esac
49
+ # Proof: run-job's exact unattended call from a fresh job-like directory, no terminal
50
+ job=$(mktemp -d -p "$RUNTIME_DIRECTORY" job-XXXXXX)
51
+ cd "$job"
52
+ set +e
53
+ fleek kit pull "$project" --target claude --yes --json </dev/null >/dev/null 2>&1
54
+ pulled=$?
55
+ set -e
56
+ cd /
57
+ rm -rf "$job"
58
+ if [ "$pulled" -ne 0 ]; then echo "run-job is NOT ready: unattended kit pull exits $pulled" >&2; exit "$pulled"; fi
59
+ echo "run-job ready: the unattended kit pull needs no confirmation. Next: systemctl restart fleek-agent-review.service"
@@ -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)