feat(ai-workspace): add local relay profiles

This commit is contained in:
Codex
2026-06-20 12:54:19 +03:00
parent 2d5fef3948
commit 3526351b1b
28 changed files with 2959 additions and 77 deletions
@@ -55,6 +55,21 @@ require_contains() {
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"
@@ -88,9 +103,12 @@ require_env_value() {
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"
@@ -98,11 +116,28 @@ require_file "$OPS_GATEWAY_REPO/docker-compose.synology.yml" "Ops Gateway Synolo
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_HUB_PUBLIC_URL" "wss://ai-hub.nodedc.ru/api/ai-workspace/hub" "Local Platform example keeps deployed Hub as hybrid worker relay by default"
require_env_value "$PLATFORM_ROOT/infra/.env.example" "AI_WORKSPACE_HUB_INTERNAL_URL" "https://ai-hub.nodedc.ru" "Local Platform example has explicit Hub internal URL"
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_HUB_INTERNAL_URL=https://ai-hub.nodedc.ru" "Local env generator emits explicit Hub internal 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"
@@ -110,6 +145,11 @@ require_env_value "$PLATFORM_ROOT/infra/synology/.env.synology.example" "AI_WORK
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"
@@ -134,6 +174,13 @@ if [ -f "$PLATFORM_ENV" ]; then
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
@@ -65,9 +65,12 @@ run_in "$PLATFORM_ROOT" \
"infra/scripts/check-ai-workspace-topology.sh"
section "Platform static and contract gates"
run_in "$PLATFORM_ROOT" \
"Local test system contract gate" \
"sh infra/scripts/check-local-test-system.sh"
run_in "$PLATFORM_ROOT" \
"Platform shell syntax" \
"sh -n infra/scripts/init-dev-env.sh infra/scripts/check-ai-workspace-topology.sh infra/scripts/check-ai-workspace-config-contract.sh infra/scripts/check-ai-workspace-release-gates.sh infra/synology/check-ai-workspace-apply-plan.sh infra/synology/prepare-ai-workspace-env.sh infra/synology/deploy-current.sh infra/synology/verify-current-runtime.sh infra/synology/apply-current-runtime.sh infra/synology/backup-current.sh"
"sh -n infra/scripts/init-dev-env.sh infra/scripts/check-local-test-system.sh infra/scripts/check-ai-workspace-topology.sh infra/scripts/check-ai-workspace-config-contract.sh infra/scripts/check-ai-workspace-release-gates.sh infra/synology/check-ai-workspace-apply-plan.sh infra/synology/prepare-ai-workspace-env.sh infra/synology/deploy-current.sh infra/synology/verify-current-runtime.sh infra/synology/apply-current-runtime.sh infra/synology/backup-current.sh"
run_in "$PLATFORM_ROOT" \
"AI Workspace config contract" \
"infra/scripts/check-ai-workspace-config-contract.sh"
+55 -13
View File
@@ -90,40 +90,63 @@ bool_and() {
}
assistant_code=$(http_status "$ASSISTANT_URL/healthz")
hub_body=$(http_body "$HUB_URL/healthz")
hub_code=000
if [ -n "$hub_body" ]; then
hub_code=$(http_status "$HUB_URL/healthz")
fi
gateway_code=$(http_status "$GATEWAY_URL/readyz")
task_code=$(http_status "$TASK_URL")
engine_code=$(http_status "$ENGINE_URL")
assistant_ok=$(ok_status "$assistant_code")
hub_ok=$(ok_status "$hub_code")
gateway_ok=$(ok_status "$gateway_code")
task_ok=$(ok_status "$task_code")
engine_ok=$(ok_status "$engine_code")
agents_online=$(printf "%s" "$hub_body" | json_number agentsOnline)
case "$agents_online" in
""|*[!0-9]*) agents_online=0 ;;
profile=$(read_env "$PLATFORM_ENV" NODEDC_ENV || true)
case "$profile" in
"") profile=local ;;
esac
hub_public=$(read_env "$PLATFORM_ENV" AI_WORKSPACE_HUB_PUBLIC_URL || true)
hub_internal=$(read_env "$PLATFORM_ENV" AI_WORKSPACE_HUB_INTERNAL_URL || true)
ops_entitlement=$(read_env "$PLATFORM_ENV" AI_WORKSPACE_OPS_ENTITLEMENT_URL || true)
action_relay_enabled=$(read_env "$PLATFORM_ENV" AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ENABLED || true)
action_relay_id=$(read_env "$PLATFORM_ENV" AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ID || true)
case "$action_relay_enabled" in
1|true|TRUE|yes|YES|on|ON) action_relay_enabled=true ;;
*) action_relay_enabled=false ;;
esac
hub_public_kind=$(url_kind "$hub_public")
hub_internal_kind=$(url_kind "$hub_internal")
ops_entitlement_kind=$(url_kind "$ops_entitlement")
local_services_ok=$(bool_and "$(bool_and "$assistant_ok" "$hub_ok")" "$(bool_and "$gateway_ok" "$task_ok")")
local_hub_body=$(http_body "$HUB_URL/healthz")
local_hub_code=000
if [ -n "$local_hub_body" ]; then
local_hub_code=$(http_status "$HUB_URL/healthz")
fi
local_hub_ok=$(ok_status "$local_hub_code")
relay_health_code=000
relay_health_ok=false
if [ -n "$hub_internal" ]; then
relay_health_code=$(http_status "$(printf "%s" "$hub_internal" | sed 's#/*$##')/healthz")
relay_health_ok=$(ok_status "$relay_health_code")
fi
agents_online=$(printf "%s" "$local_hub_body" | json_number agentsOnline)
case "$agents_online" in
""|*[!0-9]*) agents_online=0 ;;
esac
local_services_ok=$(bool_and "$assistant_ok" "$(bool_and "$gateway_ok" "$task_ok")")
worker_on_local_hub=false
if [ "$agents_online" -gt 0 ]; then
worker_on_local_hub=true
fi
action_relay_configured=false
if [ "$action_relay_enabled" = true ] && [ -n "$action_relay_id" ]; then
action_relay_configured=true
fi
hub_runtime_kind=custom
case "$hub_public_kind:$hub_internal_kind" in
local:local|local:missing|missing:local) hub_runtime_kind=local ;;
@@ -135,6 +158,10 @@ esac
classification=contract-only
if [ "$local_services_ok" = true ]; then
if [ "$engine_ok" = true ] \
&& [ "$action_relay_configured" = true ] \
&& [ "$hub_runtime_kind" = prod-public ]; then
classification=local-remote-worker-relay
elif [ "$engine_ok" = true ] \
&& [ "$worker_on_local_hub" = true ] \
&& [ "$ops_entitlement_kind" = local ] \
&& [ "$hub_runtime_kind" = local ]; then
@@ -156,9 +183,11 @@ fi
cat <<EOF
classification=$classification
profile=$profile
platform_env=$PLATFORM_ENV
assistant_health=$assistant_ok status=$assistant_code url=$ASSISTANT_URL/healthz
hub_health=$hub_ok status=$hub_code url=$HUB_URL/healthz agents_online=$agents_online
local_hub_health=$local_hub_ok status=$local_hub_code url=$HUB_URL/healthz agents_online=$agents_online
relay_health=$relay_health_ok status=$relay_health_code url=${hub_internal%/}/healthz
gateway_health=$gateway_ok status=$gateway_code url=$GATEWAY_URL/readyz
task_health=$task_ok status=$task_code url=$TASK_URL
engine_health=$engine_ok status=$engine_code url=$ENGINE_URL
@@ -166,14 +195,20 @@ hub_public_kind=$hub_public_kind
hub_internal_kind=$hub_internal_kind
hub_runtime_kind=$hub_runtime_kind
ops_entitlement_kind=$ops_entitlement_kind
assistant_action_relay_enabled=${action_relay_enabled:-missing}
assistant_action_relay_id=${action_relay_id:-missing}
assistant_action_relay_configured=$action_relay_configured
EOF
case "$classification" in
true-local-e2e|tunnel-local-e2e)
echo "meaning=local write-path e2e can be interpreted if the worker can reach returned MCP URLs"
;;
local-remote-worker-relay)
echo "meaning=local apps are the downstream target; deployed AI Hub is used only as remote worker relay and assistant actions route back to local Assistant"
;;
hybrid-prod-bridge)
echo "meaning=do not treat UI dialog as local write-path proof; worker/HUB/MCP topology is mixed"
echo "meaning=do not treat UI dialog as local write-path proof; topology is mixed or assistant action relay is not configured"
;;
local-ops-vertical)
echo "meaning=Gateway and Tasker local vertical checks are valid, but no live Codex worker e2e is proven"
@@ -192,3 +227,10 @@ if [ "${REQUIRE_TRUE_E2E:-0}" = "1" ]; then
*) exit 2 ;;
esac
fi
if [ "${REQUIRE_LOCAL_REMOTE_WORKER_RELAY:-0}" = "1" ]; then
case "$classification" in
local-remote-worker-relay) exit 0 ;;
*) exit 2 ;;
esac
fi
@@ -0,0 +1,147 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const platformRoot = path.resolve(scriptDir, "../..");
const monorepoRoot = path.resolve(platformRoot, "..");
const prodHosts = new Set([
"ai-hub.nodedc.ru",
"hub.nodedc.ru",
"engine.nodedc.ru",
"ops.nodedc.ru",
"id.nodedc.ru",
"ops-agents.nodedc.ru",
]);
const relayKeysAllowedInLocal = new Set([
"AI_WORKSPACE_HUB_PUBLIC_URL",
"AI_WORKSPACE_HUB_INTERNAL_URL",
"NDC_AI_WORKSPACE_HUB_URL",
"NDC_AI_WORKSPACE_HUB_HTTP_URL",
"AI_WORKSPACE_ASSISTANT_ACTION_GATEWAY_URL",
"NDC_AI_WORKSPACE_ASSISTANT_ACTION_GATEWAY_URL",
]);
const localOnlyFiles = [
{
file: path.join(platformRoot, "infra", ".env.example"),
required: true,
requiredKeys: ["NODEDC_ENV", "NODEDC_INTERNAL_ACCESS_TOKEN", "AI_WORKSPACE_HUB_PUBLIC_URL", "AI_WORKSPACE_HUB_INTERNAL_URL", "AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ENABLED", "AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ID"],
},
{
file: path.join(platformRoot, "infra", ".env"),
required: false,
requiredKeys: ["NODEDC_ENV", "NODEDC_INTERNAL_ACCESS_TOKEN", "AI_WORKSPACE_HUB_PUBLIC_URL", "AI_WORKSPACE_HUB_INTERNAL_URL", "AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ENABLED", "AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ID"],
},
{
file: path.join(monorepoRoot, "NODEDC_ENGINE_INFRA", "nodedc-source", "server", ".env.local"),
required: false,
requiredKeys: [
"NODEDC_ENV",
"NODEDC_LAUNCHER_INTERNAL_URL",
"NODEDC_LAUNCHER_ORIGIN",
"AUTHENTIK_PUBLIC_BASE_URL",
"NODEDC_AI_WORKSPACE_ASSISTANT_URL",
],
},
];
const failures = [];
const warnings = [];
const passes = [];
function parseEnvFile(file) {
const env = new Map();
const raw = fs.readFileSync(file, "utf8");
raw.split(/\r?\n/).forEach((line, index) => {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) return;
const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(trimmed);
if (!match) return;
let value = match[2].trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
env.set(match[1], { value, line: index + 1 });
});
return env;
}
function urlHost(value) {
try {
return new URL(value).hostname.toLowerCase();
} catch {
return "";
}
}
function isTunnelOrPrivateRelay(value) {
const host = urlHost(value);
return (
host.endsWith(".ts.net") ||
host.startsWith("100.") ||
host.startsWith("172.22.") ||
host.startsWith("192.168.") ||
host.startsWith("10.")
);
}
function checkLocalEnvFile({ file, required, requiredKeys }) {
if (!fs.existsSync(file)) {
if (required) failures.push(`${file}: missing required local env file`);
else warnings.push(`${file}: not present, skipped`);
return;
}
const env = parseEnvFile(file);
const profile = env.get("NODEDC_ENV")?.value || "local";
if (profile !== "local" && profile !== "tunnel-local-e2e") {
failures.push(`${file}: expected NODEDC_ENV=local or tunnel-local-e2e, got ${profile}`);
}
requiredKeys.forEach((key) => {
if (!env.get(key)?.value) failures.push(`${file}: missing required ${key}`);
});
for (const [key, entry] of env.entries()) {
const value = entry.value;
if (!/^https?:\/\//i.test(value) && !/^wss?:\/\//i.test(value)) continue;
const host = urlHost(value);
if (!host) continue;
if (prodHosts.has(host) && !relayKeysAllowedInLocal.has(key)) {
failures.push(`${file}:${entry.line}: ${key} points local profile at prod host ${host}`);
}
if (isTunnelOrPrivateRelay(value) && profile !== "tunnel-local-e2e") {
failures.push(`${file}:${entry.line}: ${key} uses tunnel/private host ${host}; set NODEDC_ENV=tunnel-local-e2e explicitly or use a normal local/public-relay URL`);
}
}
const hubInternal = env.get("AI_WORKSPACE_HUB_INTERNAL_URL")?.value || "";
if (hubInternal && profile === "local") {
const host = urlHost(hubInternal);
const allowed = host === "ai-hub.nodedc.ru" || host === "127.0.0.1" || host === "localhost";
if (!allowed) {
failures.push(`${file}: AI_WORKSPACE_HUB_INTERNAL_URL must be loopback or deployed relay in local profile, got ${hubInternal}`);
}
}
passes.push(`${file}: checked`);
}
localOnlyFiles.forEach(checkLocalEnvFile);
passes.forEach((item) => console.log(`PASS ${item}`));
warnings.forEach((item) => console.warn(`WARN ${item}`));
failures.forEach((item) => console.error(`FAIL ${item}`));
console.log(`summary: passed=${passes.length} warnings=${warnings.length} failed=${failures.length}`);
if (failures.length) process.exit(1);
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
PLATFORM_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
INFRA_DIR="$PLATFORM_ROOT/infra"
ASSISTANT_DIR="$PLATFORM_ROOT/services/ai-workspace-assistant"
HUB_DIR="$PLATFORM_ROOT/services/ai-workspace-hub"
ONTOLOGY_DIR="$PLATFORM_ROOT/services/ontology-core"
COMPOSE_ENV="${COMPOSE_ENV:-$INFRA_DIR/.env}"
if [ ! -f "$COMPOSE_ENV" ]; then
COMPOSE_ENV="$INFRA_DIR/.env.example"
fi
REQUIRE_RUNTIME="${REQUIRE_RUNTIME:-0}"
REQUIRE_REMOTE_WORKER_RELAY="${REQUIRE_REMOTE_WORKER_RELAY:-0}"
passed=0
failed=0
skipped=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
}
skip() {
skipped=$((skipped + 1))
printf 'SKIP %s\n' "$1"
}
run_cmd() {
label="$1"
shift
printf '\n$ %s\n' "$label"
set +e
"$@"
status=$?
set -e
if [ "$status" -eq 0 ]; then
pass "$label"
else
fail "$label (exit $status)"
fi
}
run_sh() {
label="$1"
dir="$2"
command="$3"
printf '\n$ %s\n' "$label"
set +e
(cd "$dir" && sh -lc "$command")
status=$?
set -e
if [ "$status" -eq 0 ]; then
pass "$label"
else
fail "$label (exit $status)"
fi
}
run_sh_quiet() {
label="$1"
dir="$2"
command="$3"
output_file=$(mktemp "${TMPDIR:-/tmp}/nodedc-check.XXXXXX")
printf '\n$ %s\n' "$label"
set +e
(cd "$dir" && sh -lc "$command") > "$output_file" 2>&1
status=$?
set -e
if [ "$status" -eq 0 ]; then
pass "$label"
rm -f "$output_file"
else
cat "$output_file"
rm -f "$output_file"
fail "$label (exit $status)"
fi
}
section "Static syntax"
run_cmd "Hub server syntax" node --check "$HUB_DIR/src/server.mjs"
run_cmd "Assistant server syntax" node --check "$ASSISTANT_DIR/src/server.mjs"
run_cmd "Local environment safety checker syntax" node --check "$INFRA_DIR/scripts/check-local-environment-safety.mjs"
run_sh "Shell scripts syntax" "$PLATFORM_ROOT" \
"sh -n infra/scripts/init-dev-env.sh infra/scripts/check-local-test-system.sh infra/scripts/check-ai-workspace-topology.sh infra/scripts/check-ai-workspace-config-contract.sh infra/scripts/check-ai-workspace-release-gates.sh"
if command -v bash >/dev/null 2>&1; then
run_sh "Synology hub-only deploy scripts syntax" "$PLATFORM_ROOT" \
"bash -n infra/synology/sync-ai-hub-relay.sh infra/synology/apply-ai-hub-relay.sh"
else
skip "Synology hub-only deploy scripts syntax (bash not installed)"
fi
section "Environment contracts"
run_cmd "Local environment safety" node "$INFRA_DIR/scripts/check-local-environment-safety.mjs"
run_cmd "AI Workspace config contract" "$INFRA_DIR/scripts/check-ai-workspace-config-contract.sh"
section "Compose boundary"
if command -v docker >/dev/null 2>&1; then
compose_tmp=$(mktemp "${TMPDIR:-/tmp}/nodedc-local-compose.XXXXXX")
if docker compose --env-file "$COMPOSE_ENV" -f "$INFRA_DIR/docker-compose.dev.yml" config > "$compose_tmp"; then
pass "Local compose config renders"
else
fail "Local compose config renders"
fi
rm -f "$compose_tmp"
if grep -Fq "env_file:" "$INFRA_DIR/docker-compose.dev.yml"; then
fail "Local compose must not inject the full .env into services"
else
pass "Local compose uses explicit service environment only"
fi
else
skip "Docker compose config render (docker not installed)"
fi
section "AI Workspace smokes"
run_sh_quiet "Assistant run-profile smoke" "$ASSISTANT_DIR" "npm run smoke:run-profile"
if [ -d "$ONTOLOGY_DIR" ]; then
run_sh_quiet "Ontology assistant caller smoke" "$ONTOLOGY_DIR" "npm run smoke:assistant-caller"
else
skip "Ontology assistant caller smoke (missing $ONTOLOGY_DIR)"
fi
section "Runtime classification"
topology_tmp=$(mktemp "${TMPDIR:-/tmp}/nodedc-topology.XXXXXX")
set +e
(cd "$PLATFORM_ROOT" && infra/scripts/check-ai-workspace-topology.sh) > "$topology_tmp"
topology_status=$?
set -e
cat "$topology_tmp"
classification=$(awk -F= '/^classification=/{print $2; exit}' "$topology_tmp")
rm -f "$topology_tmp"
if [ "$topology_status" -eq 0 ]; then
pass "Runtime topology classified as ${classification:-unknown}"
else
fail "Runtime topology classification failed (exit $topology_status)"
fi
if [ "$REQUIRE_RUNTIME" = "1" ]; then
case "$classification" in
true-local-e2e|tunnel-local-e2e|local-remote-worker-relay)
pass "Runtime requirement satisfied"
;;
*)
fail "Runtime requirement not satisfied (classification=${classification:-unknown})"
;;
esac
fi
if [ "$REQUIRE_REMOTE_WORKER_RELAY" = "1" ]; then
case "$classification" in
local-remote-worker-relay)
pass "Remote worker relay requirement satisfied"
;;
*)
fail "Remote worker relay requirement not satisfied (classification=${classification:-unknown})"
;;
esac
fi
section "Git hygiene"
run_sh "Platform diff hygiene" "$PLATFORM_ROOT" "git diff --check"
section "Summary"
printf 'passed=%s failed=%s skipped=%s\n' "$passed" "$failed" "$skipped"
if [ "$failed" -ne 0 ]; then
exit 1
fi
+6 -2
View File
@@ -16,6 +16,7 @@ rand() {
cat > "$ENV_FILE" <<EOF
# domains
NODEDC_ENV=local
AUTH_DOMAIN=auth.local.nodedc
LAUNCHER_DOMAIN=launcher.local.nodedc
TASK_DOMAIN=task.local.nodedc
@@ -61,8 +62,9 @@ COOKIE_DOMAIN=.local.nodedc
COOKIE_SECURE=false
# AI Workspace shared registry and Hub.
# Generated local env uses the deployed relay by default. This is suitable for
# UI/contract smoke, not proof of local live-worker write-path e2e.
# Generated local env may use the deployed AI Hub only as a relay for remote
# Codex workers. Launcher/Engine/Ops/Auth downstream URLs must remain local.
# For Tailscale/ngrok-style relay URLs, set NODEDC_ENV=tunnel-local-e2e deliberately.
AI_WORKSPACE_PG_DB=nodedc_ai_workspace
AI_WORKSPACE_PG_USER=nodedc_ai_workspace
AI_WORKSPACE_PG_PASS=$(rand 36)
@@ -70,6 +72,8 @@ AI_WORKSPACE_ASSISTANT_TOKEN=$(openssl rand -hex 48 | tr -d '\n')
AI_WORKSPACE_OPS_ENTITLEMENT_URL=http://host.docker.internal:4100/api/internal/v1/ai-workspace/entitlements
AI_WORKSPACE_OPS_ENTITLEMENT_TOKEN=replace-with-ops-agent-gateway-internal-token
AI_WORKSPACE_OPS_ENTITLEMENT_REQUIRED=false
AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ENABLED=true
AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ID=local-dev
AI_WORKSPACE_HUB_TOKEN=$(openssl rand -hex 48 | tr -d '\n')
AI_WORKSPACE_HUB_HOST_BIND=127.0.0.1:18081
AI_WORKSPACE_HUB_PUBLIC_URL=wss://ai-hub.nodedc.ru/api/ai-workspace/hub