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
|
|
@ -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":
|
||||
body, map_hash = 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"})
|
||||
|
|
|
|||
Loading…
Reference in a new issue