Skip to content

Latest commit

 

History

History
513 lines (423 loc) · 35.4 KB

File metadata and controls

513 lines (423 loc) · 35.4 KB

VotingPlugin Control agent contract

This is the compact source of truth for an AI agent or Control client implementing the Bukkit integration. The connector has three separate lanes:

  • configuration operations use the negotiated config.files.v1 / config.quick-setup.v1 contracts; Vote Party proposals that include enabled additionally require config.quick-setup.v2, and configuration operations may write only managed VotingPlugin YAML after preview and approval;
  • inspections use the optional data.inspect.v1 contract and are always read-only.
  • plugin deployment uses the optional plugin.deploy.v1 contract and only stages a verified JAR for an explicit administrator-selected restart; it never reloads or restarts the process.

Do not translate an inspection request into a configuration operation. Do not add raw SQL, arbitrary commands, player enumeration, database browsing, filesystem paths, or generic key/value reads to either contract.

Backend automatic enrollment

A backend without a completed credential sends ControlEnrollmentRequest through its configured proxy transport. The request identifies nodeId/server, requestId, the exact Control endpoint, an optional SHA-256 credential verifier, an optional proxy challenge, and an optional authenticator. The proxy replies with ControlEnrollmentResult, echoing the node and request IDs and carrying either a single-use challenge or the final success result.

Plugin messaging binds the request to the proxy-observed source server. HTTP uses its authenticated backend transport identity. Redis, MQTT, sockets, and MySQL have no authoritative per-message source identity, so both the initial request and challenge response require an HMAC-SHA256 transcript authenticator derived from the deployment's shared secretkey.key. The signed transcript includes node ID, request ID, endpoint, verifier, and challenge. A proxy rejects a missing or invalid signature without consuming the pending challenge. These transports therefore require the same key on the proxy and backend; otherwise automatic enrollment remains unavailable and the administrator must install the backend credential manually. Raw credentials and key material never appear in the envelope.

The proxy first proves that the requested endpoint is its hosted Control instance. Challenges expire after one minute and are single use. The backend retains its pending credential until the proxy reports verifier installation and the backend connector successfully authenticates to that exact endpoint. Enrollment retries may repeat the same request ID, verifier, and valid challenge. Non-plugin-message sends run on a dedicated daemon worker; plugin messaging alone is sent from the Bukkit scheduler because its transport requires the server-owned channel.

Verified plugin deployment (plugin.deploy.v1)

The capability is advertised only when the node can identify a safe staging target. Control assigns work through POST /api/v1/nodes/{nodeId}/deployments with the current sessionId. A task contains deploymentId, artifactId, lowercase sha256, byte size, and a leased attemptId. The node downloads only that assignment from GET /api/v1/nodes/{nodeId}/deployments/{deploymentId}/artifact, authenticating with its enrolled credential and the exact X-Node-Session and X-Deployment-Attempt headers.

Downloads are limited to 64 MiB, do not follow redirects, require the assigned length and SHA-256, and must be a bounded JAR with exactly one root plugin.yml declaring VotingPlugin. Staging rejects unsafe ZIP entries, symlinked targets, oversized expansion, and compression bombs. Bukkit nodes atomically publish VotingPlugin.jar to the server update folder. Proxy nodes first refresh a durable .control-backup from the current verified JAR, then atomically replace the same discovered plugin JAR. Both write a small idempotency marker only after activation; a marker is trusted only after rehashing and reinspecting the target.

The node reports the result to POST /api/v1/nodes/{nodeId}/deployments/{deploymentId}/result with its sessionId and the leased attemptId. Success is RESTART_REQUIRED; it means the JAR was staged, not loaded. Failures distinguish download, size/hash, invalid artifact, staging/write, cancellation, and lost capability. Transfer and disk work run on a dedicated background worker. Disable/reload marks the worker inactive, interrupts it, and prevents a stale completion from submitting success. Mixed-version nodes that do not negotiate plugin.deploy.v1 remain connected but are excluded from deployment targets.

