Files
ServerKit/backend/app/api/linked_panel.py
T
Juan Denis db3c29179d feat(servers): ServerKit-to-ServerKit peering — embedded agent mode
Lets a panel link itself to a master ServerKit panel as a worker WITHOUT
installing the standalone Go agent: the panel registers via the master's
existing token flow, then runs an embedded agent client (HMAC auth + HTTP
long-poll transport) that dispatches agent commands (file:write,
docker:compose:up/ps/logs, system:info/metrics) onto its own local
services. The master needs zero changes — a linked panel is just another
agent-backed Server row with scoped, revocable credentials.

- linked_panel_config table (migration 077) + Fernet-encrypted secret
- embedded agent client: connect/poll/result loop, heartbeat metrics,
  per-command threads, allowed-roots guard on file:write
- API: GET/POST/DELETE /api/v1/linked-panel (+ startup resume hook)
- Servers page: LinkedPanelCard (link form, live status, unlink)
2026-07-22 00:35:33 -04:00

46 lines
1.5 KiB
Python

"""Linked-panel API — manage this panel's link to a master ServerKit panel."""
from flask import Blueprint, jsonify, request
from flask_jwt_extended import jwt_required
from app.middleware.rbac import developer_required
from app.services.linked_panel_service import LinkedPanelService
linked_panel_bp = Blueprint('linked_panel', __name__)
@linked_panel_bp.route('', methods=['GET'])
@jwt_required()
def get_status():
"""Current link status (linked/connected/last_error)."""
return jsonify(LinkedPanelService.get_status())
@linked_panel_bp.route('', methods=['POST'])
@jwt_required()
@developer_required
def link():
"""Link this panel to a master ServerKit panel.
Body: {"master_url": "https://panel-a", "registration_token": "sk_reg_…",
"name": "optional display name"}
The registration token comes from the master's Servers page (Add Server
/ regenerate-token) — the same token the Go agent consumes.
"""
data = request.get_json(silent=True) or {}
result = LinkedPanelService.link(
master_url=data.get('master_url'),
registration_token=data.get('registration_token'),
name=data.get('name'),
)
if not result.get('success'):
return jsonify(result), 400
return jsonify(result)
@linked_panel_bp.route('', methods=['DELETE'])
@jwt_required()
@developer_required
def unlink():
"""Stop the embedded agent and forget the master credentials."""
return jsonify(LinkedPanelService.unlink())