Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ The CI definition is `.github/workflows/maven.yml`. The shaded runnable artifact
inspection gating on `VoteLogging.Enabled`, document that a restart is required after either toggle, and do not claim
enabled/available/readable are interchangeable states. Serialize APPLY operations and retries that share a target so
retained creation order is also successful completion order for restart-session warnings.
14. `plugin.deploy.v1` stages only a verified VotingPlugin JAR for the next process restart. Keep uploads and expanded ZIP
content bounded, content-addressed, private, symlink-safe, and atomically published. A node download requires its exact
live session, attempt, and unexpired lease; each node independently verifies the hash and plugin identity. Never hot
reload, automatically restart, or include older nodes that did not negotiate the exact capability. A Control restart
invalidates in-progress download authority and requires an explicit retry.

## Paired protocol workflow

Expand Down
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

VotingPlugin Control is a separate, local-first administration service for a VotingPlugin network. It provides
authenticated discovery of multiple BungeeCord and Velocity proxies, direct Bukkit backend enrollment, full VotingPlugin
YAML configuration control, guided setup, redacted snapshots/drift comparison, durable operation history, and typed
read-only vote/data diagnostics. The local WebUI uses the same versioned API. Control does not process votes, and
YAML configuration control, guided setup, redacted snapshots/drift comparison, durable operation history, verified
next-restart VotingPlugin JAR staging, and typed read-only vote/data diagnostics. The local WebUI uses the same versioned API. Control does not process votes, and
VotingPlugin does not depend on it for startup, joins, routing, rewards, or shutdown.

Maintainers and coding agents should read [AGENTS.md](AGENTS.md). The complete management, inspection, limits, and threat
Expand Down Expand Up @@ -55,8 +55,10 @@ report `development` rather than maintaining a second version literal.
| `CONTROL_LAUNCH_ID` | empty | Optional UUID echoed by health checks so a supervising VotingPlugin can verify ownership of the listener |
| `CONTROL_PARENT_PID` | empty | Optional supervising VotingPlugin process ID; hosted Control exits when that parent process ends |

The server also uses a bounded HTTP executor (8 active requests and a 32-request queue), a 4 MiB request limit, bounded
JSON depth/string/number sizes, a two-worker password-verification executor with per-client admission, and a bounded invalid-authentication failure limit. Configure the exact reverse-proxy IP addresses when HTTPS terminates upstream; forwarding headers from every other peer are ignored. Valid enrolled/admin credentials remain
The server also uses a bounded HTTP executor (8 active requests and a 32-request queue), a 4 MiB limit for generic JSON
requests, and a separate bounded streaming artifact-upload route with a 64 MiB maximum. JSON depth/string/number sizes
are bounded, password verification uses two workers with per-client admission, and invalid-authentication failures are
bounded. Configure the exact reverse-proxy IP addresses when HTTPS terminates upstream; forwarding headers from every other peer are ignored. Valid enrolled/admin credentials remain
usable even while invalid traffic is throttled. Shutdown stops the server and its daemon request workers without waiting
indefinitely.

Expand Down Expand Up @@ -158,6 +160,13 @@ All errors have the stable form:
| `POST` | `/api/v1/nodes/{nodeId}/inspections/{inspectionId}/result` | matching node | Complete that inspection attempt |
| `GET`, `POST` | `/api/v1/snapshots` | admin or WebUI session; CSRF for POST | List summaries or save a named snapshot from a completed file read |
| `GET` | `/api/v1/snapshots/{snapshotId}` | admin or WebUI session | Load one durable snapshot's full redacted file content |
| `POST` | `/api/v1/artifacts/votingplugin` | admin or WebUI session + CSRF | Stream and verify one bounded VotingPlugin JAR into private content-addressed storage |
| `GET`, `POST` | `/api/v1/deployments` | admin or WebUI session; CSRF for POST | List deployment history or stage a verified artifact on explicit `plugin.deploy.v1` nodes |
| `GET` | `/api/v1/deployments/{deploymentId}` | admin or WebUI session | Read durable per-node staging state |
| `POST` | `/api/v1/deployments/{deploymentId}/retry` | admin or WebUI session + CSRF | Retry only failed, currently eligible nodes as a new operation |
| `POST` | `/api/v1/nodes/{nodeId}/deployments` | matching node | Claim one session-pinned deployment task, or `204` |
| `GET` | `/api/v1/nodes/{nodeId}/deployments/{deploymentId}/artifact` | matching node + exact session/attempt headers | Download the claimed verified JAR during its lease |
| `POST` | `/api/v1/nodes/{nodeId}/deployments/{deploymentId}/result` | matching node | Complete the exact staging attempt |

