From 7fd716ea0956ce74e82dbeadfc6230f43d81e9fc Mon Sep 17 00:00:00 2001 From: matthewacator-cpu Date: Thu, 23 Apr 2026 20:20:53 +0700 Subject: [PATCH 1/3] Harden OCP protocol app surfaces --- docs/OPEN_COMPUTE_PROTOCOL_v0.1.md | 16 ++ mesh/sovereign.py | 24 ++- mesh_artifacts/service.py | 116 ++++++++++++- mesh_protocol/conformance.py | 67 ++++++++ mesh_protocol/schemas.py | 146 +++++++++++++++++ mesh_state/schema.py | 17 ++ ocp_startup.py | 12 ++ scripts/start_ocp_easy.py | 9 +- server.py | 17 ++ server_app.py | 121 +++++++++++++- server_app_history.py | 214 ++++++++++++++++++++++++ server_app_status.py | 252 +++++++++++++++++++++++++++++ server_artifacts.py | 4 + server_contract.py | 29 +++- server_http_handlers.py | 10 ++ server_routes.py | 2 + tests/test_sovereign_mesh.py | 250 +++++++++++++++++++++++++++- 17 files changed, 1293 insertions(+), 13 deletions(-) create mode 100644 server_app_history.py diff --git a/docs/OPEN_COMPUTE_PROTOCOL_v0.1.md b/docs/OPEN_COMPUTE_PROTOCOL_v0.1.md index 3d6e275..effd30d 100644 --- a/docs/OPEN_COMPUTE_PROTOCOL_v0.1.md +++ b/docs/OPEN_COMPUTE_PROTOCOL_v0.1.md @@ -149,8 +149,12 @@ The current reference implementation exposes the protocol under `/mesh/*`. | `/mesh/jobs/{job_id}/cancel` | POST | Cancel job | | `/mesh/artifacts/publish` | POST | Publish artifact | | `/mesh/artifacts/{artifact_id}` | GET | Fetch artifact subject to policy | +| `/mesh/artifacts/replicate` | POST | Pull and verify an artifact from a trusted peer | +| `/mesh/artifacts/replicate-graph` | POST | Pull a bundle/checkpoint graph from a trusted peer | | `/mesh/agents/handoff` | POST | Send explicit delegation packet | | `/mesh/app/status` | GET | Operator/app-facing status projection | +| `/mesh/app/history` | GET | Operator/app-facing persisted status history for local charts | +| `/mesh/app/history/sample` | POST | Record one redacted app-status sample for chart history | | `/mesh/autonomy/status` | GET | Current Autonomic Mesh posture | | `/mesh/autonomy/activate` | POST | Assisted discovery, route probing, helper planning, and proof activation | | `/mesh/routes/health` | GET | Known route-candidate health projection | @@ -181,6 +185,16 @@ Artifacts are immutable references to payloads or outputs with: The v0.1 reference implementation verifies digests and refuses downloads that violate the artifact policy. +Private remote artifact pulls are operator-mediated in v0.1.6. A caller may include: + +```json +{ + "remote_auth": {"type": "operator_token", "token": "remote-node-token"} +} +``` + +The token is used only for the outbound content fetch, is redacted from responses and events, and is not stored in artifact metadata. Signed scoped delegation grants are reserved for a future capability-law layer. + ## Agent Federation Agent federation is the first proof target of OCP v0.1. @@ -207,6 +221,8 @@ The working implementation in this repo intentionally stays pragmatic: - Golem is treated as a provider lane, not a trust authority - the standalone OCP store remains the local source of truth for mesh runtime state - app/autonomy routes are operator-facing control surfaces, not consensus or settlement surfaces +- `/mesh/app/status` is an operator/app projection that includes setup timeline, execution readiness, artifact sync, route health, and protocol status +- `/mesh/app/history` and `/mesh/app/history/sample` are local operator/app chart surfaces, not consensus-critical protocol messages ## Planned OCP v0.2 Themes diff --git a/mesh/sovereign.py b/mesh/sovereign.py index cf14c1b..56ae73b 100644 --- a/mesh/sovereign.py +++ b/mesh/sovereign.py @@ -1049,17 +1049,23 @@ def execute_job(self, job_kind: str, payload: dict, policy: dict) -> dict: class MeshPeerClient: - def __init__(self, base_url: str, *, timeout: float = 8.0): + def __init__(self, base_url: str, *, timeout: float = 8.0, operator_token: Optional[str] = None): self.base_url = (base_url or "").rstrip("/") self.timeout = float(timeout) + self.operator_token = operator_token def _operator_token(self) -> str: + if self.operator_token is not None: + return str(self.operator_token or "").strip() return ( os.environ.get("OCP_OPERATOR_TOKEN") or os.environ.get("OCP_CONTROL_TOKEN") or "" ).strip() + def with_operator_token(self, operator_token: str) -> "MeshPeerClient": + return MeshPeerClient(self.base_url, timeout=self.timeout, operator_token=operator_token) + def _request_json(self, method: str, path: str, payload: Optional[dict] = None, params: Optional[dict] = None) -> dict: url = self.base_url + path if params: @@ -1340,12 +1346,18 @@ def replicate_artifact( artifact_id: str = "", digest: str = "", pin: bool = False, + base_url: str = "", + remote_auth: Optional[dict] = None, ) -> dict: payload = {"peer_id": peer_id, "pin": bool(pin)} if artifact_id: payload["artifact_id"] = artifact_id if digest: payload["digest"] = digest + if base_url: + payload["base_url"] = base_url + if remote_auth: + payload["remote_auth"] = dict(remote_auth) return self._request_json("POST", "/mesh/artifacts/replicate", payload=payload) def replicate_artifact_graph( @@ -1355,12 +1367,18 @@ def replicate_artifact_graph( artifact_id: str = "", digest: str = "", pin: bool = False, + base_url: str = "", + remote_auth: Optional[dict] = None, ) -> dict: payload = {"peer_id": peer_id, "pin": bool(pin)} if artifact_id: payload["artifact_id"] = artifact_id if digest: payload["digest"] = digest + if base_url: + payload["base_url"] = base_url + if remote_auth: + payload["remote_auth"] = dict(remote_auth) return self._request_json("POST", "/mesh/artifacts/replicate-graph", payload=payload) def set_artifact_pin(self, artifact_id: str, *, pinned: bool = True, reason: str = "operator_pin") -> dict: @@ -6579,6 +6597,7 @@ def replicate_artifact_from_peer( base_url: Optional[str] = None, request_id: Optional[str] = None, pin: bool = False, + remote_auth: Optional[dict] = None, ) -> dict: return self.artifacts.replicate_artifact_from_peer( peer_id, @@ -6588,6 +6607,7 @@ def replicate_artifact_from_peer( base_url=base_url, request_id=request_id, pin=pin, + remote_auth=remote_auth, ) def replicate_artifact_graph_from_peer( @@ -6600,6 +6620,7 @@ def replicate_artifact_graph_from_peer( base_url: Optional[str] = None, request_id: Optional[str] = None, pin: bool = False, + remote_auth: Optional[dict] = None, ) -> dict: return self.artifacts.replicate_artifact_graph_from_peer( peer_id, @@ -6609,6 +6630,7 @@ def replicate_artifact_graph_from_peer( base_url=base_url, request_id=request_id, pin=pin, + remote_auth=remote_auth, ) def set_artifact_pin(self, artifact_id: str, *, pinned: bool = True, reason: str = "operator_pin") -> dict: diff --git a/mesh_artifacts/service.py b/mesh_artifacts/service.py index fa1e83b..a25bcc0 100644 --- a/mesh_artifacts/service.py +++ b/mesh_artifacts/service.py @@ -459,6 +459,79 @@ def resolve_remote_artifact( remote_artifact = remote_client.get_artifact(artifact_token, peer_id=self.mesh.node_id, include_content=include_content) return remote_client, remote_artifact, artifact_token + def _remote_auth_summary(self, remote_auth: Optional[dict]) -> dict: + auth = dict(remote_auth or {}) + auth_type = str(auth.get("type") or "").strip().lower() + if not auth_type: + return {"type": "none", "status": "not_used"} + if auth_type == "operator_token": + if not str(auth.get("token") or "").strip(): + raise self.mesh.MeshPolicyError("remote_auth.operator_token requires a token") + return {"type": "operator_token", "status": "used", "redacted": True} + if auth_type in {"signed_delegation", "capability_grant"}: + raise self.mesh.MeshPolicyError("signed artifact delegation is reserved for a future protocol layer") + raise self.mesh.MeshPolicyError(f"unsupported remote_auth type: {auth_type}") + + def _client_with_remote_auth(self, client, remote_auth: Optional[dict]): + auth = dict(remote_auth or {}) + auth_type = str(auth.get("type") or "").strip().lower() + if not auth_type: + if hasattr(client, "with_operator_token"): + return client.with_operator_token("") + return client + if auth_type == "operator_token": + token = str(auth.get("token") or "").strip() + if hasattr(client, "with_operator_token"): + return client.with_operator_token(token) + raise self.mesh.MeshPolicyError("remote operator token auth requires an HTTP peer client") + self._remote_auth_summary(auth) + return client + + def _fresh_route_for_replication(self, peer_id: str, *, base_url: Optional[str] = None, client=None) -> dict: + if client is not None: + return {"status": "skipped", "reason": "in_process_client"} + peer_token = str(peer_id or "").strip() + explicit_base_url = str(base_url or "").strip() + if explicit_base_url: + probe = self.mesh.probe_routes(peer_id=peer_token, base_url=explicit_base_url, timeout=3.0, limit=1) + if int(probe.get("reachable") or 0) <= 0: + raise self.mesh.MeshPolicyError("fresh reachable route required before artifact replication") + return { + "status": "fresh", + "best_route": probe.get("best_route") or explicit_base_url, + "source": "explicit_probe", + "checked_at": probe.get("generated_at") or "", + } + + routes = self.mesh.routes_health(limit=50) + for route in list(routes.get("routes") or []): + route = dict(route or {}) + if str(route.get("peer_id") or "").strip() != peer_token: + continue + if ( + str(route.get("status") or "").strip().lower() == "reachable" + and str(route.get("freshness") or "").strip().lower() == "fresh" + and str(route.get("best_route") or route.get("last_reachable_base_url") or "").strip() + ): + return { + "status": "fresh", + "best_route": str(route.get("best_route") or route.get("last_reachable_base_url") or "").strip(), + "source": "route_health", + "checked_at": route.get("checked_at") or "", + "last_success_at": route.get("last_success_at") or "", + } + break + + probe = self.mesh.probe_routes(peer_id=peer_token, timeout=3.0, limit=4) + if int(probe.get("reachable") or 0) <= 0: + raise self.mesh.MeshPolicyError("fresh reachable route required before artifact replication") + return { + "status": "fresh", + "best_route": probe.get("best_route") or "", + "source": "probe", + "checked_at": probe.get("generated_at") or "", + } + def artifact_json_payload(self, artifact: dict) -> dict: payload_bytes = b"" if str(artifact.get("content_base64") or "").strip(): @@ -646,6 +719,7 @@ def replicate_artifact_from_peer( base_url: Optional[str] = None, request_id: Optional[str] = None, pin: bool = False, + remote_auth: Optional[dict] = None, ) -> dict: peer_token = (peer_id or "").strip() if not peer_token: @@ -671,11 +745,19 @@ def replicate_artifact_from_peer( "source": {"peer_id": peer_token, "artifact_id": artifact_token, "digest": digest_token}, } + route_proof = self._fresh_route_for_replication(peer_token, base_url=base_url, client=client) + if not base_url and route_proof.get("best_route"): + base_url = str(route_proof.get("best_route") or "").strip() + auth_summary = self._remote_auth_summary(remote_auth) + remote_client_override = client + if remote_client_override is None and base_url: + remote_client_override, _ = self.mesh._resolve_peer_client(peer_token, base_url=base_url) + remote_client_override = self._client_with_remote_auth(remote_client_override, remote_auth) remote_client, remote_artifact, artifact_token = self.resolve_remote_artifact( peer_token, artifact_id=artifact_token, digest=digest_token, - client=client, + client=remote_client_override, base_url=base_url, include_content=False, ) @@ -698,9 +780,11 @@ def replicate_artifact_from_peer( "status": "already_present", "artifact": local_hit, "source": {"peer_id": peer_token, "artifact_id": artifact_token, "digest": remote_digest}, + "route_proof": route_proof, } - remote_artifact = remote_client.get_artifact(artifact_token, peer_id=self.mesh.node_id, include_content=True) + content_client = self._client_with_remote_auth(remote_client, remote_auth) + remote_artifact = content_client.get_artifact(artifact_token, peer_id=self.mesh.node_id, include_content=True) content_base64 = str(remote_artifact.get("content_base64") or "").strip() if not content_base64: raise self.mesh.MeshPolicyError("remote artifact content missing") @@ -736,6 +820,8 @@ def replicate_artifact_from_peer( "source_peer_id": peer_token, "source_artifact_id": source_artifact_id, "source_digest": remote_digest, + "route_proof": route_proof, + "remote_auth": auth_summary, "synced_at": self._utcnow(), "verified_at": verification["checked_at"], "verification_status": verification["status"], @@ -754,6 +840,8 @@ def replicate_artifact_from_peer( "digest": replicated["digest"], "source_artifact_id": source_artifact_id, "pinned": bool(pin), + "route_proof": route_proof, + "remote_auth": auth_summary, "treaty_requirements": list(governance.get("treaty_requirements") or []), }, ) @@ -762,6 +850,8 @@ def replicate_artifact_from_peer( "artifact": replicated, "source": {"peer_id": peer_token, "artifact_id": source_artifact_id, "digest": remote_digest}, "verification": verification, + "route_proof": route_proof, + "remote_auth": auth_summary, } if governance.get("treaty_requirements"): response["governance"] = governance @@ -777,20 +867,30 @@ def replicate_artifact_graph_from_peer( base_url: Optional[str] = None, request_id: Optional[str] = None, pin: bool = False, + remote_auth: Optional[dict] = None, ) -> dict: peer_token = (peer_id or "").strip() if not peer_token: raise self.mesh.MeshPolicyError("peer_id is required") + route_proof = self._fresh_route_for_replication(peer_token, base_url=base_url, client=client) + if not base_url and route_proof.get("best_route"): + base_url = str(route_proof.get("best_route") or "").strip() + auth_summary = self._remote_auth_summary(remote_auth) + remote_client_override = client + if remote_client_override is None and base_url: + remote_client_override, _ = self.mesh._resolve_peer_client(peer_token, base_url=base_url) + remote_client_override = self._client_with_remote_auth(remote_client_override, remote_auth) remote_client, remote_root, resolved_artifact_id = self.resolve_remote_artifact( peer_token, artifact_id=artifact_id, digest=digest, - client=client, + client=remote_client_override, base_url=base_url, include_content=False, ) root_governance = self.ensure_artifact_replication_allowed(peer_token, remote_root) - remote_root = remote_client.get_artifact(resolved_artifact_id, peer_id=self.mesh.node_id, include_content=True) + content_client = self._client_with_remote_auth(remote_client, remote_auth) + remote_root = content_client.get_artifact(resolved_artifact_id, peer_id=self.mesh.node_id, include_content=True) root = self.replicate_artifact_from_peer( peer_token, artifact_id=resolved_artifact_id, @@ -798,9 +898,10 @@ def replicate_artifact_graph_from_peer( client=remote_client, request_id=request_id, pin=pin, + remote_auth=remote_auth, ) pending = self.artifact_graph_targets(remote_root) - pending.extend(self.artifact_attempt_graph_targets(peer_token, remote_client=remote_client, artifact=remote_root)) + pending.extend(self.artifact_attempt_graph_targets(peer_token, remote_client=content_client, artifact=remote_root)) seen: set[tuple[str, str]] = { (str(root["artifact"].get("id") or "").strip(), str(root["artifact"].get("digest") or "").strip().lower()) } @@ -821,6 +922,7 @@ def replicate_artifact_graph_from_peer( client=remote_client, request_id=request_id, pin=pin, + remote_auth=remote_auth, ) replicated_children.append( { @@ -840,6 +942,8 @@ def replicate_artifact_graph_from_peer( "root_digest": (root.get("artifact") or {}).get("digest", ""), "replicated_count": len(replicated_children) + 1, "pinned": bool(pin), + "route_proof": route_proof, + "remote_auth": auth_summary, }, ) response = { @@ -852,6 +956,8 @@ def replicate_artifact_graph_from_peer( "count": len(replicated_children) + 1, "linked": replicated_children, }, + "route_proof": route_proof, + "remote_auth": auth_summary, } if root_governance.get("treaty_requirements"): response["governance"] = root_governance diff --git a/mesh_protocol/conformance.py b/mesh_protocol/conformance.py index c4b75d3..d98f416 100644 --- a/mesh_protocol/conformance.py +++ b/mesh_protocol/conformance.py @@ -110,6 +110,35 @@ def build_protocol_conformance_snapshot() -> dict[str, Any]: "created_at": "2026-01-01T00:00:00Z", }, ), + _fixture_entry( + "artifact-replicate-request-operator-mediated", + schema_ref="ArtifactReplicateRequest", + purpose="Explicit operator-mediated artifact pull request without persisting remote credentials.", + value={ + "peer_id": "beta-node", + "artifact_id": "artifact-fixture", + "pin": True, + "remote_auth": {"type": "operator_token", "token": "fixture-token"}, + }, + ), + _fixture_entry( + "artifact-replicate-response-redacted-auth", + schema_ref="ArtifactReplicateResponse", + purpose="Artifact replication response with route proof and redacted remote auth metadata.", + value={ + "status": "replicated", + "artifact": { + "id": "artifact-local", + "digest": "abc123", + "media_type": "application/json", + "artifact_kind": "bundle", + }, + "source": {"peer_id": "beta-node", "artifact_id": "artifact-fixture", "digest": "abc123"}, + "verification": {"status": "verified", "verified": True}, + "route_proof": {"status": "fresh", "best_route": "http://192.168.1.22:8421"}, + "remote_auth": {"type": "operator_token", "status": "used", "redacted": True}, + }, + ), _fixture_entry( "continuity-restore-request", schema_ref="ContinuityRestorePlanRequest", @@ -228,6 +257,20 @@ def build_protocol_conformance_snapshot() -> dict[str, Any]: "blocking_issue": "", "next_fix": "No fix needed. The current mesh proof completed.", "operator_summary": "Mesh is strong. Devices have proven routes and the latest proof completed.", + "timeline": [ + { + "kind": "proof_completed", + "status": "ok", + "summary": "Whole-mesh proof completed.", + "created_at": "2026-01-01T00:00:00Z", + } + ], + }, + "protocol": { + "release": "0.1", + "version": "sovereign-mesh/v1", + "schema_version": SCHEMA_VERSION, + "contract_url": "/mesh/contract", }, "autonomy": {"status": "ok", "mode": "assisted", "operator_summary": "Mesh is strong."}, "route_health": { @@ -242,6 +285,30 @@ def build_protocol_conformance_snapshot() -> dict[str, Any]: } ], }, + "execution_readiness": { + "status": "ready", + "local": {"worker_count": 1, "ready_worker_count": 1}, + "targets": [{"peer_id": "alpha-node", "status": "ready", "reasons": ["local worker registered"]}], + "worker_capacity": [ + { + "worker_id": "alpha-default-worker", + "peer_id": "alpha-node", + "status": "active", + "capabilities": ["worker-runtime", "shell"], + "resources": {"cpu": 1}, + "max_concurrent_jobs": 1, + "available_slots": 1, + } + ], + "operator_summary": "Execution is ready.", + }, + "artifact_sync": { + "status": "verified", + "replicated_count": 1, + "verified_count": 1, + "items": [], + "operator_summary": "1 replicated artifact(s) verified.", + }, "latest_proof": { "status": "completed", "mission_id": "mission-fixture", diff --git a/mesh_protocol/schemas.py b/mesh_protocol/schemas.py index 46c66a6..52250ba 100644 --- a/mesh_protocol/schemas.py +++ b/mesh_protocol/schemas.py @@ -159,6 +159,53 @@ "descriptor": {"$ref": "#/schemas/ArtifactDescriptor"}, }, }, + "ArtifactReplicationAuth": { + "type": "object", + "description": "Explicit remote-content authorization. Tokens are request-only and must never be persisted or echoed.", + "properties": { + "type": {"type": "string"}, + "token": {"type": "string"}, + "redacted": {"type": "boolean"}, + "status": {"type": "string"}, + }, + }, + "ArtifactReplicateRequest": { + "type": "object", + "properties": { + "peer_id": {"type": "string"}, + "artifact_id": {"type": "string"}, + "digest": {"type": "string"}, + "base_url": {"type": "string"}, + "pin": {"type": "boolean"}, + "remote_auth": {"$ref": "#/schemas/ArtifactReplicationAuth"}, + }, + }, + "ArtifactReplicateResponse": { + "type": "object", + "required": ["status"], + "properties": { + "status": {"type": "string"}, + "artifact": {"$ref": "#/schemas/Artifact"}, + "source": {"type": "object"}, + "verification": {"type": "object"}, + "route_proof": {"type": "object"}, + "remote_auth": {"$ref": "#/schemas/ArtifactReplicationAuth"}, + "governance": {"type": "object"}, + }, + }, + "ArtifactGraphReplicateResponse": { + "type": "object", + "required": ["status"], + "properties": { + "status": {"type": "string"}, + "root": {"$ref": "#/schemas/ArtifactReplicateResponse"}, + "artifacts": {"type": "array", "items": {"$ref": "#/schemas/Artifact"}}, + "graph": {"type": "object"}, + "route_proof": {"type": "object"}, + "remote_auth": {"$ref": "#/schemas/ArtifactReplicationAuth"}, + "governance": {"type": "object"}, + }, + }, "MissionContinuitySummary": { "type": "object", "required": ["mission_id", "continuity"], @@ -320,6 +367,19 @@ "generated_at": {"type": "string"}, }, }, + "RouteProofFreshness": { + "type": "object", + "properties": { + "status": {"type": "string"}, + "peer_id": {"type": "string"}, + "best_route": {"type": "string"}, + "freshness": {"type": "string"}, + "checked_at": {"type": "string"}, + "last_success_at": {"type": "string"}, + "source": {"type": "string"}, + "operator_summary": {"type": "string"}, + }, + }, "RouteProbeRequest": { "type": "object", "properties": { @@ -409,6 +469,88 @@ "generated_at": {"type": "string"}, }, }, + "WorkerCapacity": { + "type": "object", + "properties": { + "worker_id": {"type": "string"}, + "peer_id": {"type": "string"}, + "status": {"type": "string"}, + "capabilities": {"type": "array", "items": {"type": "string"}}, + "resources": {"type": "object"}, + "max_concurrent_jobs": {"type": "integer"}, + "available_slots": {"type": "integer"}, + "operator_summary": {"type": "string"}, + }, + }, + "ExecutionReadiness": { + "type": "object", + "properties": { + "status": {"type": "string"}, + "local": {"type": "object"}, + "targets": {"type": "array", "items": {"type": "object"}}, + "worker_capacity": {"type": "array", "items": {"$ref": "#/schemas/WorkerCapacity"}}, + "operator_summary": {"type": "string"}, + }, + }, + "SetupTimelineEvent": { + "type": "object", + "required": ["kind", "status", "summary"], + "properties": { + "kind": {"type": "string"}, + "status": {"type": "string"}, + "summary": {"type": "string"}, + "peer_id": {"type": "string"}, + "created_at": {"type": "string"}, + "details": {"type": "object"}, + }, + }, + "AppStatusSample": { + "type": "object", + "description": "Operator/app-facing normalized app status point for local charts.", + "required": ["id", "sampled_at", "node_id", "mesh_score"], + "properties": { + "id": {"type": "string"}, + "sampled_at": {"type": "string"}, + "node_id": {"type": "string"}, + "setup_status": {"type": "string"}, + "mesh_score": {"type": "integer"}, + "known_peer_count": {"type": "integer"}, + "route_count": {"type": "integer"}, + "healthy_route_count": {"type": "integer"}, + "latest_proof_status": {"type": "string"}, + "execution_ready_targets": {"type": "integer"}, + "local_ready_workers": {"type": "integer"}, + "artifact_verified_count": {"type": "integer"}, + "pending_approvals": {"type": "integer"}, + "payload": {"type": "object"}, + }, + }, + "AppStatusHistory": { + "type": "object", + "required": ["status", "count", "samples"], + "properties": { + "status": {"type": "string"}, + "count": {"type": "integer"}, + "limit": {"type": "integer"}, + "samples": {"type": "array", "items": {"$ref": "#/schemas/AppStatusSample"}}, + "generated_at": {"type": "string"}, + }, + }, + "AppHistorySampleRequest": { + "type": "object", + "properties": { + "source": {"type": "string"}, + }, + }, + "AppHistorySampleResponse": { + "type": "object", + "required": ["status", "sample"], + "properties": { + "status": {"type": "string"}, + "sample": {"$ref": "#/schemas/AppStatusSample"}, + "retention_limit": {"type": "integer"}, + }, + }, "AppStatus": { "type": "object", "description": "Operator-facing compact status for the installable OCP app home.", @@ -440,6 +582,7 @@ "operator_summary": {"type": "string"}, }, }, + "protocol": {"type": "object"}, "setup": { "type": "object", "properties": { @@ -456,10 +599,13 @@ "blocking_issue": {"type": "string"}, "next_fix": {"type": "string"}, "operator_summary": {"type": "string"}, + "timeline": {"type": "array", "items": {"$ref": "#/schemas/SetupTimelineEvent"}}, }, }, "autonomy": {"type": "object"}, "route_health": {"$ref": "#/schemas/RouteHealthList"}, + "execution_readiness": {"$ref": "#/schemas/ExecutionReadiness"}, + "artifact_sync": {"type": "object"}, "latest_proof": {"type": "object"}, "approvals": {"type": "object"}, "next_actions": {"type": "array", "items": {"type": "string"}}, diff --git a/mesh_state/schema.py b/mesh_state/schema.py index b76927f..54fc7df 100644 --- a/mesh_state/schema.py +++ b/mesh_state/schema.py @@ -320,6 +320,22 @@ created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ); +CREATE TABLE IF NOT EXISTS mesh_app_status_samples ( + id TEXT PRIMARY KEY, + sampled_at TEXT DEFAULT CURRENT_TIMESTAMP, + node_id TEXT NOT NULL, + setup_status TEXT DEFAULT '', + mesh_score INTEGER DEFAULT 0, + known_peer_count INTEGER DEFAULT 0, + route_count INTEGER DEFAULT 0, + healthy_route_count INTEGER DEFAULT 0, + latest_proof_status TEXT DEFAULT '', + execution_ready_targets INTEGER DEFAULT 0, + local_ready_workers INTEGER DEFAULT 0, + artifact_verified_count INTEGER DEFAULT 0, + pending_approvals INTEGER DEFAULT 0, + payload TEXT DEFAULT '{}' +); CREATE INDEX IF NOT EXISTS idx_mesh_events_created ON mesh_events(created_at DESC); CREATE INDEX IF NOT EXISTS idx_mesh_remote_events_peer_created ON mesh_remote_events(peer_id, remote_seq DESC); CREATE INDEX IF NOT EXISTS idx_mesh_leases_peer_status ON mesh_leases(peer_id, status); @@ -339,6 +355,7 @@ CREATE INDEX IF NOT EXISTS idx_mesh_scheduler_decisions_created ON mesh_scheduler_decisions(created_at DESC); CREATE INDEX IF NOT EXISTS idx_mesh_offload_preferences_updated ON mesh_offload_preferences(updated_at DESC); CREATE INDEX IF NOT EXISTS idx_mesh_autonomy_runs_created ON mesh_autonomy_runs(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_mesh_app_status_samples_node_sampled ON mesh_app_status_samples(node_id, sampled_at DESC); """ diff --git a/ocp_startup.py b/ocp_startup.py index 1de29be..cb2720a 100644 --- a/ocp_startup.py +++ b/ocp_startup.py @@ -144,6 +144,16 @@ def operator_app_url(base_url: str, operator_token: str = "", *, path: str = "/a return f"{url}#ocp_operator_token={urllib.parse.quote(token, safe='')}" +def auto_worker_enabled(device_class: str, form_factor: str) -> bool: + device = str(device_class or "").strip().lower() + form = str(form_factor or "").strip().lower() + return device == "full" and form not in {"phone", "watch", "tablet"} + + +def default_worker_id(node_id: str) -> str: + return f"{slugify(node_id or default_node_id()) or 'ocp'}-default-worker" + + def health_url(host: str, port: int) -> str: return build_open_url(host, port, "/mesh/manifest") @@ -325,9 +335,11 @@ def write_json_file(path: Path, payload: dict) -> None: "default_launcher_support_dir", "default_node_id", "default_repo_state_dir", + "default_worker_id", "discover_local_ipv4_addresses", "display_host_for_browser", "ensure_state_paths", + "auto_worker_enabled", "health_url", "is_loopback_host", "is_wildcard_host", diff --git a/scripts/start_ocp_easy.py b/scripts/start_ocp_easy.py index 1d0223d..cb123fa 100755 --- a/scripts/start_ocp_easy.py +++ b/scripts/start_ocp_easy.py @@ -141,7 +141,14 @@ def main() -> int: print("To share OCP with your phone or another laptop:") print(f" OCP_HOST=0.0.0.0 python3 {Path(__file__).name}") - child = subprocess.Popen(command, cwd=str(repo_root)) + env = os.environ.copy() + if ocp_startup.auto_worker_enabled(args.device_class, args.form_factor): + env.setdefault("OCP_AUTO_REGISTER_WORKER", "1") + env.setdefault("OCP_AUTO_WORKER_ID", ocp_startup.default_worker_id(args.node_id)) + print() + print("Execution readiness:") + print(f" default worker: {env['OCP_AUTO_WORKER_ID']}") + child = subprocess.Popen(command, cwd=str(repo_root), env=env) try: if not args.no_open_browser and wait_for_manifest(args.host, args.port, args.open_timeout): webbrowser.open(open_url) diff --git a/server.py b/server.py index 26fc799..05c5fb2 100644 --- a/server.py +++ b/server.py @@ -7,12 +7,14 @@ import argparse import errno import json +import os from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from typing import Any from urllib.parse import parse_qs, urlparse from mesh import SovereignMesh from mesh.sovereign import _normalize_base_url, _preferred_local_base_url +import ocp_startup from runtime import OCPRegistry, OCPStore from server_app import build_app_manifest as _build_app_manifest, build_app_page as _build_app_page from server_app_status import build_app_status as _build_app_status @@ -230,6 +232,21 @@ def _bootstrap_mesh(args) -> SovereignMesh: or None, ) mesh.network_bind_host = args.host + if str(os.environ.get("OCP_AUTO_REGISTER_WORKER") or "").strip().lower() in {"1", "true", "yes", "on"}: + worker_id = ( + os.environ.get("OCP_AUTO_WORKER_ID") + or ocp_startup.default_worker_id(args.node_id or mesh.node_id) + ) + mesh.register_worker( + worker_id=worker_id, + agent_id=args.agent_id, + capabilities=["worker-runtime", "shell", "python"], + resources={"cpu": 1}, + labels=["default", "launcher"], + max_concurrent_jobs=1, + metadata={"source": "ocp_startup", "auto_registered": True}, + status="active", + ) server_context["mesh"] = mesh server_context["runtime"] = {"lattice": lattice, "registry": registry} server_context["ready"] = True diff --git a/server_app.py b/server_app.py index ce3cf32..439703a 100644 --- a/server_app.py +++ b/server_app.py @@ -302,6 +302,41 @@ def build_app_page(mesh: SovereignMesh) -> str: color: #2a3c50; line-height: 1.35; }} + .proof-timeline {{ + margin-top: 14px; + display: grid; + gap: 8px; + }} + .timeline-item {{ + display: grid; + grid-template-columns: auto 1fr; + gap: 10px; + align-items: start; + border: 1px solid var(--line); + border-radius: 16px; + padding: 10px 12px; + background: rgba(255, 255, 255, 0.58); + }} + .timeline-dot {{ + width: 12px; + height: 12px; + margin-top: 4px; + border-radius: 999px; + background: var(--gold); + box-shadow: 0 0 0 5px rgba(168, 108, 36, 0.12); + }} + .timeline-item[data-status="ok"] .timeline-dot, + .timeline-item[data-status="completed"] .timeline-dot {{ + background: var(--green); + box-shadow: 0 0 0 5px rgba(29, 125, 88, 0.12); + }} + .timeline-item[data-status="failed"] .timeline-dot, + .timeline-item[data-status="warning"] .timeline-dot {{ + background: #c84332; + box-shadow: 0 0 0 5px rgba(200, 67, 50, 0.12); + }} + .timeline-item strong {{ display: block; font-size: 0.92rem; }} + .timeline-item span {{ display: block; color: var(--muted); font-size: 0.9rem; line-height: 1.35; }} .phone-link {{ width: 100%; border: 1px solid var(--line); @@ -502,12 +537,15 @@ def build_app_page(mesh: SovereignMesh) -> str:
+ + Inspect App Status
+