Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
853b4b1
Add a verified VotingPlugin artifact store
BenCodez Sep 13, 2026
2705bf0
Harden verified artifact retention
BenCodez Sep 13, 2026
5aed21c
Make artifact publication recoverable
BenCodez Sep 13, 2026
0001439
Make artifact eviction recovery transactional
BenCodez Sep 13, 2026
c06cf7e
Complete artifact rollback recovery safeguards
BenCodez Sep 13, 2026
9f43a48
Sync method-aware settings reads
BenCodez Sep 13, 2026
929bd3b
Harden artifact collision recovery
BenCodez Sep 13, 2026
772a9b6
Serialize artifact store transactions
BenCodez Sep 13, 2026
ee1af3d
Harden artifact publication and guided reads
BenCodez Sep 13, 2026
5d0fa7c
Harden artifact recovery and configuration state
BenCodez Sep 13, 2026
12fc708
Harden artifact upload and capability refresh
BenCodez Sep 13, 2026
6fda5bb
Fix capability-aware guided settings state
BenCodez Sep 13, 2026
e6f685d
Enforce configuration and artifact transaction contracts
BenCodez Sep 14, 2026
ad12ecd
Allow reads to report active capability state
BenCodez Sep 14, 2026
a38e68e
Keep guided reads capability-safe
BenCodez Sep 14, 2026
1e183c3
Require atomic artifact quarantine moves
BenCodez Sep 14, 2026
6057dbd
Harden artifact recovery and settings refresh
BenCodez Sep 14, 2026
e79dffd
Preserve claimed configuration capability
BenCodez Sep 14, 2026
6dfdb86
Validate reported configuration capabilities
BenCodez Sep 14, 2026
6f21a5c
Harden artifact recovery and capability state
BenCodez Sep 14, 2026
f67def2
Preserve unavailable Vote Party profile state
BenCodez Sep 14, 2026
9a0a341
Harden artifact admission and target selection
BenCodez Sep 14, 2026
ae6a344
Merge main into artifact store
BenCodez Sep 15, 2026
f6a1adf
Harden artifact access and setup reads
BenCodez Sep 15, 2026
22b4805
Require complete proxy capability sets
BenCodez Sep 15, 2026
d936c32
Reject stale proxy capability reads
BenCodez Sep 15, 2026
547b647
Document configuration task capability contract
BenCodez Sep 15, 2026
f17f5c6
Fix artifact recovery and legacy method reads
BenCodez Sep 15, 2026
979de2e
Recover artifacts before access
BenCodez Sep 15, 2026
f6f90ab
Keep artifact recovery within capacity
BenCodez Sep 15, 2026
6f0cb1d
Fail closed on legacy artifact recovery overflow
BenCodez Sep 15, 2026
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
7 changes: 7 additions & 0 deletions docs/control-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ Each target state is `QUEUED`, `IN_PROGRESS`, or `COMPLETE`; the aggregate state
`COMPLETED_WITH_ERRORS`. A claim has a two-minute lease and new `attemptId`. The result must echo that attempt and the
current node session, preventing a stale execution from completing reissued work.

The claimed `ConfigurationTask` also contains an exact `capability` field. Connectors must dispatch and validate the task
against that field rather than deriving a contract from `configuration`. In particular, an HTTP `proxy-backend` `READ`
uses `config.proxy-method.v2` while its task configuration deliberately omits the requested method so the current value is
not supplied as an answer. Legacy proxy-method reads carry `config.proxy-method.v1`. A connector must implement this
field and the matching v2 task/result contract before advertising `config.proxy-method.v2`; older connectors remain on
v1 and reject unsupported capability work through normal negotiation.

### Retry behavior

`POST /api/v1/operations/{operationId}/retry` creates a new operation; it never mutates the historical view.
Expand Down
4 changes: 4 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.11.4</junit.version>
<jackson.version>2.18.2</jackson.version>
<snakeyaml.version>2.6</snakeyaml.version>
</properties>
<dependencies>
<dependency>
Expand All @@ -19,6 +20,9 @@
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId><artifactId>jackson-datatype-jsr310</artifactId><version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId><artifactId>snakeyaml</artifactId><version>${snakeyaml.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>${junit.version}</version><scope>test</scope>
</dependency>
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ private ConfigurationTask claimCurrentSession(String nodeId, NodeStatus node) {
if ("QUEUED".equals(state) || ("IN_PROGRESS".equals(state) && leased != null
&& !now.isBefore(leased.plus(LEASE)))) {
if (cancelChangedFileRole(operation, node)) continue;
if (cancelChangedBackendSetupRole(operation, node)) continue;
if (cancelChangedProxyMethodRole(operation, node)) continue;
if (deferProxyMethodApply(operation, node)) continue;
if (cancelLostCapability(operation, node)) continue;
Expand All @@ -263,7 +264,7 @@ private ConfigurationTask claimCurrentSession(String nodeId, NodeStatus node) {
throw e;
}
return new ConfigurationTask(operation.id, operation.type, configurationForTask(operation),
operation.expectedRevisions.get(nodeId), attemptId);
operation.expectedRevisions.get(nodeId), attemptId, operation.configuration.capability());
}
}
return null;
Expand Down Expand Up @@ -378,6 +379,16 @@ private boolean cancelChangedFileRole(StoredOperation operation, NodeStatus node
return true;
}

