#!/usr/bin/env sh set -eu SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) PLATFORM_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd) OPS_GATEWAY_REPO="${OPS_GATEWAY_REPO:-$PLATFORM_ROOT/../../data/NODEDC_TASKMANAGER_CODEXAPI}" ENGINE_REPO="${ENGINE_REPO:-$PLATFORM_ROOT/../NODEDC_ENGINE_INFRA}" PLATFORM_ENV="${PLATFORM_ENV:-$PLATFORM_ROOT/infra/.env}" passed=0 failed=0 warnings=0 section() { printf '\n== %s ==\n' "$1" } pass() { passed=$((passed + 1)) printf 'PASS %s\n' "$1" } fail() { failed=$((failed + 1)) printf 'FAIL %s\n' "$1" >&2 } warn() { warnings=$((warnings + 1)) printf 'WARN %s\n' "$1" >&2 } require_file() { file="$1" label="$2" if [ -f "$file" ]; then pass "$label" else fail "$label (missing file: $file)" fi } require_contains() { file="$1" needle="$2" label="$3" if [ ! -f "$file" ]; then fail "$label (missing file: $file)" return 0 fi if grep -Fq "$needle" "$file"; then pass "$label" else fail "$label (missing: $needle)" fi } require_not_contains() { file="$1" needle="$2" label="$3" if [ ! -f "$file" ]; then fail "$label (missing file: $file)" return 0 fi if grep -Fq "$needle" "$file"; then fail "$label (forbidden: $needle)" else pass "$label" fi } read_env() { file="$1" key="$2" if [ ! -f "$file" ]; then return 0 fi awk -F= -v key="$key" ' $0 ~ "^[[:space:]]*#" { next } $1 == key { value = substr($0, index($0, "=") + 1) gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) gsub(/^"|"$/, "", value) print value exit } ' "$file" } require_env_value() { file="$1" key="$2" expected="$3" label="$4" actual=$(read_env "$file" "$key" || true) if [ "$actual" = "$expected" ]; then pass "$label" else fail "$label (expected $key=$expected, got ${actual:-})" fi } section "Required files" require_file "$PLATFORM_ROOT/infra/.env.example" "Platform local env example exists" require_file "$PLATFORM_ROOT/infra/scripts/check-local-environment-safety.mjs" "Local environment safety checker exists" require_file "$PLATFORM_ROOT/infra/synology/.env.synology.example" "Platform Synology env example exists" require_file "$PLATFORM_ROOT/infra/docker-compose.dev.yml" "Platform local compose exists" require_file "$PLATFORM_ROOT/infra/synology/docker-compose.platform-http.yml" "Platform Synology compose exists" require_file "$PLATFORM_ROOT/infra/synology/sync-ai-hub-relay.sh" "Synology AI Hub relay sync script exists" require_file "$PLATFORM_ROOT/infra/synology/apply-ai-hub-relay.sh" "Synology AI Hub relay apply script exists" require_file "$OPS_GATEWAY_REPO/.env.example" "Ops Gateway local env example exists" require_file "$OPS_GATEWAY_REPO/.env.synology.example" "Ops Gateway Synology env example exists" require_file "$OPS_GATEWAY_REPO/docker-compose.local.yml" "Ops Gateway local compose exists" require_file "$OPS_GATEWAY_REPO/docker-compose.synology.yml" "Ops Gateway Synology compose exists" require_file "$ENGINE_REPO/docker-compose.yml" "Engine compose exists" section "Platform local contract" require_env_value "$PLATFORM_ROOT/infra/.env.example" "NODEDC_ENV" "local" "Local Platform example declares NODEDC_ENV" require_env_value "$PLATFORM_ROOT/infra/.env.example" "AI_WORKSPACE_OPS_ENTITLEMENT_URL" "http://host.docker.internal:4100/api/internal/v1/ai-workspace/entitlements" "Local Platform example points Ops entitlement to local Gateway" require_env_value "$PLATFORM_ROOT/infra/.env.example" "AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ENABLED" "true" "Local Platform example enables Assistant action relay" require_env_value "$PLATFORM_ROOT/infra/.env.example" "AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ID" "local-dev" "Local Platform example declares stable local Assistant action relay id" require_env_value "$PLATFORM_ROOT/infra/.env.example" "AI_WORKSPACE_HUB_PUBLIC_URL" "wss://ai-hub.nodedc.ru/api/ai-workspace/hub" "Local Platform example keeps deployed AI Hub only as worker relay" require_env_value "$PLATFORM_ROOT/infra/.env.example" "AI_WORKSPACE_HUB_INTERNAL_URL" "https://ai-hub.nodedc.ru" "Local Platform example has explicit relay control URL" require_contains "$PLATFORM_ROOT/infra/scripts/init-dev-env.sh" "NODEDC_ENV=local" "Local env generator emits NODEDC_ENV" require_contains "$PLATFORM_ROOT/infra/scripts/init-dev-env.sh" "AI_WORKSPACE_OPS_ENTITLEMENT_URL=http://host.docker.internal:4100/api/internal/v1/ai-workspace/entitlements" "Local env generator emits local Ops entitlement URL" require_contains "$PLATFORM_ROOT/infra/scripts/init-dev-env.sh" "AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ENABLED=true" "Local env generator enables Assistant action relay" require_contains "$PLATFORM_ROOT/infra/scripts/init-dev-env.sh" "AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ID=local-dev" "Local env generator emits stable Assistant action relay id" require_contains "$PLATFORM_ROOT/infra/scripts/init-dev-env.sh" "AI_WORKSPACE_HUB_INTERNAL_URL=https://ai-hub.nodedc.ru" "Local env generator emits explicit relay control URL" require_not_contains "$PLATFORM_ROOT/infra/docker-compose.dev.yml" "env_file:" "Local compose does not inject full .env into containers" require_contains "$PLATFORM_ROOT/infra/docker-compose.dev.yml" 'NODEDC_INTERNAL_ACCESS_TOKEN: ${NODEDC_INTERNAL_ACCESS_TOKEN:-}' "Local compose explicitly passes internal token only to AI services" require_contains "$PLATFORM_ROOT/infra/docker-compose.dev.yml" 'AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ENABLED: ${AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ENABLED:-true}' "Local compose explicitly passes Assistant action relay enabled flag" require_contains "$PLATFORM_ROOT/infra/docker-compose.dev.yml" 'AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ID: ${AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ID:-local-dev}' "Local compose explicitly passes Assistant action relay id" section "Local safety guardrail" if node "$PLATFORM_ROOT/infra/scripts/check-local-environment-safety.mjs"; then pass "Local environment safety checker passes" else fail "Local environment safety checker failed" fi section "Platform Synology contract" require_env_value "$PLATFORM_ROOT/infra/synology/.env.synology.example" "NODEDC_AI_WORKSPACE_ASSISTANT_URL" "http://ai-workspace-assistant:18082" "Synology Platform exposes Assistant to app services on compose network" require_env_value "$PLATFORM_ROOT/infra/synology/.env.synology.example" "AI_WORKSPACE_OPS_ENTITLEMENT_URL" "http://172.22.0.222:18190/api/internal/v1/ai-workspace/entitlements" "Synology Platform points Ops entitlement to Synology Gateway" require_env_value "$PLATFORM_ROOT/infra/synology/.env.synology.example" "AI_WORKSPACE_HUB_PUBLIC_URL" "wss://ai-hub.nodedc.ru/api/ai-workspace/hub" "Synology Platform worker-facing Hub URL is public relay" require_env_value "$PLATFORM_ROOT/infra/synology/.env.synology.example" "AI_WORKSPACE_HUB_INTERNAL_URL" "https://ai-hub.nodedc.ru" "Synology Platform backend Hub URL is explicit" require_contains "$PLATFORM_ROOT/infra/synology/verify-current-runtime.sh" 'AI_WORKSPACE_OPS_ENTITLEMENT_URL" = "http://172.22.0.222:18190/api/internal/v1/ai-workspace/entitlements"' "Synology verify checks Ops entitlement URL" require_contains "$PLATFORM_ROOT/infra/synology/sync-ai-hub-relay.sh" 'services/ai-workspace-hub/' "Hub-only sync copies AI Workspace Hub source" require_contains "$PLATFORM_ROOT/infra/synology/apply-ai-hub-relay.sh" 'up -d --no-deps --force-recreate ai-workspace-hub' "Hub-only apply recreates only AI Workspace Hub without dependencies" require_contains "$PLATFORM_ROOT/infra/synology/apply-ai-hub-relay.sh" '/api/ai-workspace/hub/v1/assistant-relays/' "Hub-only apply verifies Assistant relay endpoint" require_not_contains "$PLATFORM_ROOT/infra/synology/apply-ai-hub-relay.sh" ' launcher' "Hub-only apply does not target Launcher" require_not_contains "$PLATFORM_ROOT/infra/synology/apply-ai-hub-relay.sh" 'authentik' "Hub-only apply does not target Authentik" section "Ops Gateway contract" require_env_value "$OPS_GATEWAY_REPO/.env.example" "NODEDC_AGENT_GATEWAY_PUBLIC_URL" "https://ops-agents.nodedc.ru" "Ops Gateway local example keeps worker-facing MCP URL explicit" require_env_value "$OPS_GATEWAY_REPO/.env.example" "NODEDC_TASKER_INTERNAL_URL" "http://task.local.nodedc" "Ops Gateway local example points downstream to local Tasker" require_env_value "$OPS_GATEWAY_REPO/.env.synology.example" "NODEDC_AGENT_GATEWAY_PUBLIC_URL" "https://ops-agents.nodedc.ru" "Ops Gateway Synology public MCP URL is public relay" require_env_value "$OPS_GATEWAY_REPO/.env.synology.example" "NODEDC_TASKER_INTERNAL_URL" "http://172.22.0.222:18090" "Ops Gateway Synology downstream points to Synology Tasker" require_contains "$OPS_GATEWAY_REPO/docker-compose.synology.yml" '${HOST_BIND:-172.22.0.222}:${HOST_PORT:-18190}:${PORT:-4100}' "Ops Gateway Synology compose binds expected internal port" require_contains "$OPS_GATEWAY_REPO/README.md" "The response uses the AI Workspace adapter contract" "Ops Gateway README documents AI Workspace adapter contract" section "Engine contract" require_contains "$ENGINE_REPO/docker-compose.yml" 'NODEDC_AI_WORKSPACE_ASSISTANT_URL: ${NODEDC_AI_WORKSPACE_ASSISTANT_URL:-http://ai-workspace-assistant:18082}' "Engine compose accepts configurable Assistant URL" require_contains "$ENGINE_REPO/docker-compose.yml" 'NDC_AI_WORKSPACE_HUB_URL: ${NDC_AI_WORKSPACE_HUB_URL:-wss://ai-hub.nodedc.ru/api/ai-workspace/hub}' "Engine compose accepts configurable worker-facing Hub URL" require_contains "$ENGINE_REPO/docker-compose.yml" 'NDC_AI_WORKSPACE_HUB_HTTP_URL: ${NDC_AI_WORKSPACE_HUB_HTTP_URL:-https://ai-hub.nodedc.ru}' "Engine compose accepts configurable Hub HTTP URL" require_contains "$ENGINE_REPO/nodedc-source/server/aiWorkspace/assistantRegistryClient.js" "'X-NODEDC-User-Role': role" "Engine forwards user role to Assistant" require_contains "$ENGINE_REPO/nodedc-source/server/aiWorkspace/assistantRegistryClient.js" "'X-NODEDC-User-Groups': groups.join(',')" "Engine forwards user groups to Assistant" section "Current local env warnings" if [ -f "$PLATFORM_ENV" ]; then local_ops_entitlement=$(read_env "$PLATFORM_ENV" AI_WORKSPACE_OPS_ENTITLEMENT_URL || true) if [ -z "$local_ops_entitlement" ]; then warn "Current Platform env has no AI_WORKSPACE_OPS_ENTITLEMENT_URL; current Mac remains hybrid/contract-only until env is deliberately updated." else pass "Current Platform env has AI_WORKSPACE_OPS_ENTITLEMENT_URL" fi local_action_relay_enabled=$(read_env "$PLATFORM_ENV" AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ENABLED || true) local_action_relay_id=$(read_env "$PLATFORM_ENV" AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ID || true) if [ "$local_action_relay_enabled" = "true" ] && [ -n "$local_action_relay_id" ]; then pass "Current Platform env has Assistant action relay enabled" else warn "Current Platform env has no enabled Assistant action relay; remote worker action calls may route incorrectly." fi else warn "Current Platform env not found: $PLATFORM_ENV" fi section "Summary" printf 'passed=%s failed=%s warnings=%s\n' "$passed" "$failed" "$warnings" if [ "$failed" -ne 0 ]; then exit 1 fi