Routes are exact. Child suffixes do not inherit a handler, every known endpoint has an intentional method/structured 405,
and all unknown endpoints return a structured 404.
Expand Down Expand Up @@ -197,9 +206,16 @@ Control and VotingPlugin both enforce fixed quick-setup preset/option schemas; u
than becoming arbitrary YAML writes. The WebUI settings catalog is a static versioned reference over these typed paths,
not a generic setting API.

Read actions load only the primary server shown in the configuration header. Preview and apply still cover every server
Opening Settings or changing the selected server automatically reads that server's current configuration. A failed read
clears the editor and exposes an inline retry; successful applies invalidate cached values and read the confirmed state
again. Read actions load only the primary server shown in the configuration header. Preview and apply still cover every server
explicitly included in configuration changes, so one slow secondary node does not delay opening the editor or guided form.

`plugin.deploy.v1` is additive and exact: older nodes remain connected but are excluded from JAR staging. The WebUI uploads
at most 64 MiB, Control validates the ZIP structure and root `plugin.yml`, and every node re-verifies the SHA-256 before
staging. Staging never reloads or restarts a server; success is reported as `RESTART_REQUIRED`. Interrupted Control attempts
become failed durable history and require an explicit retry, preventing a pre-restart lease from authorizing a download.
Comment thread
BenCodez marked this conversation as resolved.

`data.inspect.v1` is a separate read-only lane for overview, vote-site health (including persisted unconfigured service
observations), exact-player data, bounded VoteLog summary/search/correlation trace, non-creating service-site resolution,
no-side-effect reward simulation, and redacted diagnostics. It accepts only allow-listed string filters and bounded result
Expand Down
23 changes: 19 additions & 4 deletions docs/control-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ when Control is stopped.

## Mental model

There are three independent lanes:
There are four independent lanes:

| Lane | Capability examples | Direction | Can mutate a node? | Persistence |
| --- | --- | --- | --- | --- |
| Discovery | `discovery.read`, `presence.snapshot` | Node pushes registration/heartbeat/presence | No | Current topology is in memory |
| Configuration | `config.files.v1`, `config.quick-setup.v1` | Browser queues; node polls and reports | Only after preview and approval | Redacted history and audit are durable; live task input is in memory |
| Inspection | `data.inspect.v1` | Browser queues; Bukkit node polls and reports | Never | Short-lived result is in memory; kind-only audit is durable |
| Deployment | `plugin.deploy.v1` | Browser queues; node polls and downloads under its exact session and attempt lease | Stages a verified JAR only; never reloads or restarts | Artifacts and deployment history are durable |

Connectors always initiate outbound HTTP(S) to Control. No Control feature adds an inbound port to a Minecraft process.
One node credential is bound to one stable node ID. Browser sessions and the API automation credential are separate from
Expand Down Expand Up @@ -47,6 +48,7 @@ Control accepts only the intersection with its own allow-list.
| `config.transport-test.v1` | Typed, bounded proxy-to-backend communication check |
| `config.proxy-method.v1` | Coordinated preview/apply and acknowledged runtime replacement for a supported network proxy method |
| `config.proxy-method.v2` | HTTP proxy-method selection and HTTP backend quick setup; only paired connectors implementing the HTTP contract advertise it |
| `plugin.deploy.v1` | Verify and stage a VotingPlugin JAR for the node's next process restart; never hot reload or restart it |
Comment thread
BenCodez marked this conversation as resolved.
| `data.inspect.v1` | Typed read-only data, health, simulation, and diagnostics requests |