Named reward files (config.reward-files.v1)

This additive Bukkit capability manages only existing Rewards/<name>.yml files directly under VotingPlugin's Rewards directory. <name> starts with an ASCII letter or digit and then contains at most 99 ASCII letters, digits, _, or -. Paths, .yaml, symlinks, case-only duplicate names, files over 512 KiB, and inventories over 100 eligible files are rejected. A missing Rewards directory yields an empty inventory. Other configuration and inspection capabilities remain independent, so an older peer simply leaves named reward files unavailable. The connector advertises this optional capability only when the local filesystem supports secure directory handles and private POSIX staging files. An unsuccessful private-staging probe is retried on a later registration or heartbeat; only a successful probe is cached for the current Rewards directory identity.

Inventory is the read-only reward-file-inventory inspection with empty filters and a names-only result.files array. It requires both data.inspect.v1 and config.reward-files.v1 to be accepted. A named file READ/PREVIEW/APPLY uses the existing configuration operation envelope with domain:"file" and fileName:"Rewards/<name>.yml"; the new capability must be accepted before the connector dispatches it. PREVIEW does not write. APPLY requires the approved exact revision, serializes competing applies on the node, stages a private file under the pinned Rewards directory handle, and forces every published directory entry through that pinned handle. A forceable pinned directory channel is required before advertising config.reward-files.v1. The connector retains a .control-backup, reloads VotingPlugin, verifies the named reward became active against the pinned proposal, and attempts local rollback and reload if that step fails. Directory identity is rechecked on the server owner thread immediately before and after plugin reload. A replacement causes apply failure; a restore to the pinned directory is not reported as an active rollback when the live Rewards path points elsewhere. Plugin reload still reads files by path, so Control cannot make a concurrent local filesystem actor's mutations atomic. Read results and inventory remain bounded; inventory rejects more than 1,024 total Rewards directory entries, including ignored entries, and redacts configuration secrets. This capability does not create or delete reward files or grant access outside Rewards.

Recovery of a pending named-file result checks that exact file and bounded case-only aliases, without validating unrelated Rewards entries. A missing, case-ambiguous, or permanently unsafe target (symlink, directory, FIFO, or oversized file) cannot be confirmed as a successful apply and terminates that pending intent; transient I/O failures remain retryable. Unrelated invalid entries do not stall the whole configuration lane. Recovery compares the raw revision before parsing YAML, so a later malformed edit with a different revision aborts that pending intent without blocking new work. Inventory accepts regular files; subsequent named-file reads require a writable, seekable handle. A raced FIFO or other non-seekable replacement is rejected before body reading rather than blocking the single connector worker on supported Unix providers.

Proxy file contract (config.proxy-files.v1)

This is a proxy-only capability, advertised by an enrolled BungeeCord or Velocity node. It is separate from config.proxy-routing.v1, config.proxy-method.v1, and the backend config.files.v1 capability. The only managed file is the proxy's top-level bungeeconfig.yml in the VotingPlugin data folder. fileName must be exactly bungeeconfig.yml; paths, subdirectories, and other filenames are rejected. The file and proposed UTF-8 content are bounded at 512 KiB. YAML must be a mapping, use string keys and supported scalar/map/list values, and may not contain aliases/merge keys, duplicate keys, invalid UTF-8, NULs, or nesting deeper than 50.

The operation is carried in the normal authenticated node operation queue. Control claims an operation with POST /api/v1/nodes/{nodeId}/operations and {"sessionId":"<connector-session-uuid>"}. A claimed task has operationId, attemptId, type, and, for APPLY, expectedRevision, plus this configuration object:

{
  "domain": "file",
  "fileName": "bungeeconfig.yml",
  "content": "...masked YAML..."
}

