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 CHANGED
@@ -3,7 +3,7 @@
3
3
  Published on npm as **create-workframe**.
4
4
 
5
5
  ```bash
6
- npx create-workframe@0.1.1 MyProject
6
+ npx create-workframe@0.1.2 MyProject
7
7
  ```
8
8
 
9
9
  Scaffolds an isolated Workframe + Hermes project on Windows, macOS, and Linux.
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
- - **Host Hermes** (`%LOCALAPPDATA%\hermes` on Windows) is personal runtime state and is
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,6 +1,6 @@
1
1
  {
2
2
  "name": "create-workframe",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Scaffold a Workframe + Hermes workspace with guided onboarding",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- fs.mkdirSync(path.dirname(dst), { recursive: true });
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"
@@ -1,28 +1,26 @@
1
1
  # Workframe API Service
2
2
 
3
- **Version:** `0.1.0` (`workframe-api-0.1.0`). See `docs/VERSION.md`.
3
+ Python BFF for the Workframe UI. See [docs/VERSION.md](../../docs/VERSION.md) for release version.
4
4
 
5
- Active backend service for the transplanted Workframe vertical slice.
5
+ Canonical implementation not rewritten into a separate `apps/api` service.
6
6
 
7
- This is intentionally preserved as the current Python BFF instead of being rewritten into `apps/api`.
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
- Runtime files are intentionally not committed:
19
+ - `data/*.db`, vault files under `WORKFRAME_API_DATA_DIR`
20
+ - Hermes `Agents/` tree on mounted volume
22
21
 
23
- - `data/*.db`
24
- - `data/.auth_keys`
25
- - Hermes `Agents/`
26
- - workspace `Files/`
22
+ ## Docs
27
23
 
28
- For VPS deployment, mount clean persistent volumes into `/opt/data` and `/workspace`.
24
+ - [Runtime operations](../../docs/public/runtime-operations.md)
25
+ - [Security](../../docs/public/security.md)
26
+ - [BFF route map](../../docs/public/bff-route-map.md)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workframe/workframe-api",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {