create-workframe 0.1.1 → 0.1.2
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 +1 -1
- package/SECURITY.md +1 -3
- package/package.json +1 -1
- package/scripts/apply-update-hermes.sh +17 -17
- package/scripts/apply-update-workframe.sh +77 -77
- package/scripts/bootstrap-workspace-link.sh +8 -8
- package/scripts/compose-docker-host.sh +37 -37
- package/scripts/fix-zk-encryption-key.sh +35 -35
- package/scripts/restart-gateway-hermes.sh +12 -12
- package/scripts/setup-stack-secrets.sh +50 -50
- package/scripts/sync-canonical-to-package.mjs +19 -2
- package/scripts/verify-public-deploy.sh +105 -105
- package/workframe-api/README.md +11 -13
- package/workframe-api/package.json +1 -1
package/README.md
CHANGED
package/SECURITY.md
CHANGED
|
@@ -25,9 +25,7 @@ We aim to acknowledge reports within a few business days.
|
|
|
25
25
|
|
|
26
26
|
## Scope notes
|
|
27
27
|
|
|
28
|
-
-
|
|
29
|
-
out of scope for Workframe product security reports unless the issue is in shared
|
|
30
|
-
Workframe source code that affects all installs.
|
|
28
|
+
- Personal Hermes installs outside a Workframe compose stack are out of scope unless the issue is in shared Workframe source that affects all installs.
|
|
31
29
|
- Production deployments should run with `WORKFRAME_MODE=team`, invite-only access, and
|
|
32
30
|
without `DEV_LOCAL_UNSAFE`.
|
|
33
31
|
- BYOK (bring-your-own-key) is the default credential mode; workspace company-pays is an
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Safe Hermes update — pull gateway/dashboard images, recreate containers. Preserves runtime/Agents.
|
|
3
|
-
set -euo pipefail
|
|
4
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
-
# shellcheck source=compose-docker-host.sh
|
|
6
|
-
source "$SCRIPT_DIR/compose-docker-host.sh"
|
|
7
|
-
|
|
8
|
-
echo "=== Hermes update (gateway + dashboard only) ==="
|
|
9
|
-
workframe_compose_prepare
|
|
10
|
-
echo "Compose dir: $compose_cd"
|
|
11
|
-
echo "Preserves: Agents/, Files/, workframe-api data volumes"
|
|
12
|
-
|
|
13
|
-
SERVICES=(gateway dashboard)
|
|
14
|
-
workframe_compose pull "${SERVICES[@]}"
|
|
15
|
-
workframe_compose up -d --force-recreate --no-deps "${SERVICES[@]}"
|
|
16
|
-
|
|
17
|
-
echo "=== Hermes update complete ==="
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Safe Hermes update — pull gateway/dashboard images, recreate containers. Preserves runtime/Agents.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
# shellcheck source=compose-docker-host.sh
|
|
6
|
+
source "$SCRIPT_DIR/compose-docker-host.sh"
|
|
7
|
+
|
|
8
|
+
echo "=== Hermes update (gateway + dashboard only) ==="
|
|
9
|
+
workframe_compose_prepare
|
|
10
|
+
echo "Compose dir: $compose_cd"
|
|
11
|
+
echo "Preserves: Agents/, Files/, workframe-api data volumes"
|
|
12
|
+
|
|
13
|
+
SERVICES=(gateway dashboard)
|
|
14
|
+
workframe_compose pull "${SERVICES[@]}"
|
|
15
|
+
workframe_compose up -d --force-recreate --no-deps "${SERVICES[@]}"
|
|
16
|
+
|
|
17
|
+
echo "=== Hermes update complete ==="
|
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Safe Workframe update — sync npm template (optional), rebuild API/supervisor/UI containers. Never wipes runtime/DB.
|
|
3
|
-
set -euo pipefail
|
|
4
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
-
# shellcheck source=compose-docker-host.sh
|
|
6
|
-
source "$SCRIPT_DIR/compose-docker-host.sh"
|
|
7
|
-
|
|
8
|
-
workframe_compose_prepare
|
|
9
|
-
PROJECT_ROOT="${WORKFRAME_HOST_PROJECT_ROOT:-${WORKFRAME_PROJECT_ROOT:-$compose_cd}}"
|
|
10
|
-
|
|
11
|
-
echo "=== Workframe update (API + supervisor + UI) ==="
|
|
12
|
-
echo "Project root: $PROJECT_ROOT"
|
|
13
|
-
echo "Preserves: Agents/, Files/, .env, workframe-api/data, gateway/Hermes profiles"
|
|
14
|
-
|
|
15
|
-
TARGET_VERSION="${WORKFRAME_UPDATE_VERSION:-}"
|
|
16
|
-
NPM_PACKAGE="${WORKFRAME_NPM_PACKAGE:-create-workframe}"
|
|
17
|
-
|
|
18
|
-
if [[ "${WORKFRAME_UPDATE_SKIP_NPM:-1}" == "1" ]] && [[ "${WORKFRAME_UPDATE_ALLOW_NPM:-}" != "1" ]]; then
|
|
19
|
-
echo "Skipping npm template sync (WORKFRAME_UPDATE_SKIP_NPM=1; set WORKFRAME_UPDATE_ALLOW_NPM=1 to fetch)"
|
|
20
|
-
elif command -v npm >/dev/null 2>&1; then
|
|
21
|
-
if [[ "${WORKFRAME_UPDATE_ALLOW_NPM:-}" == "1" ]] && [[ -z "$TARGET_VERSION" ]]; then
|
|
22
|
-
echo "WORKFRAME_UPDATE_VERSION is required when WORKFRAME_UPDATE_ALLOW_NPM=1" >&2
|
|
23
|
-
exit 1
|
|
24
|
-
fi
|
|
25
|
-
TMP="$(mktemp -d)"
|
|
26
|
-
trap 'rm -rf "$TMP"' EXIT
|
|
27
|
-
echo "Fetching ${NPM_PACKAGE}@${TARGET_VERSION:-latest} from npm..."
|
|
28
|
-
(cd "$TMP" && npm pack "${NPM_PACKAGE}@${TARGET_VERSION:-latest}" --silent)
|
|
29
|
-
TARBALL="$(ls -1 "$TMP"/${NPM_PACKAGE}-*.tgz 2>/dev/null | head -n1 || true)"
|
|
30
|
-
if [[ -n "$TARBALL" ]]; then
|
|
31
|
-
EXPECTED="$(npm view "${NPM_PACKAGE}@${TARGET_VERSION:-latest}" dist.integrity --silent 2>/dev/null || true)"
|
|
32
|
-
if [[ -n "$EXPECTED" ]]; then
|
|
33
|
-
ACTUAL="sha512-$(sha512sum "$TARBALL" | awk '{print $1}' | xxd -r -p | base64 | tr -d '\n')"
|
|
34
|
-
if [[ "$ACTUAL" != "$EXPECTED" ]]; then
|
|
35
|
-
echo "integrity mismatch for ${NPM_PACKAGE}@${TARGET_VERSION:-latest}" >&2
|
|
36
|
-
exit 1
|
|
37
|
-
fi
|
|
38
|
-
echo "integrity ok: ${NPM_PACKAGE}@${TARGET_VERSION:-latest}"
|
|
39
|
-
fi
|
|
40
|
-
tar -xf "$TARBALL" -C "$TMP"
|
|
41
|
-
PKG="$TMP/package"
|
|
42
|
-
for dir in workframe-api workframe-supervisor; do
|
|
43
|
-
if [[ -d "$PKG/$dir" ]]; then
|
|
44
|
-
echo "Syncing $dir/"
|
|
45
|
-
mkdir -p "$PROJECT_ROOT/$dir"
|
|
46
|
-
cp -a "$PKG/$dir/." "$PROJECT_ROOT/$dir/"
|
|
47
|
-
fi
|
|
48
|
-
done
|
|
49
|
-
if [[ -d "$PKG/workframe-ui/public" ]]; then
|
|
50
|
-
echo "Syncing workframe-ui/public/"
|
|
51
|
-
mkdir -p "$PROJECT_ROOT/workframe-ui/public"
|
|
52
|
-
cp -a "$PKG/workframe-ui/public/." "$PROJECT_ROOT/workframe-ui/public/"
|
|
53
|
-
fi
|
|
54
|
-
for script in apply-update-hermes.sh apply-update-workframe.sh restart-gateway-hermes.sh compose-docker-host.sh update-hermes.sh; do
|
|
55
|
-
if [[ -f "$PKG/scripts/$script" ]]; then
|
|
56
|
-
cp -a "$PKG/scripts/$script" "$PROJECT_ROOT/scripts/workframe/$script"
|
|
57
|
-
chmod +x "$PROJECT_ROOT/scripts/workframe/$script" 2>/dev/null || true
|
|
58
|
-
fi
|
|
59
|
-
done
|
|
60
|
-
else
|
|
61
|
-
echo "npm pack produced no tarball — skipping template sync"
|
|
62
|
-
fi
|
|
63
|
-
else
|
|
64
|
-
echo "Skipping npm template sync (npm missing or WORKFRAME_UPDATE_SKIP_NPM=1)"
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
|
-
echo "Rebuilding workframe-api and workframe-supervisor..."
|
|
68
|
-
workframe_compose build workframe-api workframe-supervisor
|
|
69
|
-
workframe_compose up -d --build --no-deps workframe-api workframe-supervisor
|
|
70
|
-
|
|
71
|
-
if workframe_compose config --services 2>/dev/null | grep -qx workframe-ui; then
|
|
72
|
-
workframe_compose up -d --no-deps workframe-ui || workframe_compose restart workframe-ui || true
|
|
73
|
-
elif workframe_compose config --services 2>/dev/null | grep -qx workframe; then
|
|
74
|
-
workframe_compose up -d --no-deps workframe || workframe_compose restart workframe || true
|
|
75
|
-
fi
|
|
76
|
-
|
|
77
|
-
echo "=== Workframe update complete ==="
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Safe Workframe update — sync npm template (optional), rebuild API/supervisor/UI containers. Never wipes runtime/DB.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
# shellcheck source=compose-docker-host.sh
|
|
6
|
+
source "$SCRIPT_DIR/compose-docker-host.sh"
|
|
7
|
+
|
|
8
|
+
workframe_compose_prepare
|
|
9
|
+
PROJECT_ROOT="${WORKFRAME_HOST_PROJECT_ROOT:-${WORKFRAME_PROJECT_ROOT:-$compose_cd}}"
|
|
10
|
+
|
|
11
|
+
echo "=== Workframe update (API + supervisor + UI) ==="
|
|
12
|
+
echo "Project root: $PROJECT_ROOT"
|
|
13
|
+
echo "Preserves: Agents/, Files/, .env, workframe-api/data, gateway/Hermes profiles"
|
|
14
|
+
|
|
15
|
+
TARGET_VERSION="${WORKFRAME_UPDATE_VERSION:-}"
|
|
16
|
+
NPM_PACKAGE="${WORKFRAME_NPM_PACKAGE:-create-workframe}"
|
|
17
|
+
|
|
18
|
+
if [[ "${WORKFRAME_UPDATE_SKIP_NPM:-1}" == "1" ]] && [[ "${WORKFRAME_UPDATE_ALLOW_NPM:-}" != "1" ]]; then
|
|
19
|
+
echo "Skipping npm template sync (WORKFRAME_UPDATE_SKIP_NPM=1; set WORKFRAME_UPDATE_ALLOW_NPM=1 to fetch)"
|
|
20
|
+
elif command -v npm >/dev/null 2>&1; then
|
|
21
|
+
if [[ "${WORKFRAME_UPDATE_ALLOW_NPM:-}" == "1" ]] && [[ -z "$TARGET_VERSION" ]]; then
|
|
22
|
+
echo "WORKFRAME_UPDATE_VERSION is required when WORKFRAME_UPDATE_ALLOW_NPM=1" >&2
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
TMP="$(mktemp -d)"
|
|
26
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
27
|
+
echo "Fetching ${NPM_PACKAGE}@${TARGET_VERSION:-latest} from npm..."
|
|
28
|
+
(cd "$TMP" && npm pack "${NPM_PACKAGE}@${TARGET_VERSION:-latest}" --silent)
|
|
29
|
+
TARBALL="$(ls -1 "$TMP"/${NPM_PACKAGE}-*.tgz 2>/dev/null | head -n1 || true)"
|
|
30
|
+
if [[ -n "$TARBALL" ]]; then
|
|
31
|
+
EXPECTED="$(npm view "${NPM_PACKAGE}@${TARGET_VERSION:-latest}" dist.integrity --silent 2>/dev/null || true)"
|
|
32
|
+
if [[ -n "$EXPECTED" ]]; then
|
|
33
|
+
ACTUAL="sha512-$(sha512sum "$TARBALL" | awk '{print $1}' | xxd -r -p | base64 | tr -d '\n')"
|
|
34
|
+
if [[ "$ACTUAL" != "$EXPECTED" ]]; then
|
|
35
|
+
echo "integrity mismatch for ${NPM_PACKAGE}@${TARGET_VERSION:-latest}" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
echo "integrity ok: ${NPM_PACKAGE}@${TARGET_VERSION:-latest}"
|
|
39
|
+
fi
|
|
40
|
+
tar -xf "$TARBALL" -C "$TMP"
|
|
41
|
+
PKG="$TMP/package"
|
|
42
|
+
for dir in workframe-api workframe-supervisor; do
|
|
43
|
+
if [[ -d "$PKG/$dir" ]]; then
|
|
44
|
+
echo "Syncing $dir/"
|
|
45
|
+
mkdir -p "$PROJECT_ROOT/$dir"
|
|
46
|
+
cp -a "$PKG/$dir/." "$PROJECT_ROOT/$dir/"
|
|
47
|
+
fi
|
|
48
|
+
done
|
|
49
|
+
if [[ -d "$PKG/workframe-ui/public" ]]; then
|
|
50
|
+
echo "Syncing workframe-ui/public/"
|
|
51
|
+
mkdir -p "$PROJECT_ROOT/workframe-ui/public"
|
|
52
|
+
cp -a "$PKG/workframe-ui/public/." "$PROJECT_ROOT/workframe-ui/public/"
|
|
53
|
+
fi
|
|
54
|
+
for script in apply-update-hermes.sh apply-update-workframe.sh restart-gateway-hermes.sh compose-docker-host.sh update-hermes.sh; do
|
|
55
|
+
if [[ -f "$PKG/scripts/$script" ]]; then
|
|
56
|
+
cp -a "$PKG/scripts/$script" "$PROJECT_ROOT/scripts/workframe/$script"
|
|
57
|
+
chmod +x "$PROJECT_ROOT/scripts/workframe/$script" 2>/dev/null || true
|
|
58
|
+
fi
|
|
59
|
+
done
|
|
60
|
+
else
|
|
61
|
+
echo "npm pack produced no tarball — skipping template sync"
|
|
62
|
+
fi
|
|
63
|
+
else
|
|
64
|
+
echo "Skipping npm template sync (npm missing or WORKFRAME_UPDATE_SKIP_NPM=1)"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
echo "Rebuilding workframe-api and workframe-supervisor..."
|
|
68
|
+
workframe_compose build workframe-api workframe-supervisor
|
|
69
|
+
workframe_compose up -d --build --no-deps workframe-api workframe-supervisor
|
|
70
|
+
|
|
71
|
+
if workframe_compose config --services 2>/dev/null | grep -qx workframe-ui; then
|
|
72
|
+
workframe_compose up -d --no-deps workframe-ui || workframe_compose restart workframe-ui || true
|
|
73
|
+
elif workframe_compose config --services 2>/dev/null | grep -qx workframe; then
|
|
74
|
+
workframe_compose up -d --no-deps workframe || workframe_compose restart workframe || true
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
echo "=== Workframe update complete ==="
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# Hermes sets HERMES_WRITE_SAFE_ROOT=/opt/data — user artifacts must resolve under it.
|
|
3
|
-
# Bind host Files at /opt/data/workspace; keep /workspace as a symlink for agents and API.
|
|
4
|
-
mkdir -p /opt/data/workspace
|
|
5
|
-
if [ -L /workspace ] || [ ! -e /workspace ]; then
|
|
6
|
-
rm -rf /workspace 2>/dev/null || true
|
|
7
|
-
ln -sfn /opt/data/workspace /workspace
|
|
8
|
-
fi
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Hermes sets HERMES_WRITE_SAFE_ROOT=/opt/data — user artifacts must resolve under it.
|
|
3
|
+
# Bind host Files at /opt/data/workspace; keep /workspace as a symlink for agents and API.
|
|
4
|
+
mkdir -p /opt/data/workspace
|
|
5
|
+
if [ -L /workspace ] || [ ! -e /workspace ]; then
|
|
6
|
+
rm -rf /workspace 2>/dev/null || true
|
|
7
|
+
ln -sfn /opt/data/workspace /workspace
|
|
8
|
+
fi
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Shared docker compose invocation for in-container apply (docker.sock on host).
|
|
3
|
-
set -euo pipefail
|
|
4
|
-
|
|
5
|
-
workframe_compose_prepare() {
|
|
6
|
-
compose_cd=""
|
|
7
|
-
compose_files=()
|
|
8
|
-
|
|
9
|
-
# Host-bindings overlay: docker.sock apply from supervisor uses WORKFRAME_HOST_* paths.
|
|
10
|
-
if [[ -n "${WORKFRAME_HOST_COMPOSE_DIR:-}" && -n "${WORKFRAME_COMPOSE_DIR:-}" \
|
|
11
|
-
&& -f "${WORKFRAME_COMPOSE_DIR}/docker-compose.yml" \
|
|
12
|
-
&& -f "${WORKFRAME_COMPOSE_DIR}/docker-compose.host-bindings.yml" ]]; then
|
|
13
|
-
compose_cd="${WORKFRAME_COMPOSE_DIR}"
|
|
14
|
-
compose_files=(-f docker-compose.yml -f docker-compose.host-bindings.yml)
|
|
15
|
-
return 0
|
|
16
|
-
fi
|
|
17
|
-
|
|
18
|
-
# ponytail: docker Desktop resolves WORKFRAME_HOST_* via socket even when path is not visible in this container.
|
|
19
|
-
if [[ -n "${WORKFRAME_HOST_COMPOSE_DIR:-}" ]]; then
|
|
20
|
-
compose_cd="${WORKFRAME_HOST_COMPOSE_DIR}"
|
|
21
|
-
compose_files=(-f docker-compose.yml)
|
|
22
|
-
return 0
|
|
23
|
-
fi
|
|
24
|
-
|
|
25
|
-
compose_cd="${WORKFRAME_COMPOSE_DIR:-${WORKFRAME_PROJECT_ROOT:-.}}"
|
|
26
|
-
compose_files=(-f docker-compose.yml)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
workframe_compose() {
|
|
30
|
-
workframe_compose_prepare
|
|
31
|
-
cd "$compose_cd"
|
|
32
|
-
if [[ ! -f docker-compose.yml ]]; then
|
|
33
|
-
echo "docker-compose.yml not found in $compose_cd" >&2
|
|
34
|
-
exit 1
|
|
35
|
-
fi
|
|
36
|
-
docker compose "${compose_files[@]}" "$@"
|
|
37
|
-
}
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared docker compose invocation for in-container apply (docker.sock on host).
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
workframe_compose_prepare() {
|
|
6
|
+
compose_cd=""
|
|
7
|
+
compose_files=()
|
|
8
|
+
|
|
9
|
+
# Host-bindings overlay: docker.sock apply from supervisor uses WORKFRAME_HOST_* paths.
|
|
10
|
+
if [[ -n "${WORKFRAME_HOST_COMPOSE_DIR:-}" && -n "${WORKFRAME_COMPOSE_DIR:-}" \
|
|
11
|
+
&& -f "${WORKFRAME_COMPOSE_DIR}/docker-compose.yml" \
|
|
12
|
+
&& -f "${WORKFRAME_COMPOSE_DIR}/docker-compose.host-bindings.yml" ]]; then
|
|
13
|
+
compose_cd="${WORKFRAME_COMPOSE_DIR}"
|
|
14
|
+
compose_files=(-f docker-compose.yml -f docker-compose.host-bindings.yml)
|
|
15
|
+
return 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# ponytail: docker Desktop resolves WORKFRAME_HOST_* via socket even when path is not visible in this container.
|
|
19
|
+
if [[ -n "${WORKFRAME_HOST_COMPOSE_DIR:-}" ]]; then
|
|
20
|
+
compose_cd="${WORKFRAME_HOST_COMPOSE_DIR}"
|
|
21
|
+
compose_files=(-f docker-compose.yml)
|
|
22
|
+
return 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
compose_cd="${WORKFRAME_COMPOSE_DIR:-${WORKFRAME_PROJECT_ROOT:-.}}"
|
|
26
|
+
compose_files=(-f docker-compose.yml)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
workframe_compose() {
|
|
30
|
+
workframe_compose_prepare
|
|
31
|
+
cd "$compose_cd"
|
|
32
|
+
if [[ ! -f docker-compose.yml ]]; then
|
|
33
|
+
echo "docker-compose.yml not found in $compose_cd" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
docker compose "${compose_files[@]}" "$@"
|
|
37
|
+
}
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Fix ZK_AUTH_ENCRYPTION_KEY when it was generated as hex instead of base64.
|
|
3
|
-
set -euo pipefail
|
|
4
|
-
ENV_FILE="${1:-/opt/workframe/MyBusiness/.env}"
|
|
5
|
-
FORCE="${2:-}"
|
|
6
|
-
python3 - "$ENV_FILE" "$FORCE" <<'PY'
|
|
7
|
-
import base64, os, re, sys
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
env, force = Path(sys.argv[1]), sys.argv[2] == "--force"
|
|
10
|
-
text = env.read_text(encoding="utf-8")
|
|
11
|
-
m = re.search(r"^ZK_AUTH_ENCRYPTION_KEY=(.*)$", text, re.M)
|
|
12
|
-
if not m:
|
|
13
|
-
print("ZK_AUTH_ENCRYPTION_KEY missing"); sys.exit(1)
|
|
14
|
-
val = m.group(1).strip()
|
|
15
|
-
try:
|
|
16
|
-
ok = len(base64.b64decode(val)) == 32
|
|
17
|
-
except Exception:
|
|
18
|
-
ok = False
|
|
19
|
-
if ok:
|
|
20
|
-
print("ZK_AUTH_ENCRYPTION_KEY already valid"); sys.exit(0)
|
|
21
|
-
|
|
22
|
-
zk_db = Path(os.environ.get("WORKFRAME_API_DATA_DIR", "/app/data")) / "zk_auth.db"
|
|
23
|
-
if zk_db.is_file() and not force:
|
|
24
|
-
import sqlite3
|
|
25
|
-
n = sqlite3.connect(str(zk_db)).execute("SELECT COUNT(*) FROM identities").fetchone()[0]
|
|
26
|
-
if n > 0:
|
|
27
|
-
print(f"REFUSING: {n} encrypted identities present. Regenerating the KEK will lock "
|
|
28
|
-
f"out every existing user. Re-run with --force to proceed.", file=sys.stderr)
|
|
29
|
-
sys.exit(1)
|
|
30
|
-
|
|
31
|
-
new_key = base64.b64encode(os.urandom(32)).decode()
|
|
32
|
-
text = re.sub(r"^ZK_AUTH_ENCRYPTION_KEY=.*$", f"ZK_AUTH_ENCRYPTION_KEY={new_key}", text, flags=re.M)
|
|
33
|
-
env.write_text(text, encoding="utf-8")
|
|
34
|
-
print("ZK_AUTH_ENCRYPTION_KEY regenerated (was invalid base64)")
|
|
35
|
-
PY
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Fix ZK_AUTH_ENCRYPTION_KEY when it was generated as hex instead of base64.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
ENV_FILE="${1:-/opt/workframe/MyBusiness/.env}"
|
|
5
|
+
FORCE="${2:-}"
|
|
6
|
+
python3 - "$ENV_FILE" "$FORCE" <<'PY'
|
|
7
|
+
import base64, os, re, sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
env, force = Path(sys.argv[1]), sys.argv[2] == "--force"
|
|
10
|
+
text = env.read_text(encoding="utf-8")
|
|
11
|
+
m = re.search(r"^ZK_AUTH_ENCRYPTION_KEY=(.*)$", text, re.M)
|
|
12
|
+
if not m:
|
|
13
|
+
print("ZK_AUTH_ENCRYPTION_KEY missing"); sys.exit(1)
|
|
14
|
+
val = m.group(1).strip()
|
|
15
|
+
try:
|
|
16
|
+
ok = len(base64.b64decode(val)) == 32
|
|
17
|
+
except Exception:
|
|
18
|
+
ok = False
|
|
19
|
+
if ok:
|
|
20
|
+
print("ZK_AUTH_ENCRYPTION_KEY already valid"); sys.exit(0)
|
|
21
|
+
|
|
22
|
+
zk_db = Path(os.environ.get("WORKFRAME_API_DATA_DIR", "/app/data")) / "zk_auth.db"
|
|
23
|
+
if zk_db.is_file() and not force:
|
|
24
|
+
import sqlite3
|
|
25
|
+
n = sqlite3.connect(str(zk_db)).execute("SELECT COUNT(*) FROM identities").fetchone()[0]
|
|
26
|
+
if n > 0:
|
|
27
|
+
print(f"REFUSING: {n} encrypted identities present. Regenerating the KEK will lock "
|
|
28
|
+
f"out every existing user. Re-run with --force to proceed.", file=sys.stderr)
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
new_key = base64.b64encode(os.urandom(32)).decode()
|
|
32
|
+
text = re.sub(r"^ZK_AUTH_ENCRYPTION_KEY=.*$", f"ZK_AUTH_ENCRYPTION_KEY={new_key}", text, flags=re.M)
|
|
33
|
+
env.write_text(text, encoding="utf-8")
|
|
34
|
+
print("ZK_AUTH_ENCRYPTION_KEY regenerated (was invalid base64)")
|
|
35
|
+
PY
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Restart Hermes gateway container only — no image pull. Preserves runtime/Agents.
|
|
3
|
-
set -euo pipefail
|
|
4
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
-
# shellcheck source=compose-docker-host.sh
|
|
6
|
-
source "$SCRIPT_DIR/compose-docker-host.sh"
|
|
7
|
-
|
|
8
|
-
echo "=== Hermes gateway restart ==="
|
|
9
|
-
workframe_compose_prepare
|
|
10
|
-
echo "Compose dir: $compose_cd"
|
|
11
|
-
workframe_compose up -d --force-recreate --no-deps gateway
|
|
12
|
-
echo "=== Gateway restart complete ==="
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Restart Hermes gateway container only — no image pull. Preserves runtime/Agents.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
# shellcheck source=compose-docker-host.sh
|
|
6
|
+
source "$SCRIPT_DIR/compose-docker-host.sh"
|
|
7
|
+
|
|
8
|
+
echo "=== Hermes gateway restart ==="
|
|
9
|
+
workframe_compose_prepare
|
|
10
|
+
echo "Compose dir: $compose_cd"
|
|
11
|
+
workframe_compose up -d --force-recreate --no-deps gateway
|
|
12
|
+
echo "=== Gateway restart complete ==="
|
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Append production stack secrets to a Workframe .env (idempotent).
|
|
3
|
-
# Usage: bash scripts/workframe/setup-stack-secrets.sh path/to/.env
|
|
4
|
-
set -euo pipefail
|
|
5
|
-
|
|
6
|
-
ENV_FILE="${1:-/workspace/.env}"
|
|
7
|
-
|
|
8
|
-
append_if_missing() {
|
|
9
|
-
local key="$1"
|
|
10
|
-
local value="$2"
|
|
11
|
-
local comment="${3:-}"
|
|
12
|
-
if [[ -f "$ENV_FILE" ]] && grep -q "^${key}=" "$ENV_FILE"; then
|
|
13
|
-
echo "${key} already exists in ${ENV_FILE}"
|
|
14
|
-
return 0
|
|
15
|
-
fi
|
|
16
|
-
mkdir -p "$(dirname "$ENV_FILE")"
|
|
17
|
-
{
|
|
18
|
-
[[ -n "$comment" ]] && printf '\n# %s\n' "$comment"
|
|
19
|
-
printf '%s=%s\n' "$key" "$value"
|
|
20
|
-
} >>"$ENV_FILE"
|
|
21
|
-
echo "${key} generated and appended to ${ENV_FILE}"
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
rand_hex() {
|
|
25
|
-
openssl rand -hex 32 2>/dev/null || python3 - <<'PY'
|
|
26
|
-
import secrets
|
|
27
|
-
print(secrets.token_hex(32))
|
|
28
|
-
PY
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
rand_b64() {
|
|
32
|
-
python3 - <<'PY'
|
|
33
|
-
import base64, os
|
|
34
|
-
print(base64.b64encode(os.urandom(32)).decode())
|
|
35
|
-
PY
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
rand_proxy() {
|
|
39
|
-
python3 - <<'PY'
|
|
40
|
-
import secrets
|
|
41
|
-
print(secrets.token_urlsafe(32))
|
|
42
|
-
PY
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
append_if_missing WORKFRAME_SUPERVISOR_TOKEN "${WORKFRAME_SUPERVISOR_TOKEN:-$(rand_hex)}" \
|
|
46
|
-
"Workframe supervisor token. Keep this secret."
|
|
47
|
-
append_if_missing WORKFRAME_PROXY_TOKEN "${WORKFRAME_PROXY_TOKEN:-$(rand_proxy)}" \
|
|
48
|
-
"Internal LLM/action proxy secret — gateway + API must match."
|
|
49
|
-
append_if_missing WORKFRAME_VAULT_KEK "${WORKFRAME_VAULT_KEK:-$(rand_b64)}" \
|
|
50
|
-
"Credential vault KEK (32-byte base64). Required for public_multi_user."
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Append production stack secrets to a Workframe .env (idempotent).
|
|
3
|
+
# Usage: bash scripts/workframe/setup-stack-secrets.sh path/to/.env
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
ENV_FILE="${1:-/workspace/.env}"
|
|
7
|
+
|
|
8
|
+
append_if_missing() {
|
|
9
|
+
local key="$1"
|
|
10
|
+
local value="$2"
|
|
11
|
+
local comment="${3:-}"
|
|
12
|
+
if [[ -f "$ENV_FILE" ]] && grep -q "^${key}=" "$ENV_FILE"; then
|
|
13
|
+
echo "${key} already exists in ${ENV_FILE}"
|
|
14
|
+
return 0
|
|
15
|
+
fi
|
|
16
|
+
mkdir -p "$(dirname "$ENV_FILE")"
|
|
17
|
+
{
|
|
18
|
+
[[ -n "$comment" ]] && printf '\n# %s\n' "$comment"
|
|
19
|
+
printf '%s=%s\n' "$key" "$value"
|
|
20
|
+
} >>"$ENV_FILE"
|
|
21
|
+
echo "${key} generated and appended to ${ENV_FILE}"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
rand_hex() {
|
|
25
|
+
openssl rand -hex 32 2>/dev/null || python3 - <<'PY'
|
|
26
|
+
import secrets
|
|
27
|
+
print(secrets.token_hex(32))
|
|
28
|
+
PY
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
rand_b64() {
|
|
32
|
+
python3 - <<'PY'
|
|
33
|
+
import base64, os
|
|
34
|
+
print(base64.b64encode(os.urandom(32)).decode())
|
|
35
|
+
PY
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
rand_proxy() {
|
|
39
|
+
python3 - <<'PY'
|
|
40
|
+
import secrets
|
|
41
|
+
print(secrets.token_urlsafe(32))
|
|
42
|
+
PY
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
append_if_missing WORKFRAME_SUPERVISOR_TOKEN "${WORKFRAME_SUPERVISOR_TOKEN:-$(rand_hex)}" \
|
|
46
|
+
"Workframe supervisor token. Keep this secret."
|
|
47
|
+
append_if_missing WORKFRAME_PROXY_TOKEN "${WORKFRAME_PROXY_TOKEN:-$(rand_proxy)}" \
|
|
48
|
+
"Internal LLM/action proxy secret — gateway + API must match."
|
|
49
|
+
append_if_missing WORKFRAME_VAULT_KEK "${WORKFRAME_VAULT_KEK:-$(rand_b64)}" \
|
|
50
|
+
"Credential vault KEK (32-byte base64). Required for public_multi_user."
|
|
@@ -57,6 +57,17 @@ function removeIfExists(p) {
|
|
|
57
57
|
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** Alpine sh breaks on CRLF (then\r). Normalize at pack time — Windows checkout is CRLF. */
|
|
61
|
+
function copyIntoPackage(src, dst) {
|
|
62
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
63
|
+
if (dst.endsWith('.sh')) {
|
|
64
|
+
const text = fs.readFileSync(src, 'utf8').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
65
|
+
fs.writeFileSync(dst, text, 'utf8');
|
|
66
|
+
} else {
|
|
67
|
+
fs.copyFileSync(src, dst);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
console.log(`Sync canonical BFF: ${CANONICAL_API} -> ${PKG_API}`);
|
|
61
72
|
copyTree(CANONICAL_API, PKG_API);
|
|
62
73
|
|
|
@@ -107,11 +118,17 @@ for (const name of applyScripts) {
|
|
|
107
118
|
const src = path.join(REPO_ROOT, 'scripts/workframe', name);
|
|
108
119
|
const dst = path.join(PKG_ROOT, 'scripts', name);
|
|
109
120
|
if (!fs.existsSync(src)) throw new Error(`Missing apply script: ${src}`);
|
|
110
|
-
|
|
111
|
-
fs.copyFileSync(src, dst);
|
|
121
|
+
copyIntoPackage(src, dst);
|
|
112
122
|
console.log(`Synced ${name} -> package/scripts/`);
|
|
113
123
|
}
|
|
114
124
|
|
|
125
|
+
for (const sh of fs.readdirSync(path.join(PKG_ROOT, 'scripts')).filter((n) => n.endsWith('.sh'))) {
|
|
126
|
+
const p = path.join(PKG_ROOT, 'scripts', sh);
|
|
127
|
+
if (fs.readFileSync(p).includes(0x0d)) {
|
|
128
|
+
throw new Error(`CRLF remains in package script after sync: ${sh}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
115
132
|
const publicDeploySrc = path.join(REPO_ROOT, 'infra/compose/workframe/PUBLIC_DEPLOY.md');
|
|
116
133
|
const publicDeployDst = path.join(PKG_ROOT, 'docs/PUBLIC_DEPLOY.md');
|
|
117
134
|
if (fs.existsSync(publicDeploySrc)) {
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Fail-closed preflight for WORKFRAME_DEPLOYMENT_MODE=public_multi_user.
|
|
3
|
-
# Usage: bash scripts/workframe/verify-public-deploy.sh [compose-dir]
|
|
4
|
-
set -euo pipefail
|
|
5
|
-
|
|
6
|
-
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
7
|
-
COMPOSE_DIR="${1:-$ROOT/infra/compose/workframe}"
|
|
8
|
-
ENV_FILE="$COMPOSE_DIR/.env"
|
|
9
|
-
COMPOSE_FILE="$COMPOSE_DIR/docker-compose.yml"
|
|
10
|
-
|
|
11
|
-
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
12
|
-
warn() { echo "WARN: $*" >&2; }
|
|
13
|
-
ok() { echo "OK: $*"; }
|
|
14
|
-
|
|
15
|
-
[[ -f "$ENV_FILE" ]] || fail "missing $ENV_FILE"
|
|
16
|
-
[[ -f "$COMPOSE_FILE" ]] || fail "missing $COMPOSE_FILE"
|
|
17
|
-
|
|
18
|
-
env_val() {
|
|
19
|
-
local key="$1"
|
|
20
|
-
grep -E "^${key}=" "$ENV_FILE" | tail -n1 | cut -d= -f2- | tr -d '\r' || true
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
MODE="$(env_val WORKFRAME_DEPLOYMENT_MODE)"
|
|
24
|
-
MODE="${MODE:-trusted_team}"
|
|
25
|
-
if [[ "$MODE" != "public_multi_user" ]]; then
|
|
26
|
-
ok "WORKFRAME_DEPLOYMENT_MODE=$MODE (skipping public checks; pass nothing to force public)"
|
|
27
|
-
exit 0
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
ok "checking public_multi_user preflight"
|
|
31
|
-
|
|
32
|
-
if [[ "$(env_val DEV_LOCAL_UNSAFE)" =~ ^(1|true|yes|on)$ ]]; then
|
|
33
|
-
fail "DEV_LOCAL_UNSAFE must be off for public_multi_user"
|
|
34
|
-
fi
|
|
35
|
-
if [[ "$(env_val SECURE_MODE)" != "true" ]]; then
|
|
36
|
-
fail "SECURE_MODE=true required"
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
for key in WORKFRAME_SUPERVISOR_TOKEN WORKFRAME_API_TOKEN WORKFRAME_PROXY_TOKEN WORKFRAME_VAULT_KEK \
|
|
40
|
-
ZK_AUTH_HMAC_KEY ZK_AUTH_ENCRYPTION_KEY ZK_AUTH_SESSION_SECRET \
|
|
41
|
-
SMTP_HOST SMTP_USER SMTP_PASS EMAIL_FROM; do
|
|
42
|
-
[[ -n "$(env_val "$key")" ]] || fail "$key is empty"
|
|
43
|
-
done
|
|
44
|
-
|
|
45
|
-
APP_URL="$(env_val APP_BASE_URL)"
|
|
46
|
-
[[ "$APP_URL" == https://* ]] || fail "APP_BASE_URL must be https:// (got ${APP_URL:-<empty>})"
|
|
47
|
-
|
|
48
|
-
enc_key="$(env_val ZK_AUTH_ENCRYPTION_KEY)"
|
|
49
|
-
if [[ -n "$enc_key" ]]; then
|
|
50
|
-
python3 - "$enc_key" <<'PY' || fail "ZK_AUTH_ENCRYPTION_KEY must be base64-encoded 32 bytes (not hex)"
|
|
51
|
-
import base64, sys
|
|
52
|
-
raw = base64.b64decode(sys.argv[1].strip())
|
|
53
|
-
assert len(raw) == 32
|
|
54
|
-
PY
|
|
55
|
-
ok "ZK_AUTH_ENCRYPTION_KEY format"
|
|
56
|
-
fi
|
|
57
|
-
|
|
58
|
-
if [[ "$(env_val WORKFRAME_E2E)" =~ ^(1|true|yes|on)$ ]] && [[ "$APP_URL" == https://* ]]; then
|
|
59
|
-
fail "WORKFRAME_E2E must be off for public HTTPS deploy"
|
|
60
|
-
fi
|
|
61
|
-
|
|
62
|
-
if grep -A40 '^ gateway:' "$COMPOSE_FILE" | grep -q 'env_file:'; then
|
|
63
|
-
fail "gateway must not use env_file in docker-compose.yml"
|
|
64
|
-
fi
|
|
65
|
-
if ! grep -q 'control-net' "$COMPOSE_FILE"; then
|
|
66
|
-
fail "control-net missing from docker-compose.yml"
|
|
67
|
-
fi
|
|
68
|
-
|
|
69
|
-
UI_PORT="$(env_val WORKFRAME_UI_PORT)"
|
|
70
|
-
UI_PORT="${UI_PORT:-18644}"
|
|
71
|
-
API_PORT="$(env_val WORKFRAME_API_PORT)"
|
|
72
|
-
API_PORT="${API_PORT:-19120}"
|
|
73
|
-
|
|
74
|
-
if command -v curl >/dev/null 2>&1; then
|
|
75
|
-
health="$(curl -fsS "http://127.0.0.1:${API_PORT}/api/health" 2>/dev/null || true)"
|
|
76
|
-
if [[ -z "$health" ]]; then
|
|
77
|
-
warn "API health unreachable on 127.0.0.1:${API_PORT} (stack down?)"
|
|
78
|
-
else
|
|
79
|
-
echo "$health" | grep -q 'public_multi_user' || fail "API health missing deployment_mode public_multi_user"
|
|
80
|
-
echo "$health" | grep -q '"mode": "secure"' || fail "API not in secure mode"
|
|
81
|
-
echo "$health" | grep -q '"proxy_token_configured": true' || fail "API proxy_token_configured is false"
|
|
82
|
-
echo "$health" | grep -q '"vault_envelope": true' || fail "API vault_envelope is false"
|
|
83
|
-
echo "$health" | grep -q '"docker_sock_on_api": false' || fail "API still has docker.sock mounted"
|
|
84
|
-
echo "$health" | grep -q 'install_window_open' || warn "API health missing install_window_open"
|
|
85
|
-
echo "$health" | grep -q 'workframe_e2e' || warn "API health missing workframe_e2e"
|
|
86
|
-
echo "$health" | grep -q 'dev_local_unsafe' || warn "API health missing dev_local_unsafe"
|
|
87
|
-
ok "API health"
|
|
88
|
-
fi
|
|
89
|
-
dash_code="$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:${UI_PORT}/hermes-dashboard/" || true)"
|
|
90
|
-
[[ "$dash_code" == "403" ]] || warn "hermes-dashboard without session returned $dash_code (expected 403)"
|
|
91
|
-
else
|
|
92
|
-
warn "curl not found — skipping HTTP checks"
|
|
93
|
-
fi
|
|
94
|
-
|
|
95
|
-
if command -v docker >/dev/null 2>&1 && docker inspect workframe-gateway >/dev/null 2>&1; then
|
|
96
|
-
gw_env="$(docker inspect workframe-gateway --format '{{range .Config.Env}}{{println .}}{{end}}')"
|
|
97
|
-
for marker in WORKFRAME_SUPERVISOR_TOKEN ZK_AUTH_ SMTP_PASS; do
|
|
98
|
-
echo "$gw_env" | grep -q "$marker" && fail "gateway env contains $marker"
|
|
99
|
-
done
|
|
100
|
-
ok "gateway env allowlist"
|
|
101
|
-
else
|
|
102
|
-
warn "workframe-gateway not running — skipping docker inspect"
|
|
103
|
-
fi
|
|
104
|
-
|
|
105
|
-
ok "public_multi_user preflight passed"
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Fail-closed preflight for WORKFRAME_DEPLOYMENT_MODE=public_multi_user.
|
|
3
|
+
# Usage: bash scripts/workframe/verify-public-deploy.sh [compose-dir]
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
7
|
+
COMPOSE_DIR="${1:-$ROOT/infra/compose/workframe}"
|
|
8
|
+
ENV_FILE="$COMPOSE_DIR/.env"
|
|
9
|
+
COMPOSE_FILE="$COMPOSE_DIR/docker-compose.yml"
|
|
10
|
+
|
|
11
|
+
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
12
|
+
warn() { echo "WARN: $*" >&2; }
|
|
13
|
+
ok() { echo "OK: $*"; }
|
|
14
|
+
|
|
15
|
+
[[ -f "$ENV_FILE" ]] || fail "missing $ENV_FILE"
|
|
16
|
+
[[ -f "$COMPOSE_FILE" ]] || fail "missing $COMPOSE_FILE"
|
|
17
|
+
|
|
18
|
+
env_val() {
|
|
19
|
+
local key="$1"
|
|
20
|
+
grep -E "^${key}=" "$ENV_FILE" | tail -n1 | cut -d= -f2- | tr -d '\r' || true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
MODE="$(env_val WORKFRAME_DEPLOYMENT_MODE)"
|
|
24
|
+
MODE="${MODE:-trusted_team}"
|
|
25
|
+
if [[ "$MODE" != "public_multi_user" ]]; then
|
|
26
|
+
ok "WORKFRAME_DEPLOYMENT_MODE=$MODE (skipping public checks; pass nothing to force public)"
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
ok "checking public_multi_user preflight"
|
|
31
|
+
|
|
32
|
+
if [[ "$(env_val DEV_LOCAL_UNSAFE)" =~ ^(1|true|yes|on)$ ]]; then
|
|
33
|
+
fail "DEV_LOCAL_UNSAFE must be off for public_multi_user"
|
|
34
|
+
fi
|
|
35
|
+
if [[ "$(env_val SECURE_MODE)" != "true" ]]; then
|
|
36
|
+
fail "SECURE_MODE=true required"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
for key in WORKFRAME_SUPERVISOR_TOKEN WORKFRAME_API_TOKEN WORKFRAME_PROXY_TOKEN WORKFRAME_VAULT_KEK \
|
|
40
|
+
ZK_AUTH_HMAC_KEY ZK_AUTH_ENCRYPTION_KEY ZK_AUTH_SESSION_SECRET \
|
|
41
|
+
SMTP_HOST SMTP_USER SMTP_PASS EMAIL_FROM; do
|
|
42
|
+
[[ -n "$(env_val "$key")" ]] || fail "$key is empty"
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
APP_URL="$(env_val APP_BASE_URL)"
|
|
46
|
+
[[ "$APP_URL" == https://* ]] || fail "APP_BASE_URL must be https:// (got ${APP_URL:-<empty>})"
|
|
47
|
+
|
|
48
|
+
enc_key="$(env_val ZK_AUTH_ENCRYPTION_KEY)"
|
|
49
|
+
if [[ -n "$enc_key" ]]; then
|
|
50
|
+
python3 - "$enc_key" <<'PY' || fail "ZK_AUTH_ENCRYPTION_KEY must be base64-encoded 32 bytes (not hex)"
|
|
51
|
+
import base64, sys
|
|
52
|
+
raw = base64.b64decode(sys.argv[1].strip())
|
|
53
|
+
assert len(raw) == 32
|
|
54
|
+
PY
|
|
55
|
+
ok "ZK_AUTH_ENCRYPTION_KEY format"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if [[ "$(env_val WORKFRAME_E2E)" =~ ^(1|true|yes|on)$ ]] && [[ "$APP_URL" == https://* ]]; then
|
|
59
|
+
fail "WORKFRAME_E2E must be off for public HTTPS deploy"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if grep -A40 '^ gateway:' "$COMPOSE_FILE" | grep -q 'env_file:'; then
|
|
63
|
+
fail "gateway must not use env_file in docker-compose.yml"
|
|
64
|
+
fi
|
|
65
|
+
if ! grep -q 'control-net' "$COMPOSE_FILE"; then
|
|
66
|
+
fail "control-net missing from docker-compose.yml"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
UI_PORT="$(env_val WORKFRAME_UI_PORT)"
|
|
70
|
+
UI_PORT="${UI_PORT:-18644}"
|
|
71
|
+
API_PORT="$(env_val WORKFRAME_API_PORT)"
|
|
72
|
+
API_PORT="${API_PORT:-19120}"
|
|
73
|
+
|
|
74
|
+
if command -v curl >/dev/null 2>&1; then
|
|
75
|
+
health="$(curl -fsS "http://127.0.0.1:${API_PORT}/api/health" 2>/dev/null || true)"
|
|
76
|
+
if [[ -z "$health" ]]; then
|
|
77
|
+
warn "API health unreachable on 127.0.0.1:${API_PORT} (stack down?)"
|
|
78
|
+
else
|
|
79
|
+
echo "$health" | grep -q 'public_multi_user' || fail "API health missing deployment_mode public_multi_user"
|
|
80
|
+
echo "$health" | grep -q '"mode": "secure"' || fail "API not in secure mode"
|
|
81
|
+
echo "$health" | grep -q '"proxy_token_configured": true' || fail "API proxy_token_configured is false"
|
|
82
|
+
echo "$health" | grep -q '"vault_envelope": true' || fail "API vault_envelope is false"
|
|
83
|
+
echo "$health" | grep -q '"docker_sock_on_api": false' || fail "API still has docker.sock mounted"
|
|
84
|
+
echo "$health" | grep -q 'install_window_open' || warn "API health missing install_window_open"
|
|
85
|
+
echo "$health" | grep -q 'workframe_e2e' || warn "API health missing workframe_e2e"
|
|
86
|
+
echo "$health" | grep -q 'dev_local_unsafe' || warn "API health missing dev_local_unsafe"
|
|
87
|
+
ok "API health"
|
|
88
|
+
fi
|
|
89
|
+
dash_code="$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:${UI_PORT}/hermes-dashboard/" || true)"
|
|
90
|
+
[[ "$dash_code" == "403" ]] || warn "hermes-dashboard without session returned $dash_code (expected 403)"
|
|
91
|
+
else
|
|
92
|
+
warn "curl not found — skipping HTTP checks"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
if command -v docker >/dev/null 2>&1 && docker inspect workframe-gateway >/dev/null 2>&1; then
|
|
96
|
+
gw_env="$(docker inspect workframe-gateway --format '{{range .Config.Env}}{{println .}}{{end}}')"
|
|
97
|
+
for marker in WORKFRAME_SUPERVISOR_TOKEN ZK_AUTH_ SMTP_PASS; do
|
|
98
|
+
echo "$gw_env" | grep -q "$marker" && fail "gateway env contains $marker"
|
|
99
|
+
done
|
|
100
|
+
ok "gateway env allowlist"
|
|
101
|
+
else
|
|
102
|
+
warn "workframe-gateway not running — skipping docker inspect"
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
ok "public_multi_user preflight passed"
|
package/workframe-api/README.md
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
1
|
# Workframe API Service
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Python BFF for the Workframe UI. See [docs/VERSION.md](../../docs/VERSION.md) for release version.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Canonical implementation — not rewritten into a separate `apps/api` service.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## Local
|
|
7
|
+
## Local (outside Docker)
|
|
10
8
|
|
|
11
9
|
```bash
|
|
12
10
|
cd services/workframe-api
|
|
13
11
|
python3 -m venv .venv
|
|
14
|
-
. .venv/bin/activate
|
|
12
|
+
. .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
15
13
|
pip install -r requirements.txt
|
|
16
14
|
HOST=0.0.0.0 PORT=8080 HERMES_DATA=/opt/data WORKSPACE=/workspace python3 server.py
|
|
17
15
|
```
|
|
18
16
|
|
|
19
|
-
## Runtime state
|
|
17
|
+
## Runtime state (not committed)
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
- `data/*.db`, vault files under `WORKFRAME_API_DATA_DIR`
|
|
20
|
+
- Hermes `Agents/` tree on mounted volume
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
- `data/.auth_keys`
|
|
25
|
-
- Hermes `Agents/`
|
|
26
|
-
- workspace `Files/`
|
|
22
|
+
## Docs
|
|
27
23
|
|
|
28
|
-
|
|
24
|
+
- [Runtime operations](../../docs/public/runtime-operations.md)
|
|
25
|
+
- [Security](../../docs/public/security.md)
|
|
26
|
+
- [BFF route map](../../docs/public/bff-route-map.md)
|