Do not infer support from plugin version strings. Check `acceptedCapabilities` for the exact capability.
Expand All @@ -64,7 +66,8 @@ authenticated, CSRF-protected endpoint and node capability checks as an external
| Overview dashboard | Builds six summary cards, Attention Required, quick actions, topology, logged-service activity, and recent operation activity from the node registry plus existing `overview`, `vote-site-health`, and `vote-log-summary` inspections | Read-only aggregation; failed, incomplete, or malformed sub-inspections produce a warning rather than a Healthy claim; disconnected registered nodes produce a warning; unknown Minecraft presence is not treated as offline; VoteLog counts are labeled as logged events; actions only open existing safe workflows rather than inventing auto-fixes |
| Network Doctor | Runs `diagnostics` (which includes the overview fields), combines node health with Control's current topology, and displays checks for connector, configuration, Votifier, vote sites, rewards, logging, and proxy topology | Read-only; “healthy” is bounded reported state, not a synthetic vote |
| Diagnostics download | Downloads the last Network Doctor result as local JSON | Redacted status bundle only; no raw configuration/logs/player records/infrastructure secrets |
| Activity | Loads the newest 50 live/recovered operation views, labels phases, lineage, reload/rollback, resumes eligible guided preview approvals, and offers retry only when `retryable` | Recovered history cannot be retried; approval is single-use and apply is CSRF-protected; proxy-method apply needs a new preview |
| Activity | Loads the newest 50 live/recovered configuration operation views, labels phases, lineage, reload/rollback, resumes eligible guided preview approvals, and offers retry only when `retryable` | Recovered configuration history cannot be retried; approval is single-use and apply is CSRF-protected; proxy-method apply needs a new preview |
| Plugin update | Uploads one bounded JAR, shows the deployment-capable subset, and stages it on those nodes | SHA-256 and JAR identity are verified; session/attempt leases authorize downloads; Control never automatically reloads or restarts nodes; private storage is capped at 32 artifacts / 512 MiB and evicts only artifacts not referenced by retained deployment history |
| Fast file reads | Caches a successful file read for 30 seconds by node ID, node session, and file | Browser memory only; cleared on logout and successful relevant writes; session binding prevents reuse after reconnect |
| Full-YAML drafts | Keeps unsaved editor contents during a registry refresh | A dirty draft is bound to its source node, session, and file; it cannot preview or apply after that session changes. The operator must explicitly confirm a current-file read/reload, which discards the retained draft and rebinds the editor. |
| Proxy configuration | Opens `bungeeconfig.yml` only for the selected online proxy that negotiated `config.proxy-files.v1` | Fixed one-file capability, never proxy file browsing; redacted READ, PREVIEW, and one-time approved APPLY still apply |
Expand All @@ -81,7 +84,11 @@ authenticated, CSRF-protected endpoint and node capability checks as an external

The Setup tab replaces the former “Quick Setup” framing but retains existing typed presets, VoteSites sync, detected-plugin
command suggestions, preview, approval, node backup, reload, and rollback. Setup profiles are convenience input only; live
values should be loaded before modifying an existing configuration.
values should be loaded before modifying an existing configuration. The normal Settings flow performs that read
automatically on entry and whenever the selected node or file changes. It clears the prior node's editable state before the
request, deduplicates navigation-triggered reads, caches successful reads briefly by node session and file, and
invalidates/re-reads after a successful apply. Failure leaves no stale editable value and exposes an inline Retry action.
None of these reads can trigger APPLY.

## Configuration operations

Expand Down Expand Up @@ -450,6 +457,13 @@ node resources require the bearer credential bound to the path node ID.
| `POST` | `/api/v1/operations/{operationId}/retry` | admin/browser + CSRF | Reissue safe failed work as a new operation |
| `POST` | `/api/v1/nodes/{nodeId}/operations` | matching node | Claim one configuration task or `204` |
| `POST` | `/api/v1/nodes/{nodeId}/operations/{operationId}/result` | matching node | Complete one claimed configuration task |
| `POST` | `/api/v1/artifacts/votingplugin` | admin/browser + CSRF | Stream, hash, inspect, and atomically retain one VotingPlugin JAR (64 MiB maximum) |
| `GET`, `POST` | `/api/v1/deployments` | admin/browser; CSRF for POST | List durable staging operations or target an exact verified artifact |
| `GET` | `/api/v1/deployments/{deploymentId}` | admin/browser | Read per-node staging state |
| `POST` | `/api/v1/deployments/{deploymentId}/retry` | admin/browser + CSRF | Create a new operation for currently eligible failed targets only |
| `POST` | `/api/v1/nodes/{nodeId}/deployments` | matching node | Claim one session-pinned deployment task |
| `GET` | `/api/v1/nodes/{nodeId}/deployments/{deploymentId}/artifact` | matching node + session/attempt headers | Stream the verified artifact during the exact live lease |
| `POST` | `/api/v1/nodes/{nodeId}/deployments/{deploymentId}/result` | matching node | Complete the exact attempt with staged/restart-required or a bounded failure |
| `POST` | `/api/v1/inspections` | admin/browser + CSRF | Queue one typed read-only query |
| `GET` | `/api/v1/inspections/{inspectionId}` | admin/browser | Read short-lived inspection status/result |
| `POST` | `/api/v1/nodes/{nodeId}/inspections` | matching node | Claim one inspection or `204` |
Expand All @@ -471,7 +485,8 @@ tunnel/network outside loopback.

| Boundary | Limit/behavior |
| --- | --- |
| HTTP request | 4 MiB; bounded Jackson depth/string/number constraints; duplicate and trailing JSON rejected |
| Generic JSON request | 4 MiB; bounded Jackson depth/string/number constraints; duplicate and trailing JSON rejected |
| VotingPlugin artifact upload | Separate bounded streaming route; 64 MiB maximum |
| HTTP execution | 8 active request workers plus queue of 32; bounded request/response time |
| Browser sessions | 100; 30-minute idle and 8-hour absolute expiry; HttpOnly, SameSite=Strict cookie |
| Node operation targets | 1–100 distinct online capable nodes |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.bencodez.votingplugin.control;

import com.bencodez.votingplugin.control.auth.CredentialStore;
import com.bencodez.votingplugin.control.artifact.ArtifactStore;
import com.bencodez.votingplugin.control.domain.InMemoryNodeRegistry;
import com.bencodez.votingplugin.control.domain.ConfigurationAuditLog;
import com.bencodez.votingplugin.control.domain.ConfigurationOperations;
import com.bencodez.votingplugin.control.domain.ConfigurationOperationJournal;
import com.bencodez.votingplugin.control.domain.ConfigurationSnapshots;
import com.bencodez.votingplugin.control.domain.InspectionOperations;
import com.bencodez.votingplugin.control.domain.DeploymentOperations;
import com.bencodez.votingplugin.control.http.ControlHttpServer;
import com.bencodez.votingplugin.control.protocol.ControlIdentity;
import com.bencodez.votingplugin.control.protocol.Protocol;
Expand Down Expand Up @@ -162,9 +164,12 @@ static void runServer(Map<String, String> environment) throws Exception {
ConfigurationOperations operations = new ConfigurationOperations(registry, audit, clock, operationJournal);
InspectionOperations inspections = new InspectionOperations(registry, audit, clock);
ConfigurationSnapshots snapshots = new ConfigurationSnapshots(configuration.dataDirectory(), clock);
ArtifactStore artifacts = new ArtifactStore(configuration.dataDirectory().resolve("plugin-artifacts"));
DeploymentOperations deployments = new DeploymentOperations(registry, audit,
configuration.dataDirectory(), clock);
ControlHttpServer server = new ControlHttpServer(configuration.address(), registry, identity, credentials,
operations, inspections, snapshots, configuration.secureCookies(), configuration.trustedProxyAddresses(),
configuration.launchId());
operations, inspections, snapshots, artifacts, deployments, configuration.secureCookies(),
configuration.trustedProxyAddresses(), configuration.launchId());
ProcessHandle parent = parentProcess(configuration.parentPid());
CountDownLatch shutdown = new CountDownLatch(1);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
Expand Down
Loading