private boolean cancelChangedBackendSetupRole(StoredOperation operation, NodeStatus node) {
if (!ManagedConfiguration.QUICK_SETUP.equals(operation.configuration.domain())
|| !"proxy-backend".equals(operation.configuration.preset())) return false;
String expectedPlatform = operation.targetPlatforms.get(node.nodeId());
if ("BUKKIT".equalsIgnoreCase(expectedPlatform) && "BUKKIT".equalsIgnoreCase(node.platform())) return false;
automaticCancellation(operation, node.nodeId(), sessionId(node), "TARGET_CHANGED",
"Node platform changed after the task was created; create it again", "TARGET_ROLE_CHANGED");
return true;
}

private boolean cancelLostCapability(StoredOperation operation, NodeStatus node) {
if (node.online() && node.acceptedCapabilities().contains(operation.configuration.capability())) return false;
automaticCancellation(operation, node.nodeId(), sessionId(node), "CAPABILITY_LOST",
Expand Down Expand Up @@ -557,11 +568,12 @@ private OperationView completeCurrentSession(UUID operationId, String nodeId, Co
if (!Objects.equals(operation.claimSessions.get(nodeId), result.sessionId())) {
throw new ValidationException("SESSION_MISMATCH", "Operation task belongs to another node session", List.of());
}
if (cancelChangedFileRole(operation, node) || cancelChangedProxyMethodRole(operation, node)
if (cancelChangedFileRole(operation, node) || cancelChangedBackendSetupRole(operation, node)
|| cancelChangedProxyMethodRole(operation, node)
|| cancelLostCapability(operation, node)) {
return view(operation);
}
validateResultConfiguration(operation, result);
validateResultConfiguration(operation, result, node);
String priorState = operation.states.get(nodeId);
ConfigurationTaskResult priorResult = operation.results.get(nodeId);
Instant priorLease = operation.leasedAt.get(nodeId);
Expand Down Expand Up @@ -698,17 +710,33 @@ private void reclaimStaleRestartSessions(String incomingNodeId) {
}
}

private static void validateResultConfiguration(StoredOperation operation, ConfigurationTaskResult result) {
private static void validateResultConfiguration(StoredOperation operation, ConfigurationTaskResult result,
NodeStatus node) {
ManagedConfiguration actual = result.configuration();
if (actual == null) return;
ManagedConfiguration expected = operation.configuration;
boolean mismatch = expected == null || !expected.domain().equals(actual.domain())
|| (ManagedConfiguration.FILE.equals(expected.domain()) && !expected.fileName().equals(actual.fileName()))
|| (ManagedConfiguration.QUICK_SETUP.equals(expected.domain()) && !expected.preset().equals(actual.preset()))
|| (!activeMethodRead(operation, expected) && !expected.capability().equals(actual.capability()));
|| (!expected.capability().equals(actual.capability())
&& !compatibleActiveMethodRead(operation, expected, actual, node));
if (mismatch) throw invalid("result configuration does not match the operation selector");
}

private static boolean compatibleActiveMethodRead(StoredOperation operation, ManagedConfiguration expected,
ManagedConfiguration actual, NodeStatus node) {
if (!activeMethodRead(operation, expected)) return false;
try {
actual.validateProposal();
} catch (IllegalArgumentException invalidMethod) {
return false;
}
if (PROXY_METHOD_HTTP_CAPABILITY.equals(expected.capability())
&& actual.options().containsKey("method")
&& !"HTTP".equals(actual.options().get("method"))) return true;
return node.acceptedCapabilities().contains(actual.capability());
}

private static boolean activeMethodRead(StoredOperation operation, ManagedConfiguration expected) {
return "READ".equals(operation.type) && ManagedConfiguration.QUICK_SETUP.equals(expected.domain())
&& (ManagedConfiguration.PROXY_METHOD.equals(expected.preset())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
import java.util.UUID;

public record ConfigurationTask(UUID operationId, String type, ManagedConfiguration configuration,
String expectedRevision, UUID attemptId) { }
String expectedRevision, UUID attemptId, String capability) { }
Comment thread
BenCodez marked this conversation as resolved.
Loading
Loading