content is omitted for READ. The node submits the result through the normal operation-result endpoint; a result has success, code, message, revision (on success), configuration (on success), changes, reloaded, and rolledBack, and includes the claimed attemptId. A successful configuration object contains domain, fileName, and masked content. changes is a deterministic, lexicographically ordered list of at most 20 flattened YAML paths, using added, changed, or removed prefixes. The complete operation-result request and connector HTTP response retain the shared 4 MiB protocol bound; the stricter 512 KiB limit applies to the managed YAML content itself.

The node posts that result to POST /api/v1/nodes/{nodeId}/operations/{operationId}/result with the same sessionId; there is no second proxy-file-specific envelope. attemptId is retained in the result so Control can match the leased attempt. Control's HTTP success response acknowledges receipt; a 409 TASK_LEASE_EXPIRED leaves the result journaled for recovery/retry, while 404 OPERATION_NOT_FOUND is treated as an acknowledgement because the Control-side operation is already gone. Malformed or non-success transport responses affect only this connector queue and are retried with its normal bounded backoff.

READ returns the current masked document and its SHA-256 revision. PREVIEW parses and validates the proposal, resolves unchanged redacted secret markers against the local document, returns the current revision and changes, and does not write. APPLY requires the exact revision returned by the preview (and checks it again around staging/installation), writes through a staged file and atomic activation, and retains bungeeconfig.yml.control-backup. A stale or changed revision returns success:false, code:"STALE_REVISION", with no configuration payload. Invalid file names, YAML, content, or redaction use VALIDATION_ERROR; unavailable/read or save failures use the fixed APPLY_FAILED result; an installation failure returns APPLY_FAILED and reports whether rollback succeeded in rolledBack.

Proxy-file APPLY does not reload the proxy in this connector (reloaded:false); the success message instructs an operator to restart the proxy for general settings to take effect. It is not the proxy-method runtime-replacement operation. A failed installation attempts to restore the backup atomically. The result is journaled by operation ID until Control acknowledges it, so a leased retry does not apply the change twice; on node recovery an unfinished APPLY is either recognized as already installed by revision or reported as RECOVERY_ABORTED.

Control must authenticate as the enrolled node and must include config.proxy-files.v1 in acceptedCapabilities before assigning these tasks. If the capability was not negotiated, the node returns UNSUPPORTED without reading or writing the file. Proxy and backend nodes may be enrolled against the same Control instance, but each node has its own identity, credential/session, operation lease, revision, and result journal: a backend capability does not authorize a proxy-file task, and one peer's approval or revision cannot be used for another peer. Control should therefore present this editor only for a capable proxy node and require its normal authenticated admin preview/approval flow.

All reads mask secrets. The mask covers password/secret/token/API-key/authorization/webhook URL fields and selected database, Redis, MQTT, proxy-host, and Control infrastructure fields; JDBC-style and credential-bearing URLs are also masked. A submitted redaction marker preserves the local secret; replacement secrets may be submitted in an authenticated operation but are never returned or journaled.

Easy automatic vote-site toggle

The auto-create-vote-sites quick-setup preset owns exactly one setting:

{"domain":"quick-setup","preset":"auto-create-vote-sites","options":{"enabled":"false"}}

READ uses an empty options object and returns options.enabled; unknown READ options are rejected. PREVIEW and APPLY use the normal revision/approval workflow. The preset changes only Config.yml → AutoCreateVoteSites. Use it for the prominent “Automatically create unknown vote sites” switch instead of submitting every field in common-settings. The setting gates only automatic creation from an inbound unknown service. An administrator's explicit /av VoteSite <site> Create or admin-GUI creation remains available when the toggle is off. vote-site-health still reports a bounded detectedUnconfiguredServices list from persisted GottenServiceSites observations, so turning automatic creation off does not hide new service names. This is read-only discovery, not an approve/create action.

Vote-logging setup

The vote-logging quick-setup preset owns exactly three non-secret settings:

{
  "domain": "quick-setup",
  "preset": "vote-logging",
  "options": {"enabled":"true", "purgeDays":"30", "useMainMySQL":"true"}
}

purgeDays is exactly -1 (disable automatic purge) or an integer from 1 through 3650; 0 and other negative values are invalid, and READ round-trips -1. The preset rejects unknown options and never reads/writes a hostname, database name, username, password, or other connection field. Choosing a dedicated connection therefore remains a full redacted-editor task; turning useMainMySQL off alone does not invent credentials. READ accepts no options and rejects unknown fields. READ returns current typed state and its revision, PREVIEW computes changes and produces an approval, and only the approved APPLY writes atomically, reloads, and rolls back on reload failure, like every other quick setup.

Verified plugin staging (plugin.deploy.v1)

This optional capability stages one verified VotingPlugin JAR for the node's next process restart. It is never a hot reload and never restarts a proxy or backend automatically. A node advertises it only when all of the following are true:

  • the connector is the currently enabled Control route, not a recovery-only connector draining an older durable result;
  • a safe local staging target was prepared;
  • the Control endpoint uses HTTPS, literal loopback HTTP, or proven direct same-node HTTP; otherwise, a non-loopback literal private-network/link-local HTTP endpoint requires the node to explicitly set Control.AllowInsecureHttpPluginDeployment: true (missing defaults to false).

Normal private-network HTTP Control communication remains supported without that opt-in. Only executable plugin.deploy.v1 preparation, advertising, and polling are disabled by default on those routes. Public HTTP and arbitrary HTTP hostnames remain prohibited even with the opt-in; localhost still requires proven direct local hosting. An opted-in connector emits a startup warning: node credentials, deployment metadata, and executable artifacts cross unauthenticated plaintext HTTP. SHA-256 checks detect mismatched bytes, but cannot authenticate the source when an attacker can substitute both the task digest and matching artifact. HTTPS is recommended. The separate credential-endpoint policy remains HTTPS or proven same-node loopback HTTP; this opt-in does not relax that contract.

Control leases deployment work separately from configuration operations:

POST /api/v1/nodes/{nodeId}/deployments
Content-Type: application/json

{"sessionId":"<connector-session-uuid>"}

A 204 means no work. A 200 task contains deploymentId, artifactId, lowercase SHA-256 sha256, byte size, and attemptId. Artifact size is limited to 64 MiB. The node downloads the exact leased artifact with:

GET /api/v1/nodes/{nodeId}/deployments/{deploymentId}/artifact
Authorization: Bearer <node credential>
X-Node-Session: <connector-session-uuid>
X-Deployment-Attempt: <attemptId>

Control verifies the uploaded artifact before leasing it. The node independently re-verifies the exact size and SHA-256, bounded ZIP/JAR structure, root plugin.yml, and name: VotingPlugin before publication. Bukkit nodes stage to the server update folder; proxy nodes atomically replace their discovered plugin JAR only after creating a durable .control-backup. A small durable .control-deployment marker is published before the verified target is moved into place, so a lost result acknowledgement does not apply the same artifact twice. On Bukkit, a consumed staged update must match the installed JAR. On proxies, both a safe backup and a target JAR matching the marker's digest are required for acknowledgement; a missing marker requires a fresh verified download and staging attempt.

The result is posted to:

POST /api/v1/nodes/{nodeId}/deployments/{deploymentId}/result
Content-Type: application/json

{
  "sessionId":"<connector-session-uuid>",
  "attemptId":"<same attempt id>",
  "success":true,
  "code":"RESTART_REQUIRED",
  "message":"Plugin update staged; restart is required"
}

Failure codes include CANCELLED, DOWNLOAD_FAILED, SIZE_MISMATCH, HASH_MISMATCH, INVALID_ARTIFACT, STAGING_FAILED, DEPLOYMENT_FAILED, and INSECURE_ENDPOINT. Deployment polling is serialized with the connector lifecycle, uses the normal bounded failure backoff, and is cancelled during shutdown before transport teardown.

Inspection transport

An enrolled Bukkit node advertises data.inspect.v1. Once Control includes it in acceptedCapabilities, the node polls:

POST /api/v1/nodes/{nodeId}/inspections
Content-Type: application/json

{"sessionId":"<connector-session-uuid>"}

A 204 means no work. A 200 assignment is:

{
  "inspectionId": "<uuid>",
  "attemptId": "<opaque attempt id>",
  "query": {"kind":"overview","filters":{}}
}

The node posts the result to /api/v1/nodes/{nodeId}/inspections/{inspectionId}/result:

{
  "sessionId": "<connector-session-uuid>",
  "attemptId": "<same attempt id>",
  "success": true,
  "code": "OK",
  "message": "Inspection completed",
  "data": {
    "schemaVersion": 1,
    "kind": "overview",
    "generatedAt": "2026-08-30T12:00:00Z",
    "result": {}
  }
}

For compatibility, a successful result may use "code":"OK" as above or omit/set code to null. It must contain a JSON object whose schemaVersion is the JSON integer 1 (not a string), kind exactly matches the assigned query, generatedAt parses as an ISO-8601 instant, and result is a JSON object. Failures omit data and use VALIDATION_ERROR, UNAVAILABLE, RESULT_TOO_LARGE, or INSPECTION_FAILED.

Data is limited to 512 KiB. General rows and diagnostics plugin inventories are limited to 100, top lists to 20, and lookback windows to 365 days. Summary top lists order vote counts descending, then names case-insensitively and by exact spelling; the database applies the same name tie-break before its limit. The connector performs inspections on a dedicated single-thread daemon executor, separate from presence and configuration work and never on the Bukkit primary thread. Database reads therefore cannot block vote handling, server ticks, or configuration polling; one long query serializes only later inspections. Shutdown cancels this lane and waits at most five seconds for its worker. Inspections have no write-ahead journal because retrying a read is safe. A persistent missing route, transport failure, server error, or malformed response backs this polling lane off exponentially from one second to a five-minute cap; a successful poll or newly accepted capability resets the delay.

Query allow-list

Unknown kinds and unknown filter fields are rejected. The versioned filters contract is a string-to-string object; parse each allowed value strictly after selecting the kind. Numeric and boolean examples are "days":"30", "limit":"25", and "includeDisabled":"false". Control's current JSON mapper may coerce some scalar admin-request values before it creates the task, but clients must not rely on that implementation detail: connector assignments contain strings and the VotingPlugin handler requires text. Ordinary filter values are limited to 500 UTF-8 bytes at Control. Only reward-simulation's encoded proposal may be larger, with a 64 KiB hard limit.

