feat: add fixed enterprise Agent gate
Part 2/4 of verified local 18dfdfd: fail-closed navigation and intent gate, fixed Agent connector, loopback code-channel candidate scripts and tests.
This commit is contained in:
parent
7f5ad74c6b
commit
8485822da6
10 changed files with 643 additions and 49 deletions
|
|
@ -4,6 +4,32 @@
|
|||
|
||||
部署后只监听本机 `127.0.0.1:8031`。公众网站与后续 GLSV 页面通过 Nginx 以单独的受控路由接入;在邮件确认与节点连接器完成前,不开放节点激活或执行。
|
||||
|
||||
人类管理员与人格体遵守同一条地图门禁:先 `GET /v1/navigation-map` 完整读取地图,再向 `/v1/navigation-map/ack` 签收当前哈希。任何域、动作或地图版本变化都会让旧签收失效;未签收时所有登记、预检和变更请求返回 `423`。管理员 token 不能绕过地图。
|
||||
人类管理员与人格体遵守同一条入口链:
|
||||
|
||||
```text
|
||||
GET /v2/global-navigation-map
|
||||
→ POST /v1/navigation-map/ack
|
||||
→ POST /v1/intent-state/restore
|
||||
→ POST /v1/agents/unlock
|
||||
→ 固定 Agent 连接器消费一次性 unlock
|
||||
→ 验证和回执
|
||||
```
|
||||
|
||||
地图签收按当前主体和人类锚点隔离,不能共用;地图、主体、意图胶囊或动作任一变化都会让
|
||||
旧解锁失效。管理员 token 只能证明入口认证,不能绕过地图、意图状态和 Agent 解锁。
|
||||
HTTP 头中的主体编号必须使用注册表登记的 ASCII 传输别名,例如 `ICE-GL-INF`;显示层可
|
||||
继续呈现 `ICE-GL∞`,两者必须由编号注册表逐条映射,不能临时猜写。
|
||||
|
||||
root 可以在物理层停止或移除本服务,但绕过本门直接执行的动作属于
|
||||
`OUT_OF_SYSTEM · NO_GUANGHU_RECEIPT`,不得登记为光湖系统内部成功。
|
||||
|
||||
固定动作只有:`health_check`、`backup`、`deploy_release`、`restart_service`、`rollback`。部署、重启和回滚必须在预检中具备备份引用与回滚计划;预检通过也只是“可申请人类授权”,不会执行。
|
||||
|
||||
`fixed_agent_connector.py` 是灯塔之后的 root 所有本地边界。它先消费一个完全匹配的
|
||||
短期解锁,再把精确的 Agent/动作/目标三元组映射到代码中固化的可执行文件;调用者不能
|
||||
传入命令或可执行路径。首个注册连接器 `aw-hlcc-candidate-deploy` 只会安装并启动锁定到
|
||||
已审核提交的、仅监听回环地址的 HLCC 隔离候选实例。
|
||||
|
||||
`agent_gate_client.py` 负责按固定顺序读取地图、按当前主体签收、恢复外显意图胶囊、申请
|
||||
单 Agent/单动作解锁,并把一次性解锁交给固定连接器。管理员凭据只从进程环境读取,不
|
||||
写入参数、意图胶囊、仓库或回执。
|
||||
|
|
|
|||
127
server-tools/enterprise-lighthouse/agent_gate_client.py
Executable file
127
server-tools/enterprise-lighthouse/agent_gate_client.py
Executable file
|
|
@ -0,0 +1,127 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Read, acknowledge, restore intent, unlock one Agent, then call the fixed connector."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
BASE_URL = "http://127.0.0.1:8031"
|
||||
CONNECTOR = "/usr/local/sbin/guanghu-fixed-agent-connector"
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--intent-state", required=True)
|
||||
parser.add_argument("--principal-id", required=True)
|
||||
parser.add_argument("--subject-kind", required=True, choices=("human", "persona", "persona_system"))
|
||||
parser.add_argument("--human-anchor", required=True)
|
||||
parser.add_argument("--agent-id", required=True)
|
||||
parser.add_argument("--action", required=True)
|
||||
parser.add_argument("--target-node-id", required=True)
|
||||
parser.add_argument("--execute", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def request_json(path, token="", headers=None, payload=None):
|
||||
request_headers = dict(headers or {})
|
||||
if token:
|
||||
request_headers["X-Lighthouse-Admin-Token"] = token
|
||||
data = None
|
||||
method = "GET"
|
||||
if payload is not None:
|
||||
request_headers["Content-Type"] = "application/json"
|
||||
data = json.dumps(payload).encode()
|
||||
method = "POST"
|
||||
request = urllib.request.Request(
|
||||
BASE_URL + path,
|
||||
data=data,
|
||||
method=method,
|
||||
headers=request_headers,
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=10) as response:
|
||||
return json.load(response)
|
||||
except urllib.error.HTTPError as error:
|
||||
print("Gate rejected %s with HTTP %d." % (path, error.code), file=sys.stderr)
|
||||
raise SystemExit(4)
|
||||
except (OSError, ValueError) as error:
|
||||
print("Gate request failed: %s" % error, file=sys.stderr)
|
||||
raise SystemExit(4)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
token = os.environ.get("LIGHTHOUSE_ADMIN_TOKEN", "")
|
||||
if not token:
|
||||
print("LIGHTHOUSE_ADMIN_TOKEN is required in the process environment.", file=sys.stderr)
|
||||
raise SystemExit(3)
|
||||
with open(args.intent_state, encoding="utf-8") as handle:
|
||||
capsule = json.load(handle)
|
||||
identity_headers = {
|
||||
"X-Guanghu-Principal-Id": args.principal_id,
|
||||
"X-Guanghu-Subject-Kind": args.subject_kind,
|
||||
"X-Guanghu-Human-Anchor": args.human_anchor,
|
||||
}
|
||||
navigation = request_json("/v2/global-navigation-map")
|
||||
acknowledged = request_json(
|
||||
"/v1/navigation-map/ack",
|
||||
token=token,
|
||||
headers=identity_headers,
|
||||
payload={"map_hash": navigation["map_hash"]},
|
||||
)
|
||||
if not acknowledged.get("ok"):
|
||||
raise SystemExit(4)
|
||||
restored = request_json(
|
||||
"/v1/intent-state/restore",
|
||||
token=token,
|
||||
headers=identity_headers,
|
||||
payload=capsule,
|
||||
)
|
||||
if not restored.get("ok"):
|
||||
raise SystemExit(4)
|
||||
unlocked = request_json(
|
||||
"/v1/agents/unlock",
|
||||
token=token,
|
||||
headers=identity_headers,
|
||||
payload={
|
||||
"agent_id": args.agent_id,
|
||||
"action": args.action,
|
||||
"target_node_id": args.target_node_id,
|
||||
},
|
||||
)
|
||||
unlock_id = unlocked.get("unlock_id", "")
|
||||
if not unlock_id:
|
||||
raise SystemExit(4)
|
||||
if not args.execute:
|
||||
print(unlock_id)
|
||||
return
|
||||
completed = subprocess.run(
|
||||
[
|
||||
CONNECTOR,
|
||||
"--unlock-id",
|
||||
unlock_id,
|
||||
"--principal-id",
|
||||
args.principal_id,
|
||||
"--subject-kind",
|
||||
args.subject_kind,
|
||||
"--human-anchor",
|
||||
args.human_anchor,
|
||||
"--agent-id",
|
||||
args.agent_id,
|
||||
"--action",
|
||||
args.action,
|
||||
"--target-node-id",
|
||||
args.target_node_id,
|
||||
],
|
||||
check=False,
|
||||
)
|
||||
raise SystemExit(completed.returncode)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
75
server-tools/enterprise-lighthouse/aw-hlcc-candidate-deploy
Executable file
75
server-tools/enterprise-lighthouse/aw-hlcc-candidate-deploy
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly SOURCE_ROOT="/var/tmp/hlcc-deploy-src-20260726"
|
||||
readonly SOURCE_COMMIT="b03619e359b9c5f5eeae73b15db6b255b9b243f7"
|
||||
readonly TOOL_ROOT="/usr/local/libexec/guanghu/hlcc-candidate"
|
||||
readonly PACKAGE_ROOT="/var/tmp/hlcc-offline-20260726"
|
||||
readonly CONFIG_FILE="${TOOL_ROOT}/candidate-app.enterprise.ini"
|
||||
readonly STATE_ROOT="/var/lib/guanghu-enterprise-code-channel"
|
||||
readonly RECEIPT_ROOT="${STATE_ROOT}/receipts"
|
||||
readonly RECEIPT_FILE="${RECEIPT_ROOT}/AW-GZ-001-HLCC-16.0.1.json"
|
||||
readonly GUANGHU_GROUP="$(id -gn guanghu)"
|
||||
|
||||
if [[ "$(id -u)" != "0" ]]; then
|
||||
echo "This fixed connector must be invoked by root." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "$(git -C "${SOURCE_ROOT}" rev-parse HEAD)" != "${SOURCE_COMMIT}" ]]; then
|
||||
echo "Pinned deployment source commit does not match." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if [[ ! -d "${PACKAGE_ROOT}" || ! -f "${CONFIG_FILE}" ]]; then
|
||||
echo "Verified package staging or pinned candidate configuration is missing." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
install -d -o guanghu -g "${GUANGHU_GROUP}" -m 0700 "${STATE_ROOT}"
|
||||
|
||||
if ss -ltnH 'sport = :3340' | grep -q .; then
|
||||
echo "Loopback candidate port 3340 is already in use." >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
chown -R root:"${GUANGHU_GROUP}" "${PACKAGE_ROOT}"
|
||||
chmod 0750 "${PACKAGE_ROOT}"
|
||||
find "${PACKAGE_ROOT}" -type f -exec chmod 0640 {} +
|
||||
|
||||
rollback_candidate() {
|
||||
runuser -u guanghu -- "${TOOL_ROOT}/stop-candidate.sh" || true
|
||||
}
|
||||
trap rollback_candidate ERR
|
||||
|
||||
runuser -u guanghu -- "${TOOL_ROOT}/install-candidate.sh" \
|
||||
"${PACKAGE_ROOT}" \
|
||||
"${CONFIG_FILE}"
|
||||
runuser -u guanghu -- "${TOOL_ROOT}/start-candidate.sh"
|
||||
|
||||
install -d -o guanghu -g "${GUANGHU_GROUP}" -m 0700 "${RECEIPT_ROOT}"
|
||||
runuser -u guanghu -- python3 - "${RECEIPT_FILE}" "${SOURCE_COMMIT}" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
receipt = {
|
||||
"schema": "guanghu.agent-execution-receipt/v1",
|
||||
"agent_id": "AW-HLCC-CANDIDATE-DEPLOYER",
|
||||
"action": "deploy_release",
|
||||
"target_node_id": "AW-GZ-001",
|
||||
"release": "Forgejo 16.0.1",
|
||||
"source_commit": sys.argv[2],
|
||||
"listen": "127.0.0.1:3340",
|
||||
"scope": "isolated-candidate",
|
||||
"production_cutover": False,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
with open(sys.argv[1], "w", encoding="utf-8") as handle:
|
||||
json.dump(receipt, handle, ensure_ascii=False, indent=2)
|
||||
handle.write("\n")
|
||||
PY
|
||||
chmod 0600 "${RECEIPT_FILE}"
|
||||
trap - ERR
|
||||
|
||||
echo "AW-HLCC-CANDIDATE-DEPLOYER completed the registered deployment action."
|
||||
91
server-tools/enterprise-lighthouse/fixed_agent_connector.py
Executable file
91
server-tools/enterprise-lighthouse/fixed_agent_connector.py
Executable file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Consume one lighthouse unlock and execute one compiled-in local connector."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
LIGHTHOUSE_URL = "http://127.0.0.1:8031"
|
||||
FIXED_CONNECTORS = {
|
||||
(
|
||||
"AW-HLCC-CANDIDATE-DEPLOYER",
|
||||
"deploy_release",
|
||||
"AW-GZ-001",
|
||||
): "/usr/local/libexec/guanghu/aw-hlcc-candidate-deploy",
|
||||
}
|
||||
|
||||
|
||||
def fail(message, code=2):
|
||||
print(message, file=sys.stderr)
|
||||
raise SystemExit(code)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--unlock-id", required=True)
|
||||
parser.add_argument("--principal-id", required=True)
|
||||
parser.add_argument("--subject-kind", required=True, choices=("human", "persona", "persona_system"))
|
||||
parser.add_argument("--human-anchor", required=True)
|
||||
parser.add_argument("--agent-id", required=True)
|
||||
parser.add_argument("--action", required=True)
|
||||
parser.add_argument("--target-node-id", required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def consume_unlock(args, token):
|
||||
payload = json.dumps(
|
||||
{
|
||||
"unlock_id": args.unlock_id,
|
||||
"agent_id": args.agent_id,
|
||||
"action": args.action,
|
||||
"target_node_id": args.target_node_id,
|
||||
}
|
||||
).encode()
|
||||
request = urllib.request.Request(
|
||||
LIGHTHOUSE_URL + "/v1/agents/consume",
|
||||
data=payload,
|
||||
method="POST",
|
||||
headers={
|
||||
"X-Lighthouse-Admin-Token": token,
|
||||
"Content-Type": "application/json",
|
||||
"X-Guanghu-Principal-Id": args.principal_id,
|
||||
"X-Guanghu-Subject-Kind": args.subject_kind,
|
||||
"X-Guanghu-Human-Anchor": args.human_anchor,
|
||||
},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=10) as response:
|
||||
body = json.load(response)
|
||||
except urllib.error.HTTPError as error:
|
||||
fail("Lighthouse rejected the connector request with HTTP %d." % error.code, 4)
|
||||
except (OSError, ValueError) as error:
|
||||
fail("Lighthouse connector request failed: %s" % error, 4)
|
||||
if not body.get("ok") or not body.get("consumed"):
|
||||
fail("Lighthouse did not return a consumed Agent unlock.", 4)
|
||||
|
||||
|
||||
def main():
|
||||
if os.geteuid() != 0:
|
||||
fail("The fixed connector must be invoked by root.", 3)
|
||||
args = parse_args()
|
||||
key = (args.agent_id, args.action, args.target_node_id)
|
||||
executable = FIXED_CONNECTORS.get(key)
|
||||
if not executable:
|
||||
fail("No fixed local connector is registered for this Agent/action/target.", 3)
|
||||
token = os.environ.get("LIGHTHOUSE_ADMIN_TOKEN", "")
|
||||
if not token:
|
||||
fail("LIGHTHOUSE_ADMIN_TOKEN is required but must not be passed on the command line.", 3)
|
||||
if not os.path.isfile(executable) or not os.access(executable, os.X_OK):
|
||||
fail("The registered fixed local connector is unavailable.", 3)
|
||||
consume_unlock(args, token)
|
||||
completed = subprocess.run([executable], check=False)
|
||||
raise SystemExit(completed.returncode)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -19,7 +19,17 @@ DB = os.environ.get("LIGHTHOUSE_DB", "/var/lib/guanghu-enterprise-lighthouse/lig
|
|||
TOKEN = os.environ.get("LIGHTHOUSE_ADMIN_TOKEN", "")
|
||||
HOST = os.environ.get("LIGHTHOUSE_BIND", "127.0.0.1")
|
||||
PORT = int(os.environ.get("LIGHTHOUSE_PORT", "8031"))
|
||||
NODE_MAP_PATH = os.environ.get("LIGHTHOUSE_NODE_MAP", "/etc/guanghu/navigation-maps/AW-GZ-001.json")
|
||||
MAP_ACK_TTL = int(os.environ.get("LIGHTHOUSE_MAP_ACK_TTL", "900"))
|
||||
INTENT_STATE_TTL = int(os.environ.get("LIGHTHOUSE_INTENT_STATE_TTL", "1800"))
|
||||
AGENT_UNLOCK_TTL = int(os.environ.get("LIGHTHOUSE_AGENT_UNLOCK_TTL", "600"))
|
||||
FIXED_ACTIONS = {"health_check", "backup", "deploy_release", "restart_service", "rollback"}
|
||||
REGISTERED_AGENTS = {
|
||||
"AW-INSPECTOR": {"health_check"},
|
||||
"AW-HLCC-CANDIDATE-DEPLOYER": {"health_check", "backup", "deploy_release", "restart_service", "rollback"},
|
||||
}
|
||||
SUBJECT_KINDS = {"human", "persona", "persona_system"}
|
||||
FORBIDDEN_INTENT_KEYS = {"password", "token", "secret", "private_key", "authorization_code", "credential"}
|
||||
DOMAINS = {
|
||||
"DOMAIN-ZS": "零感域", "DOMAIN-MAIN": "光湖主域", "DOMAIN-SUB": "光湖分域",
|
||||
"DOMAIN-ZERO": "光湖零域", "DOMAIN-FIFTH": "第五域",
|
||||
|
|
@ -57,6 +67,15 @@ def connection():
|
|||
CREATE TABLE IF NOT EXISTS map_acks (
|
||||
principal TEXT PRIMARY KEY, map_hash TEXT NOT NULL, acknowledged_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS intent_states (
|
||||
principal TEXT PRIMARY KEY, capsule_hash TEXT NOT NULL, capsule TEXT NOT NULL,
|
||||
restored_at INTEGER NOT NULL, expires_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS agent_unlocks (
|
||||
id TEXT PRIMARY KEY, principal TEXT NOT NULL, agent_id TEXT NOT NULL,
|
||||
action TEXT NOT NULL, target_node_id TEXT NOT NULL, map_hash TEXT NOT NULL,
|
||||
capsule_hash TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL
|
||||
);
|
||||
""")
|
||||
for domain_id, name in DOMAINS.items():
|
||||
state = "EXTERNAL_PRIVATE_FOUNDATION" if domain_id in EXTERNAL_FOUNDATION_DOMAINS else "PENDING_ENTRY_NODE"
|
||||
|
|
@ -86,27 +105,123 @@ def require_admin(handler):
|
|||
return bool(TOKEN) and hmac.compare_digest(received, TOKEN)
|
||||
|
||||
|
||||
def load_node_map():
|
||||
with open(NODE_MAP_PATH, encoding="utf-8") as handle:
|
||||
node_map = json.load(handle)
|
||||
if node_map.get("schema") != "guanghu.navigation-map/v1" or node_map.get("node_id") != "AW-GZ-001":
|
||||
raise ValueError("AW-GZ-001 navigation map identity is invalid")
|
||||
return node_map
|
||||
|
||||
|
||||
def navigation_map():
|
||||
body = {
|
||||
"schema": "guanghu.enterprise-navigation-map/v1",
|
||||
"schema": "guanghu.enterprise-global-navigation-map/v2",
|
||||
"world_entry": {
|
||||
"system_id": "SYS-GLW-ZY-EXEC-0001",
|
||||
"architecture": "GLS-0243",
|
||||
"rule": "The public Zhuyuan system is an intelligent operations system, not a persona or AI instance.",
|
||||
"physical_sovereignty_boundary": "Root may remove the system. Any operation that bypasses this gate is outside the Guanghu system and cannot claim a Guanghu receipt.",
|
||||
},
|
||||
"node_id": "AW-GZ-001",
|
||||
"node_map": load_node_map(),
|
||||
"domains": [{"id": key, "name": DOMAINS[key]} for key in sorted(DOMAINS)],
|
||||
"fixed_actions": sorted(FIXED_ACTIONS),
|
||||
"mandatory_order": ["read-navigation-map", "ack-current-map", "execute-registered-action"],
|
||||
"forbidden": ["raw-shell", "unregistered-action", "secret-in-repository", "unmapped-human-operation"],
|
||||
"registered_agents": {agent: sorted(actions) for agent, actions in sorted(REGISTERED_AGENTS.items())},
|
||||
"mandatory_order": [
|
||||
"read-global-navigation-map",
|
||||
"ack-current-map-as-current-subject",
|
||||
"restore-intent-state-capsule",
|
||||
"unlock-one-registered-agent-for-one-action",
|
||||
"execute-registered-action",
|
||||
"write-verification-receipt",
|
||||
],
|
||||
"forbidden": [
|
||||
"raw-shell",
|
||||
"unregistered-action",
|
||||
"secret-in-repository-or-intent-state",
|
||||
"shared-map-ack-between-subjects",
|
||||
"agent-unlock-without-current-intent-state",
|
||||
"unmapped-human-or-persona-operation",
|
||||
],
|
||||
}
|
||||
encoded = json.dumps(body, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
|
||||
return body, hashlib.sha256(encoded).hexdigest()
|
||||
|
||||
|
||||
def admin_principal():
|
||||
return hashlib.sha256(TOKEN.encode()).hexdigest()
|
||||
def request_principal(handler):
|
||||
principal_id = handler.headers.get("X-Guanghu-Principal-Id", "").strip()
|
||||
subject_kind = handler.headers.get("X-Guanghu-Subject-Kind", "").strip()
|
||||
human_anchor = handler.headers.get("X-Guanghu-Human-Anchor", "").strip()
|
||||
if not principal_id or subject_kind not in SUBJECT_KINDS or not human_anchor:
|
||||
return None
|
||||
identity = f"{subject_kind}|{principal_id}|{human_anchor}".encode()
|
||||
return {
|
||||
"id": principal_id,
|
||||
"subject_kind": subject_kind,
|
||||
"human_anchor": human_anchor,
|
||||
"digest": hmac.new(TOKEN.encode(), identity, hashlib.sha256).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def has_current_map_ack(db):
|
||||
def has_current_map_ack(db, principal_digest):
|
||||
_, current_hash = navigation_map()
|
||||
row = db.execute("SELECT map_hash FROM map_acks WHERE principal=?", (admin_principal(),)).fetchone()
|
||||
return bool(row and hmac.compare_digest(row["map_hash"], current_hash))
|
||||
row = db.execute("SELECT map_hash, acknowledged_at FROM map_acks WHERE principal=?", (principal_digest,)).fetchone()
|
||||
return bool(
|
||||
row
|
||||
and hmac.compare_digest(row["map_hash"], current_hash)
|
||||
and now() - row["acknowledged_at"] <= MAP_ACK_TTL
|
||||
)
|
||||
|
||||
|
||||
def has_forbidden_intent_key(value):
|
||||
if isinstance(value, dict):
|
||||
return any(str(key).lower() in FORBIDDEN_INTENT_KEYS or has_forbidden_intent_key(item) for key, item in value.items())
|
||||
if isinstance(value, list):
|
||||
return any(has_forbidden_intent_key(item) for item in value)
|
||||
return False
|
||||
|
||||
|
||||
def validate_intent_capsule(payload, principal):
|
||||
required = {
|
||||
"schema", "human_anchor", "persona_id", "task_intent", "identity_boundary",
|
||||
"established_facts", "decisions", "rejected_routes", "authorization_state",
|
||||
"current_checkpoint", "next_action", "completion_definition", "evidence",
|
||||
}
|
||||
if payload.get("schema") != "guanghu.intent-state-capsule/v1" or required.difference(payload):
|
||||
raise ValueError("a complete guanghu.intent-state-capsule/v1 is required")
|
||||
if payload["human_anchor"] != principal["human_anchor"]:
|
||||
raise ValueError("intent-state human anchor does not match the current subject")
|
||||
if has_forbidden_intent_key(payload):
|
||||
raise ValueError("credentials and secrets are forbidden in intent state")
|
||||
for field in ("established_facts", "decisions", "rejected_routes", "completion_definition", "evidence"):
|
||||
if not isinstance(payload[field], list):
|
||||
raise ValueError(f"{field} must be a list")
|
||||
encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
return encoded, hashlib.sha256(encoded.encode()).hexdigest()
|
||||
|
||||
|
||||
def current_intent_state(db, principal_digest):
|
||||
return db.execute(
|
||||
"SELECT capsule_hash, expires_at FROM intent_states WHERE principal=? AND expires_at>=?",
|
||||
(principal_digest, now()),
|
||||
).fetchone()
|
||||
|
||||
|
||||
def valid_agent_unlock(db, unlock_id, principal_digest, agent_id, action, target_node_id):
|
||||
_, map_hash = navigation_map()
|
||||
intent = current_intent_state(db, principal_digest)
|
||||
if not intent:
|
||||
return False
|
||||
row = db.execute(
|
||||
"""SELECT * FROM agent_unlocks
|
||||
WHERE id=? AND principal=? AND agent_id=? AND action=? AND target_node_id=? AND expires_at>=?""",
|
||||
(unlock_id, principal_digest, agent_id, action, target_node_id, now()),
|
||||
).fetchone()
|
||||
return bool(
|
||||
row
|
||||
and hmac.compare_digest(row["map_hash"], map_hash)
|
||||
and hmac.compare_digest(row["capsule_hash"], intent["capsule_hash"])
|
||||
)
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
|
|
@ -128,12 +243,19 @@ class Handler(BaseHTTPRequestHandler):
|
|||
db = connection()
|
||||
try:
|
||||
if self.path == "/health":
|
||||
return self.respond(200, {"ok": True, "service": "guanghu-enterprise-lighthouse", "mode": "registry-and-preflight-only", "execution": "disabled"})
|
||||
try:
|
||||
_, map_hash = navigation_map()
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
return self.respond(503, {"ok": False, "service": "guanghu-enterprise-lighthouse", "error": f"navigation map unavailable: {error}"})
|
||||
return self.respond(200, {"ok": True, "service": "guanghu-enterprise-lighthouse", "mode": "map-intent-agent-gate", "map_hash": map_hash, "execution": "disabled"})
|
||||
if self.path == "/v1/status":
|
||||
counts = {row["state"]: row["count"] for row in db.execute("SELECT state, COUNT(*) AS count FROM nodes GROUP BY state")}
|
||||
return self.respond(200, {"ok": True, "domains": [dict(row) for row in db.execute("SELECT id,name,state FROM domains ORDER BY id")], "node_counts": counts, "fixed_actions": sorted(FIXED_ACTIONS), "raw_shell": "rejected"})
|
||||
if self.path == "/v1/navigation-map":
|
||||
if self.path in {"/v1/navigation-map", "/v2/global-navigation-map"}:
|
||||
try:
|
||||
body, map_hash = navigation_map()
|
||||
except (OSError, ValueError, json.JSONDecodeError) as error:
|
||||
return self.respond(503, {"ok": False, "error": f"navigation map unavailable: {error}"})
|
||||
return self.respond(200, {"ok": True, "map_hash": map_hash, "navigation_map": body})
|
||||
if self.path == "/v1/nodes":
|
||||
return self.respond(200, {"ok": True, "nodes": [dict(row) for row in db.execute("SELECT id,domain_id,state,display_name,allowed_actions,last_heartbeat,created_at FROM nodes ORDER BY created_at DESC")]})
|
||||
|
|
@ -150,6 +272,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||
return self.respond(400, {"ok": False, "error": str(error)})
|
||||
if "cmd" in payload or "shell" in payload or "command" in payload:
|
||||
return self.respond(400, {"ok": False, "error": "raw commands are never accepted by the lighthouse"})
|
||||
principal = request_principal(self)
|
||||
if not principal:
|
||||
return self.respond(400, {"ok": False, "error": "current human or persona identity and human anchor are required"})
|
||||
db = connection()
|
||||
try:
|
||||
if self.path == "/v1/navigation-map/ack":
|
||||
|
|
@ -157,11 +282,53 @@ class Handler(BaseHTTPRequestHandler):
|
|||
supplied = str(payload.get("map_hash", ""))
|
||||
if not hmac.compare_digest(supplied, current_hash):
|
||||
return self.respond(409, {"ok": False, "error": "navigation map changed; read the current map again"})
|
||||
db.execute("INSERT OR REPLACE INTO map_acks VALUES (?, ?, ?)", (admin_principal(), current_hash, now()))
|
||||
audit(db, "navigation_map_acknowledged", {"map_hash": current_hash})
|
||||
return self.respond(200, {"ok": True, "map_hash": current_hash})
|
||||
if not has_current_map_ack(db):
|
||||
db.execute("INSERT OR REPLACE INTO map_acks VALUES (?, ?, ?)", (principal["digest"], current_hash, now()))
|
||||
audit(db, "navigation_map_acknowledged", {"map_hash": current_hash, "principal_id": principal["id"], "subject_kind": principal["subject_kind"]})
|
||||
return self.respond(200, {"ok": True, "map_hash": current_hash, "expires_in": MAP_ACK_TTL})
|
||||
if not has_current_map_ack(db, principal["digest"]):
|
||||
return self.respond(423, {"ok": False, "error": "current navigation map must be read and acknowledged before any operation", "required": ["GET /v1/navigation-map", "POST /v1/navigation-map/ack"]})
|
||||
if self.path == "/v1/intent-state/restore":
|
||||
try:
|
||||
encoded, capsule_hash = validate_intent_capsule(payload, principal)
|
||||
except ValueError as error:
|
||||
return self.respond(400, {"ok": False, "error": str(error)})
|
||||
restored_at = now()
|
||||
db.execute(
|
||||
"INSERT OR REPLACE INTO intent_states VALUES (?, ?, ?, ?, ?)",
|
||||
(principal["digest"], capsule_hash, encoded, restored_at, restored_at + INTENT_STATE_TTL),
|
||||
)
|
||||
audit(db, "intent_state_restored", {"principal_id": principal["id"], "capsule_hash": capsule_hash})
|
||||
return self.respond(200, {"ok": True, "capsule_hash": capsule_hash, "expires_in": INTENT_STATE_TTL})
|
||||
if self.path == "/v1/agents/unlock":
|
||||
agent_id = str(payload.get("agent_id", ""))
|
||||
action = str(payload.get("action", ""))
|
||||
target_node_id = str(payload.get("target_node_id", ""))
|
||||
intent = current_intent_state(db, principal["digest"])
|
||||
if not intent:
|
||||
return self.respond(423, {"ok": False, "error": "current intent state must be restored before an Agent can unlock"})
|
||||
if agent_id not in REGISTERED_AGENTS or action not in REGISTERED_AGENTS[agent_id] or target_node_id != "AW-GZ-001":
|
||||
return self.respond(403, {"ok": False, "error": "Agent, action, or target is not registered"})
|
||||
_, map_hash = navigation_map()
|
||||
unlock_id = "UNLOCK-" + uuid.uuid4().hex.upper()
|
||||
created_at = now()
|
||||
db.execute(
|
||||
"INSERT INTO agent_unlocks VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(unlock_id, principal["digest"], agent_id, action, target_node_id, map_hash, intent["capsule_hash"], created_at, created_at + AGENT_UNLOCK_TTL),
|
||||
)
|
||||
audit(db, "agent_unlocked", {"principal_id": principal["id"], "agent_id": agent_id, "action": action, "target_node_id": target_node_id, "unlock_id": unlock_id})
|
||||
return self.respond(200, {"ok": True, "unlock_id": unlock_id, "agent_id": agent_id, "action": action, "target_node_id": target_node_id, "expires_in": AGENT_UNLOCK_TTL})
|
||||
if self.path == "/v1/agents/consume":
|
||||
agent_id = str(payload.get("agent_id", ""))
|
||||
action = str(payload.get("action", ""))
|
||||
target_node_id = str(payload.get("target_node_id", ""))
|
||||
unlock_id = str(payload.get("unlock_id", ""))
|
||||
if not valid_agent_unlock(db, unlock_id, principal["digest"], agent_id, action, target_node_id):
|
||||
return self.respond(423, {"ok": False, "error": "a current matching Agent unlock is required"})
|
||||
db.execute("DELETE FROM agent_unlocks WHERE id=?", (unlock_id,))
|
||||
audit(db, "agent_unlock_consumed", {"principal_id": principal["id"], "agent_id": agent_id, "action": action, "target_node_id": target_node_id, "unlock_id": unlock_id})
|
||||
return self.respond(200, {"ok": True, "unlock_id": unlock_id, "consumed": True, "execution": "not performed by the lighthouse; pass this receipt to the fixed local connector"})
|
||||
if not current_intent_state(db, principal["digest"]):
|
||||
return self.respond(423, {"ok": False, "error": "current intent state must be restored before any operation"})
|
||||
if self.path == "/v1/intakes":
|
||||
required = ("human_name", "email", "server_ip", "domain_id")
|
||||
if any(not payload.get(field) for field in required) or payload["domain_id"] not in DOMAINS or not valid_email(payload["email"]):
|
||||
|
|
@ -225,6 +392,10 @@ class Handler(BaseHTTPRequestHandler):
|
|||
action, node_id = payload.get("action"), payload.get("target_node_id")
|
||||
if action not in FIXED_ACTIONS or not node_id:
|
||||
return self.respond(400, {"ok": False, "error": "fixed action and target_node_id are required", "allowed_actions": sorted(FIXED_ACTIONS)})
|
||||
agent_id = str(payload.get("agent_id", ""))
|
||||
unlock_id = str(payload.get("unlock_id", ""))
|
||||
if not valid_agent_unlock(db, unlock_id, principal["digest"], agent_id, action, node_id):
|
||||
return self.respond(423, {"ok": False, "decision": "REJECT", "reason": "a current matching Agent unlock is required"})
|
||||
node = db.execute("SELECT * FROM nodes WHERE id=?", (node_id,)).fetchone()
|
||||
if not node:
|
||||
return self.respond(404, {"ok": False, "decision": "REJECT", "reason": "target node is not registered"})
|
||||
|
|
|
|||
|
|
@ -5,42 +5,145 @@ import subprocess
|
|||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).parent
|
||||
NODE_MAP = ROOT.parents[1] / "deployment" / "navigation-maps" / "AW-GZ-001.json"
|
||||
BASE = "http://127.0.0.1:48031"
|
||||
HEADERS = {
|
||||
"X-Lighthouse-Admin-Token": "test-token",
|
||||
"X-Guanghu-Principal-Id": "ICE-GL-ZY001:TEST-INSTANCE",
|
||||
"X-Guanghu-Subject-Kind": "persona",
|
||||
"X-Guanghu-Human-Anchor": "ICE-GL-INF",
|
||||
}
|
||||
|
||||
|
||||
def post(path, payload, headers=HEADERS):
|
||||
request = urllib.request.Request(
|
||||
BASE + path,
|
||||
data=json.dumps(payload).encode(),
|
||||
method="POST",
|
||||
headers=headers,
|
||||
)
|
||||
return json.load(urllib.request.urlopen(request))
|
||||
|
||||
|
||||
def expect_error(code, path, payload, headers=HEADERS):
|
||||
try:
|
||||
post(path, payload, headers)
|
||||
except urllib.error.HTTPError as error:
|
||||
assert error.code == code, (error.code, json.load(error))
|
||||
return json.load(error)
|
||||
raise AssertionError(f"{path} unexpectedly succeeded")
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
env = {**os.environ, "LIGHTHOUSE_DB": f"{temp}/lighthouse.db", "LIGHTHOUSE_ADMIN_TOKEN": "test-token", "LIGHTHOUSE_PORT": "48031"}
|
||||
process = subprocess.Popen([sys.executable, "lighthouse.py"], cwd=ROOT, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
env = {
|
||||
**os.environ,
|
||||
"LIGHTHOUSE_DB": f"{temp}/lighthouse.db",
|
||||
"LIGHTHOUSE_ADMIN_TOKEN": "test-token",
|
||||
"LIGHTHOUSE_PORT": "48031",
|
||||
"LIGHTHOUSE_NODE_MAP": str(NODE_MAP),
|
||||
}
|
||||
process = subprocess.Popen(
|
||||
[sys.executable, "lighthouse.py"],
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
try:
|
||||
for _ in range(30):
|
||||
try:
|
||||
assert json.load(urllib.request.urlopen("http://127.0.0.1:48031/health", timeout=1))["execution"] == "disabled"
|
||||
health = json.load(urllib.request.urlopen(BASE + "/health", timeout=1))
|
||||
assert health["execution"] == "disabled"
|
||||
assert health["mode"] == "map-intent-agent-gate"
|
||||
break
|
||||
except OSError:
|
||||
time.sleep(.1)
|
||||
else: raise AssertionError("server did not start")
|
||||
locked = urllib.request.Request("http://127.0.0.1:48031/v1/intakes", data=json.dumps({"human_name":"Test","email":"test@example.invalid","server_ip":"203.0.113.8","domain_id":"DOMAIN-ZS"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"})
|
||||
try: urllib.request.urlopen(locked)
|
||||
except urllib.error.HTTPError as error: assert error.code == 423
|
||||
else: raise AssertionError("mutation was allowed before navigation map acknowledgement")
|
||||
nav = json.load(urllib.request.urlopen("http://127.0.0.1:48031/v1/navigation-map"))
|
||||
ack = urllib.request.Request("http://127.0.0.1:48031/v1/navigation-map/ack", data=json.dumps({"map_hash":nav["map_hash"]}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"})
|
||||
assert json.load(urllib.request.urlopen(ack))["ok"] is True
|
||||
request = urllib.request.Request("http://127.0.0.1:48031/v1/intakes", data=json.dumps({"human_name":"Test","email":"test@example.invalid","server_ip":"203.0.113.8","domain_id":"DOMAIN-ZS"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"})
|
||||
assert json.load(urllib.request.urlopen(request))["state"] == "PENDING_REVIEW"
|
||||
bootstrap = urllib.request.Request("http://127.0.0.1:48031/v1/nodes/bootstrap", data=json.dumps({"id":"NODE-TEST-001","domain_id":"DOMAIN-ZS","display_name":"Test entry","server_ip":"203.0.113.8"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"})
|
||||
assert json.load(urllib.request.urlopen(bootstrap))["state"] == "CONNECTED_PENDING_CONNECTOR"
|
||||
preflight = urllib.request.Request("http://127.0.0.1:48031/v1/preflight", data=json.dumps({"action":"health_check","target_node_id":"NODE-TEST-001"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"})
|
||||
assert json.load(urllib.request.urlopen(preflight))["decision"] == "REJECT"
|
||||
fifth = urllib.request.Request("http://127.0.0.1:48031/v1/nodes/bootstrap", data=json.dumps({"id":"NODE-FIFTH-001","domain_id":"DOMAIN-FIFTH","display_name":"Not allowed","server_ip":"203.0.113.8"}).encode(), method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"})
|
||||
try: urllib.request.urlopen(fifth)
|
||||
except urllib.error.HTTPError as error: assert error.code == 403
|
||||
else: raise AssertionError("private fifth domain was accepted by enterprise lighthouse")
|
||||
bad = urllib.request.Request("http://127.0.0.1:48031/v1/intakes", data=b'{"cmd":"rm -rf /"}', method="POST", headers={"X-Lighthouse-Admin-Token":"test-token"})
|
||||
try: urllib.request.urlopen(bad)
|
||||
except urllib.error.HTTPError as error: assert error.code == 400
|
||||
else: raise AssertionError("raw command was not rejected")
|
||||
else:
|
||||
raise AssertionError("server did not start")
|
||||
|
||||
intake = {
|
||||
"human_name": "Test",
|
||||
"email": "test@example.invalid",
|
||||
"server_ip": "203.0.113.8",
|
||||
"domain_id": "DOMAIN-ZS",
|
||||
}
|
||||
expect_error(423, "/v1/intakes", intake)
|
||||
|
||||
nav = json.load(urllib.request.urlopen(BASE + "/v2/global-navigation-map"))
|
||||
assert nav["navigation_map"]["schema"] == "guanghu.enterprise-global-navigation-map/v2"
|
||||
assert nav["navigation_map"]["node_map"]["node_id"] == "AW-GZ-001"
|
||||
assert post("/v1/navigation-map/ack", {"map_hash": nav["map_hash"]})["ok"] is True
|
||||
|
||||
expect_error(423, "/v1/intakes", intake)
|
||||
capsule = {
|
||||
"schema": "guanghu.intent-state-capsule/v1",
|
||||
"human_anchor": "ICE-GL-INF",
|
||||
"persona_id": "ICE-GL-ZY001",
|
||||
"task_intent": "Test the enterprise navigation and Agent gate.",
|
||||
"identity_boundary": "The public operations system is not a persona.",
|
||||
"established_facts": ["AW map is current."],
|
||||
"decisions": ["Use only a registered Agent."],
|
||||
"rejected_routes": ["raw shell"],
|
||||
"authorization_state": "test-only",
|
||||
"current_checkpoint": "map acknowledged",
|
||||
"next_action": "unlock inspector",
|
||||
"completion_definition": ["preflight is gated"],
|
||||
"evidence": ["deployment/navigation-maps/AW-GZ-001.json"],
|
||||
}
|
||||
restored = post("/v1/intent-state/restore", capsule)
|
||||
assert restored["ok"] is True
|
||||
assert json.load(urllib.request.urlopen(urllib.request.Request(
|
||||
BASE + "/v1/intakes",
|
||||
data=json.dumps(intake).encode(),
|
||||
method="POST",
|
||||
headers=HEADERS,
|
||||
)))["state"] == "PENDING_REVIEW"
|
||||
|
||||
bootstrap = post("/v1/nodes/bootstrap", {
|
||||
"id": "AW-GZ-001",
|
||||
"domain_id": "DOMAIN-ZS",
|
||||
"display_name": "Enterprise root node",
|
||||
"server_ip": "203.0.113.8",
|
||||
})
|
||||
assert bootstrap["state"] == "CONNECTED_PENDING_CONNECTOR"
|
||||
|
||||
preflight = {"action": "health_check", "target_node_id": "AW-GZ-001", "agent_id": "AW-INSPECTOR"}
|
||||
expect_error(423, "/v1/preflight", preflight)
|
||||
unlocked = post("/v1/agents/unlock", {
|
||||
"agent_id": "AW-INSPECTOR",
|
||||
"action": "health_check",
|
||||
"target_node_id": "AW-GZ-001",
|
||||
})
|
||||
preflight["unlock_id"] = unlocked["unlock_id"]
|
||||
assert post("/v1/preflight", preflight)["decision"] == "REJECT"
|
||||
assert post("/v1/agents/consume", {
|
||||
**preflight,
|
||||
"unlock_id": unlocked["unlock_id"],
|
||||
})["consumed"] is True
|
||||
expect_error(423, "/v1/preflight", preflight)
|
||||
|
||||
expect_error(403, "/v1/nodes/bootstrap", {
|
||||
"id": "NODE-FIFTH-001",
|
||||
"domain_id": "DOMAIN-FIFTH",
|
||||
"display_name": "Not allowed",
|
||||
"server_ip": "203.0.113.8",
|
||||
})
|
||||
expect_error(400, "/v1/intakes", {"cmd": "unsafe"})
|
||||
|
||||
other_headers = {
|
||||
**HEADERS,
|
||||
"X-Guanghu-Principal-Id": "ANOTHER-PERSONA:TEST",
|
||||
}
|
||||
expect_error(423, "/v1/intent-state/restore", capsule, other_headers)
|
||||
assert post("/v1/navigation-map/ack", {"map_hash": nav["map_hash"]}, other_headers)["ok"] is True
|
||||
expect_error(400, "/v1/intent-state/restore", {**capsule, "token": "forbidden"}, other_headers)
|
||||
finally:
|
||||
process.terminate(); process.wait(timeout=5)
|
||||
process.terminate()
|
||||
process.wait(timeout=5)
|
||||
|
||||
print("enterprise lighthouse tests passed")
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ RUN_MODE = prod
|
|||
|
||||
[database]
|
||||
DB_TYPE = sqlite3
|
||||
PATH = /var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1/data/hlcc.db
|
||||
PATH = /var/lib/guanghu-enterprise-code-channel/candidates/hlcc-v16.0.1/data/hlcc.db
|
||||
|
||||
[repository]
|
||||
ROOT = /var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1/data/repositories
|
||||
ROOT = /var/lib/guanghu-enterprise-code-channel/candidates/hlcc-v16.0.1/data/repositories
|
||||
|
||||
[server]
|
||||
DOMAIN = 127.0.0.1
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
set -euo pipefail
|
||||
|
||||
readonly HLCC_VERSION="16.0.1"
|
||||
readonly HLCC_ROOT="/var/lib/guanghu/code-channel/candidates/hlcc-v${HLCC_VERSION}"
|
||||
readonly HLCC_SOURCE_ARCHIVE="/var/lib/guanghu/code-channel/offline-source/hlcc-v${HLCC_VERSION}"
|
||||
readonly HLCC_STATE_ROOT="/var/lib/guanghu-enterprise-code-channel"
|
||||
readonly HLCC_ROOT="${HLCC_STATE_ROOT}/candidates/hlcc-v${HLCC_VERSION}"
|
||||
readonly HLCC_SOURCE_ARCHIVE="${HLCC_STATE_ROOT}/offline-source/hlcc-v${HLCC_VERSION}"
|
||||
readonly HLCC_RELEASE_KEY="EB114F5E6C0DC2BCDD183550A4B61A2DC5923710"
|
||||
readonly HLCC_PACKAGE_SOURCE="${1:-}"
|
||||
readonly HLCC_CONFIG_SOURCE="${2:-}"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly HLCC_ROOT="/var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1"
|
||||
readonly HLCC_ROOT="/var/lib/guanghu-enterprise-code-channel/candidates/hlcc-v16.0.1"
|
||||
readonly HLCC_PID_FILE="${HLCC_ROOT}/hlcc.pid"
|
||||
readonly HLCC_LOG_FILE="${HLCC_ROOT}/logs/hlcc.log"
|
||||
|
||||
|
|
@ -39,6 +39,6 @@ if ! kill -0 "${candidate_pid}" 2>/dev/null; then
|
|||
fi
|
||||
|
||||
curl --fail --silent --show-error \
|
||||
"http://127.0.0.1:3340/api/v1/version"
|
||||
"http://127.0.0.1:3340/api/healthz"
|
||||
echo
|
||||
echo "HLCC isolated candidate is running on loopback only."
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly HLCC_ROOT="/var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1"
|
||||
readonly HLCC_ROOT="/var/lib/guanghu-enterprise-code-channel/candidates/hlcc-v16.0.1"
|
||||
readonly HLCC_PID_FILE="${HLCC_ROOT}/hlcc.pid"
|
||||
|
||||
if [[ "$(id -un)" != "guanghu" ]]; then
|
||||
|
|
|
|||
Loading…
Reference in a new issue