feat: deploy HoloLake mobile capabilities safely
This commit is contained in:
parent
84f7c4c198
commit
41b2016b8b
3 changed files with 236 additions and 3 deletions
|
|
@ -8,22 +8,197 @@ fi
|
|||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
install_root=/opt/guanghu/lake-lamp-authz
|
||||
state_root=/var/lib/guanghu/architecture-provision
|
||||
secret_root=/etc/guanghu/secrets/lake-lamp
|
||||
authorization_env=$secret_root/authorization.env
|
||||
provider_registry=/etc/guanghu/secrets/hololake-ai-providers.json
|
||||
knowledge_repo=/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1/data/repositories/bingshuo/hololake-knowledge-base.git
|
||||
stamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||||
backup_root=$state_root/manual-backups/lake-lamp-authz-$stamp
|
||||
|
||||
required_source_files=(
|
||||
server.js
|
||||
workorder-manager.js
|
||||
map-gate.js
|
||||
smtp-mailer.js
|
||||
action-client.js
|
||||
architecture-provision-broker.js
|
||||
deployment-event.js
|
||||
deployment-event-worker.js
|
||||
deployment-source-policy.js
|
||||
guanghu-router.js
|
||||
repo-push-broker.js
|
||||
hololake-session.js
|
||||
hololake-capabilities.js
|
||||
)
|
||||
|
||||
for file in "${required_source_files[@]}" lake-lamp-authz.service lake-lamp-architecture-provision.service lake-lamp-deployment-event-worker.service; do
|
||||
[[ -f "$script_dir/$file" ]] || {
|
||||
echo "missing required release file: $file" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
install -d -m 0700 "$backup_root"
|
||||
[[ ! -e "$install_root" ]] || cp -a "$install_root" "$backup_root/install-root"
|
||||
for existing in \
|
||||
/etc/systemd/system/lake-lamp-authz.service \
|
||||
/etc/systemd/system/lake-lamp-architecture-provision.service \
|
||||
/etc/systemd/system/lake-lamp-deployment-event-worker.service \
|
||||
"$authorization_env" \
|
||||
"$provider_registry"; do
|
||||
if [[ -e "$existing" ]]; then
|
||||
destination=$backup_root/existing${existing}
|
||||
install -d -m 0700 "$(dirname "$destination")"
|
||||
cp -a "$existing" "$destination"
|
||||
fi
|
||||
done
|
||||
|
||||
rollback() {
|
||||
set +e
|
||||
if [[ -d "$backup_root/install-root" ]]; then
|
||||
rm -rf -- "$install_root"
|
||||
cp -a "$backup_root/install-root" "$install_root"
|
||||
fi
|
||||
for existing in \
|
||||
/etc/systemd/system/lake-lamp-authz.service \
|
||||
/etc/systemd/system/lake-lamp-architecture-provision.service \
|
||||
/etc/systemd/system/lake-lamp-deployment-event-worker.service \
|
||||
"$authorization_env" \
|
||||
"$provider_registry"; do
|
||||
saved=$backup_root/existing${existing}
|
||||
if [[ -e "$saved" ]]; then
|
||||
install -d -m 0755 "$(dirname "$existing")"
|
||||
cp -a "$saved" "$existing"
|
||||
elif [[ "$existing" = "$provider_registry" ]]; then
|
||||
rm -f -- "$existing"
|
||||
fi
|
||||
done
|
||||
systemctl daemon-reload
|
||||
systemctl restart lake-lamp-authz.service
|
||||
systemctl restart lake-lamp-architecture-provision.service
|
||||
systemctl restart lake-lamp-deployment-event-worker.service
|
||||
}
|
||||
trap 'rc=$?; if [[ $rc -ne 0 ]]; then rollback; fi; exit "$rc"' EXIT
|
||||
|
||||
install -d -m 0755 "$install_root"
|
||||
for file in server.js workorder-manager.js map-gate.js smtp-mailer.js action-client.js architecture-provision-broker.js deployment-event.js deployment-event-worker.js deployment-source-policy.js hololake-session.js hololake-capabilities.js; do
|
||||
for file in "${required_source_files[@]}"; do
|
||||
install -m 0644 "$script_dir/$file" "$install_root/$file"
|
||||
done
|
||||
install -m 0644 "$script_dir/lake-lamp-authz.service" /etc/systemd/system/lake-lamp-authz.service
|
||||
install -m 0644 "$script_dir/lake-lamp-architecture-provision.service" /etc/systemd/system/lake-lamp-architecture-provision.service
|
||||
install -m 0644 "$script_dir/lake-lamp-deployment-event-worker.service" /etc/systemd/system/lake-lamp-deployment-event-worker.service
|
||||
install -d -m 0700 /var/lib/guanghu/architecture-provision
|
||||
install -d -m 0700 "$state_root"
|
||||
install -d -m 0750 /var/lib/guanghu/deployment-events
|
||||
install -d -m 0700 /var/lib/guanghu/deployment-events/receipts
|
||||
install -d -m 0755 /opt/guanghu/architecture-releases
|
||||
install -d -m 0755 /etc/guanghu/lake-lamp
|
||||
install -d -m 0750 "$secret_root"
|
||||
if [[ ! -e /etc/guanghu/lake-lamp/deployment-repositories.json ]]; then
|
||||
install -m 0644 "$script_dir/deployment-repositories.example.json" /etc/guanghu/lake-lamp/deployment-repositories.json
|
||||
fi
|
||||
|
||||
[[ -f "$authorization_env" ]] || {
|
||||
echo "private authorization environment is missing" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
append_setting() {
|
||||
local key=$1
|
||||
local value=$2
|
||||
if ! grep -q "^${key}=" "$authorization_env"; then
|
||||
printf '%s=%s\n' "$key" "$value" >>"$authorization_env"
|
||||
fi
|
||||
}
|
||||
|
||||
if ! grep -q '^HOLOLAKE_SESSION_PEPPER=' "$authorization_env"; then
|
||||
append_setting HOLOLAKE_SESSION_PEPPER "$(/usr/bin/openssl rand -hex 32)"
|
||||
fi
|
||||
append_setting HOLOLAKE_SESSION_STATE_FILE /var/lib/guanghu/lake-lamp-authz/hololake-sessions.json
|
||||
append_setting HOLOLAKE_OTP_TTL 600
|
||||
append_setting HOLOLAKE_ACCOUNT_SESSION_TTL 86400
|
||||
append_setting HOLOLAKE_OTP_REQUEST_LIMIT 6
|
||||
append_setting HOLOLAKE_KNOWLEDGE_REPOSITORY_PATH "$knowledge_repo"
|
||||
append_setting HOLOLAKE_KNOWLEDGE_MAX_ARCHIVE_BYTES 134217728
|
||||
append_setting HOLOLAKE_AI_PROVIDERS_FILE "$provider_registry"
|
||||
chmod 0600 "$authorization_env"
|
||||
|
||||
if [[ ! -f "$provider_registry" ]]; then
|
||||
/usr/bin/python3 - "$provider_registry" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
destination = pathlib.Path(sys.argv[1])
|
||||
values = {}
|
||||
roots = (
|
||||
pathlib.Path("/etc/guanghu/secrets"),
|
||||
pathlib.Path("/etc/guanghu/persona-secrets"),
|
||||
)
|
||||
for root in roots:
|
||||
if not root.exists():
|
||||
continue
|
||||
for file in root.rglob("*.env"):
|
||||
try:
|
||||
for raw in file.read_text().splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
values.setdefault(key.strip(), value.strip().strip("'\""))
|
||||
except (OSError, UnicodeError):
|
||||
continue
|
||||
|
||||
providers = {}
|
||||
if values.get("DEEPSEEK_API_KEY"):
|
||||
providers["deepseek"] = {
|
||||
"name": "DeepSeek",
|
||||
"base_url": "https://api.deepseek.com",
|
||||
"api_key": values["DEEPSEEK_API_KEY"],
|
||||
"models": ["deepseek-chat", "deepseek-reasoner"],
|
||||
}
|
||||
elif values.get("OPENAI_API_KEY"):
|
||||
providers["openai"] = {
|
||||
"name": "OpenAI",
|
||||
"base_url": "https://api.openai.com/v1",
|
||||
"api_key": values["OPENAI_API_KEY"],
|
||||
"models": ["gpt-4.1-mini"],
|
||||
}
|
||||
elif values.get("DASHSCOPE_API_KEY"):
|
||||
providers["qwen"] = {
|
||||
"name": "Qwen",
|
||||
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"api_key": values["DASHSCOPE_API_KEY"],
|
||||
"models": ["qwen-plus"],
|
||||
}
|
||||
|
||||
if providers:
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = destination.with_suffix(".tmp")
|
||||
temporary.write_text(json.dumps({
|
||||
"schema": "guanghu.hololake-ai-providers/v1",
|
||||
"providers": providers,
|
||||
}, ensure_ascii=False, indent=2) + "\n")
|
||||
temporary.chmod(0o640)
|
||||
temporary.replace(destination)
|
||||
PY
|
||||
fi
|
||||
|
||||
if [[ -f "$provider_registry" ]]; then
|
||||
chown root:guanghu-authz "$provider_registry"
|
||||
chmod 0640 "$provider_registry"
|
||||
fi
|
||||
|
||||
[[ -d "$knowledge_repo" ]] || {
|
||||
echo "registered HoloLake knowledge repository is missing" >&2
|
||||
exit 1
|
||||
}
|
||||
if command -v setfacl >/dev/null 2>&1; then
|
||||
setfacl -R -m u:guanghu-authz:rX "$knowledge_repo"
|
||||
setfacl -R -d -m u:guanghu-authz:rX "$knowledge_repo"
|
||||
fi
|
||||
runuser -u guanghu-authz -- git --git-dir="$knowledge_repo" rev-parse --verify refs/heads/main >/dev/null
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now lake-lamp-architecture-provision.service
|
||||
systemctl restart lake-lamp-authz.service
|
||||
|
|
@ -31,4 +206,17 @@ systemctl enable --now lake-lamp-deployment-event-worker.service
|
|||
systemctl is-active --quiet lake-lamp-architecture-provision.service
|
||||
systemctl is-active --quiet lake-lamp-authz.service
|
||||
systemctl is-active --quiet lake-lamp-deployment-event-worker.service
|
||||
echo ARCHITECTURE_PROVISIONER_INSTALLED
|
||||
health=$(/usr/bin/curl -fsS --max-time 10 http://127.0.0.1:3921/health)
|
||||
/usr/bin/node -e '
|
||||
const health = JSON.parse(process.argv[1]);
|
||||
if (!health.ok || health.service !== "lake-lamp-authz") process.exit(1);
|
||||
if (!health.hololake_mobile || !health.hololake_mobile.email_session || !health.hololake_mobile.knowledge_snapshot) process.exit(1);
|
||||
' "$health"
|
||||
if [[ -f "$provider_registry" ]]; then
|
||||
/usr/bin/node -e '
|
||||
const health = JSON.parse(process.argv[1]);
|
||||
if (!health.hololake_mobile || !health.hololake_mobile.ai_gateway) process.exit(1);
|
||||
' "$health"
|
||||
fi
|
||||
trap - EXIT
|
||||
printf 'HOLOLAKE_MOBILE_CAPABILITIES_INSTALLED backup=%s\n' "$backup_root"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
"use strict";
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, "install-architecture-provisioner.sh"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("bootstrap installer preserves secrets and deploys the complete HoloLake capability set", () => {
|
||||
for (const file of [
|
||||
"server.js",
|
||||
"guanghu-router.js",
|
||||
"repo-push-broker.js",
|
||||
"hololake-session.js",
|
||||
"hololake-capabilities.js",
|
||||
]) {
|
||||
assert.match(source, new RegExp(`\\b${file.replaceAll(".", "\\.")}\\b`));
|
||||
}
|
||||
assert.match(source, /HOLOLAKE_SESSION_PEPPER/);
|
||||
assert.match(source, /openssl rand -hex 32/);
|
||||
assert.match(source, /manual-backups\/lake-lamp-authz-/);
|
||||
assert.match(source, /rollback\(\)/);
|
||||
assert.match(source, /setfacl -R -m u:guanghu-authz:rX/);
|
||||
assert.doesNotMatch(source, /cat ["']?\$authorization_env/);
|
||||
assert.doesNotMatch(source, /set -x/);
|
||||
});
|
||||
|
||||
test("provider migration writes only a private registry and never prints API keys", () => {
|
||||
assert.match(source, /DEEPSEEK_API_KEY/);
|
||||
assert.match(source, /OPENAI_API_KEY/);
|
||||
assert.match(source, /DASHSCOPE_API_KEY/);
|
||||
assert.match(source, /chmod 0640 "\$provider_registry"/);
|
||||
assert.match(source, /chown root:guanghu-authz "\$provider_registry"/);
|
||||
assert.doesNotMatch(source, /echo .*API_KEY/);
|
||||
assert.doesNotMatch(source, /printf .*API_KEY/);
|
||||
});
|
||||
|
|
@ -256,6 +256,11 @@ function createApp(options = {}) {
|
|||
session_ttl: manager.sessionTtl,
|
||||
max_session_lifetime: manager.maxSessionLifetime,
|
||||
auto_renew_on_activity: true,
|
||||
hololake_mobile: {
|
||||
email_session: Boolean(hololakeSessionManager),
|
||||
knowledge_snapshot: Boolean(hololakeKnowledgeProvider),
|
||||
ai_gateway: Boolean(hololakeAiGateway),
|
||||
},
|
||||
});
|
||||
if (req.method === "GET" && url.pathname === "/api/public/capabilities") return json(res, 200, {
|
||||
schema: "guanghu.lake-lamp-public-workorder/v1",
|
||||
|
|
|
|||
Loading…
Reference in a new issue