Kind Allowed filters Meaning
overview none Versions, configuration health, bounded data-storage mode, proxy mode, vote-site counts, and configured/readable VoteLog state
vote-site-health string days (1–365, default 30) Configured site status, bounded aggregate last-vote/count data, unmatched logged services, and persisted unconfigured service observations
player exactly one of name (1–16 characters) or uuid (canonical 36-character UUID) Exact existing-player lookup; totals, points, streaks, up to 100 per-site last-vote rows, and pending vote count saturated at 100,000; never lists players
vote-log-summary string days (1–365, default 30) Vote totals, immediate/cached split, unique voters, and bounded top-service (service, canonical count) and top-server (server, canonical count) rows; schema-v1 nodes also emit the equal legacy votes alias for staggered dashboard upgrades
vote-log-search at most one of exact player (1–16 characters), service (1–64), or server (1–64); optional event and string days/limit Bounded recent event rows; limit is 1–100 and defaults to 25
vote-trace required canonical 36-character UUID voteId; optional string days/limit Chronological VoteLog events sharing one correlation ID
vote-site-resolution required valid serviceSite (1–64 characters); optional string boolean includeDisabled Dry-runs existing resolution and reports whether auto-create would be attempted; never calls the creating resolver
reward-simulation required proposal, a JSON object encoded as one filter string Validates and normalizes typed actions, reports the plan, and never invokes RewardBuilder
reward-file-inventory none; also requires config.reward-files.v1 Lists at most 100 approved named Rewards/*.yml basenames without reading their contents
diagnostics none Bounded redacted environment/configuration status, configured/readable VoteLog state, and up to 100 detected plugin names with an explicit truncation indicator

Valid VoteLog event values are VOTE_RECEIVED, VOTEMILESTONE, VOTE_STREAK_REWARD, TOP_VOTER_REWARD, and VOTESHOP_PURCHASE. Search values are exact, not substrings. VoteLog summary/search/trace return UNAVAILABLE when the optional table is disabled, its adapter is absent, or its bounded readability probe fails.

Reward proposal

After parsing the filters.proposal string as JSON, the typed object accepts only:

{
  "scope": "site",
  "site": "PMC",
  "commands": ["eco give %player% 100"],
  "playerMessages": ["Thanks for voting"],
  "broadcastMessages": [],
  "items": [{"material":"DIAMOND","amount":2}],
  "money": 0,
  "permissions": [],
  "chancePercent": 100,
  "onlineOnly": false
}

For example, a client produces the request with the equivalent of:

const query = {
  kind: 'reward-simulation',
  filters: {proposal: JSON.stringify(proposal)}
};

Use a standard JSON serializer. Never construct the escaped proposal by concatenating user-controlled strings.

scope is site, every-site, or vote-party. For site, site must match [A-Za-z0-9_-]{1,64} and already exist in VoteSites.yml; for a global scope it must be omitted, null, or empty. Unknown proposal and item fields are invalid. The remaining typed limits are:

Field Contract
commands, playerMessages, broadcastMessages Optional arrays of at most 20 nonblank, single-line strings; each string is at most 500 characters
permissions Optional array of at most 20 nonblank, single-line strings; each string is at most 200 characters
items Optional array of at most 20 objects containing only material and amount; material is uppercased, must match [A-Z0-9_]{1,80}, resolve through Bukkit Material.matchMaterial, and be an item material; amount is an integer 1–64 and defaults to 1
money Optional finite JSON number from 0 through 1,000,000,000; defaults to 0 and only a positive value counts as an action
chancePercent Optional finite JSON number from 0 through 100; defaults to 100
onlineOnly Optional native JSON boolean; defaults to false

At least one command, message, item, permission, or positive money value is required. This endpoint validates a proposal for the UI: it does not evaluate arbitrary requirement code, choose random outcomes, execute commands, grant items/currency/permissions, or persist YAML. Configuration still requires the normal preview/apply lane. The complete encoded proposal filter is limited to 64 KiB.

Reward-builder persistence

The configuration lane can persist the same typed object with the PREVIEW/APPLY-only reward-builder preset:

{
  "domain": "quick-setup",
  "preset": "reward-builder",
  "options": {"proposal":"{\"scope\":\"site\",...}"}
}

options must contain only proposal, encoded with a standard JSON serializer and limited to 64 KiB of UTF-8. There is no READ operation for this preset. Control strips the proposal from public operation views, and its durable journal stores only the redacted domain/preset. The node result and pending-result journal retain only the safe derived targetFile (VoteSites.yml or SpecialRewards.yml), never the proposal. PREVIEW validates the proposal and reports deterministic path changes; APPLY remains bound to that preview's revision and one-time approval.

The preset clears and rebuilds exactly one selected reward subtree:

Scope Managed file Replaced path
site VoteSites.yml VoteSites.<site>.Rewards; <site> must already be configured
every-site VoteSites.yml EverySiteReward
vote-party SpecialRewards.yml VoteParty.Rewards

The mapping is deterministic: commands → Commands; player/broadcast messages → Messages.Player / Messages.Broadcast; items → numbered Items.ControlItemN.Material and .Amount; positive money → Money; permissions → numbered AdvancedRewards.ControlPermissionN.TempPermission.Permission with Expiration: 2147483647; chance → Chance; and online-only → RewardType: ONLINE (otherwise BOTH). At least one action is required. The preset neither executes rewards nor changes a different site's rewards, the other global reward scope, or unrelated VoteParty settings. Atomic staging, reload, rollback, and stale-revision protection are unchanged.

Detected unconfigured services and player last votes

vote-site-health.result.detectedUnconfiguredServices is a case-insensitive, deduplicated, sorted array of at most 100 sanitized service names copied from VotingPlugin's persisted GottenServiceSites list when no configured ServiceSite matches. detectedUnconfiguredServicesTruncated reports whether more values existed. This list remains available without VoteLogging. Keep it distinct from unmatchedLoggedServices, which is derived from retained VoteLog aggregates and is empty/non-authoritative unless voteLogReadable is true. An enabled row with a configured service uses VOTE_LOG_UNAVAILABLE or VOTE_LOG_UNREADABLE, not NO_RECENT_VOTES, when aggregates cannot be read; DISABLED and SERVICE_SITE_MISSING keep their higher-priority configuration status. The at-most-100 configured sites shown in the response are queried through a separate bounded prepared filter, so falling outside the 100 most recently active services cannot be misreported as zero votes. Matching and aggregates use the complete case-normalized ServiceSite, including valid names longer than the 64-character display field; output truncation never changes lookup identity. Case variants are combined into one aggregate, and unmatchedLoggedServices excludes all configured ServiceSites, including configured rows beyond the displayed 100-site page.

An exact player result includes at most 100 lastVotes rows with siteKey, displayName, serviceSite, and time, plus lastVotesTruncated. These are stored last-vote values for sites that currently resolve as enabled; disabled, invalid, or unloaded site keys are not returned. They are not log enumeration or an end-to-end delivery history. pendingOfflineVotes is a bounded count saturated at 100,000 rather than a detailed queue view.

Exact-player storage fields (schema version 1)

The player result additionally contains storageRowAvailable, storage, columns, and columnsTruncated. storageRowAvailable is true only when the exact loaded player has user data and a configured storage type that can be read. When false, columns is an empty array, columnsTruncated is false, and storage is omitted. storage is the storage enum name (for example SQLITE), not a connection or table description.

Each columns entry is exactly { "name": <string>, "type": <string>, "value": <string> }. value is a bounded string rendering: integer values are decimal text, booleans are true/false text, and string values are returned as text (a null string renders as empty text). type is exactly the underlying stored DataValue.getType().name(); the allow-list accepts only the corresponding string, boolean, or integer value type described below. Only these fields are eligible:

  • Static string names (must have a string value): UUID, PlayerName, LastOnline, DayVoteStreakLastUpdate, VoteRemindersLast.
  • Static boolean names (native boolean, or a string exactly matching true/false, case-insensitively): TopVoterIgnore, Reminded, DisableBroadcast.
  • Static integer names (integer value): VotePartyVotes, MonthTotal, AllTimeTotal, DailyTotal, WeeklyTotal, Points, DayVoteStreak, BestDayVoteStreak, WeekVoteStreak, BestWeekVoteStreak, MonthVoteStreak, BestMonthVoteStreak, HighestDailyTotal, HighestMonthlyTotal, HighestWeeklyTotal, LastMonthTotal, LastWeeklyTotal, LastDailyTotal.
  • Dynamic integer names: MonthTotal-<MONTH>-<YYYY> where <MONTH> is an uppercase English month name and <YYYY> is exactly four digits; and VoteShopLimit<suffix> where <suffix> is 1–64 characters from [A-Za-z0-9_-].
  • Four runtime-derived exact names: the configured cooldown flag (CoolDownCheck or CoolDownCheck_<server-storage-name>, boolean), cooldown-site list (CoolDownCheck_Sites or CoolDownCheck_<server-storage-name>_Sites, string), all-sites day (AllSitesLast or AllSitesLast_<server-storage-name>, integer), and almost-all-sites day (AlmostAllSitesLast or AlmostAllSitesLast_<server-storage-name>, integer). The runtime names are compared exactly; they are not wildcards.

Other stored keys—including serialized offline/reward payloads, plugin-specific keys, malformed dynamic spellings, and allow-listed names with the wrong value type—are omitted. Entries are sorted by name case-insensitively, then by exact spelling, and at most 100 eligible entries are returned. A 101st eligible entry or a value larger than 16 KiB sets columnsTruncated:true; oversized values themselves are omitted. The complete result is bounded at 512 KiB. No credential, secret, raw payload, SQL metadata, or arbitrary storage key is exposed.

Data and security invariants

  • Inspection results may contain only the typed fields documented above. Never echo a credential, password, token, database or transport host, Control endpoint, webhook URL, raw configuration, or raw server log.
  • Unexpected managed configuration read, preview, apply, and reload exceptions retain their action-specific result code but return fixed external text; their detailed cause is logged only on the backend and is never copied into a Control result.
  • An unexpected handler exception returns only generic INSPECTION_FAILED text. The backend log may identify its exception class, but omits the exception message because it can contain storage endpoints, users, or filesystem paths.
  • diagnostics explicitly lists sensitive categories it omitted. It is a status report, not a support archive.
  • VoteLog access goes through bounded methods on VoteLogMysqlTable; Control never supplies SQL or a table name.
  • VoteLog queries execute on the connector worker and use prepared, bounded table methods. Never move them to the Bukkit primary thread or return database/table connection settings. Preserve the 10-second JDBC statement timeout.
  • Player lookup requires an exact UUID or valid Minecraft name and checks existence before loading. There is no “all players”, prefix search, or pagination cursor.
  • vote-site-resolution uses resolveVoteSite, not getVoteSite; the latter can auto-create configuration.
  • Reward simulation returns wouldExecute:false and sideEffects:false.
  • Reward-builder preview/apply shares the simulation parser but replaces only the selected subtree and never invokes a reward executor.
  • Capability negotiation is authoritative. An older Control that does not accept data.inspect.v1 must not receive inspection polling.

VoteLog interpretation

VoteLogging is optional and SQL-backed; a dependent query returns UNAVAILABLE when it is disabled, lacks an initialized adapter, or fails its readability probe. The quick setup updates and reloads Config.yml, but it does not recreate or close the runtime VoteLog manager. A server restart is therefore required after either VoteLogging.Enabled transition. Until then, disabling immediately gates any stale adapter and reports unavailable/readable false, while enabling a previously disabled instance can report enabled true but available false. The exposed rows are selected logged events: VOTE_RECEIVED, VOTEMILESTONE, VOTE_STREAK_REWARD, TOP_VOTER_REWARD, and VOTESHOP_PURCHASE. IMMEDIATE and CACHED describe recorded processing status.

overview, diagnostics, and vote-site health distinguish configured, enabled-and-adapter-available, and currently readable state. voteLogReadable runs a bounded probe with a 10-second JDBC statement timeout. Existing VoteLog table methods catch SQL failures and return empty/zero values, so the inspection layer probes first: summary/search/trace fail UNAVAILABLE when the probe fails, and health skips aggregates and labels each applicable row unavailable/unreadable. The probe is point-in-time; a later SQL failure can still trigger a legacy empty/zero fallback, so it is not a transactional health guarantee.

A voteId correlates rows written with the same identifier. It is not a complete delivery trace: VoteLog does not promise an entry for every validation rejection, network hop, duplicate decision, reward command, command outcome, or expiry. UI and support output must say “logged events” and must not claim end-to-end delivery proof.