Complete platform management workflows

This commit is contained in:
npc0-hue
2026-07-14 16:39:37 +08:00
parent 7e05d0a4e7
commit 4f33f761a3
106 changed files with 11313 additions and 460 deletions
+5
View File
@@ -17,6 +17,11 @@ type RunCapabilityReport struct {
type RunHelloRequest struct {
RegistrationToken string `json:"registrationToken"`
RunEndpointID string `json:"runEndpointId"`
ServerInstanceID string `json:"serverInstanceId,omitempty"`
PluginID string `json:"pluginId,omitempty"`
ComponentKind string `json:"componentKind,omitempty"`
ComponentKey string `json:"componentKey,omitempty"`
KeyGeneration int `json:"keyGeneration,omitempty"`
DisplayName string `json:"displayName"`
Version string `json:"version"`
Status string `json:"status"`
+23 -7
View File
@@ -3,13 +3,29 @@ package protocol
import "time"
const (
RunCapabilityProcessInstall = "process.install"
RunCapabilityProcessStart = "process.start"
RunCapabilityProcessStop = "process.stop"
RunCapabilityLogsRead = "logs.read"
RunCapabilityConfigWrite = "config.write"
RunCapabilityFilesRead = "files.read"
RunCapabilityFilesWrite = "files.write"
RunCapabilityProcessInstall = "process.install"
RunCapabilityProcessStart = "process.start"
RunCapabilityProcessStop = "process.stop"
RunCapabilityLogsRead = "logs.read"
RunCapabilityConfigWrite = "config.write"
RunCapabilityFilesRead = "files.read"
RunCapabilityFilesWrite = "files.write"
RunCapabilityRemoteFTPRead = "remote.ftp.read"
RunCapabilityRemoteFTPWrite = "remote.ftp.write"
RunCapabilityRemoteRsyncRead = "remote.rsync.read"
RunCapabilityRemoteRsyncWrite = "remote.rsync.write"
RunCapabilityRemoteRunFilesRead = "remote.run.files.read"
RunCapabilityRemoteRunFilesWrite = "remote.run.files.write"
RunCapabilityRemoteRunProcessStart = "remote.run.process.start"
RunCapabilityRemoteRunProcessStop = "remote.run.process.stop"
RunCapabilityRemoteRunDBMySQLQuery = "remote.run.db.mysql.query"
RunCapabilityRemoteRunDBSQLiteQuery = "remote.run.db.sqlite.query"
RunCapabilityRemoteRunLogsTransfer = "remote.run.logs.transfer"
RunCapabilityRemoteRunRCONCommand = "remote.run.rcon.command"
RunCapabilityRunSelfUpdate = "run.self-update"
RunCapabilityDependenciesCheck = "dependencies.check"
RunCapabilityDependenciesInstall = "dependencies.install"
RunCapabilityLogsBackfill = "logs.backfill"
)
type RunJobProgressReport struct {
+19
View File
@@ -39,6 +39,23 @@ Platform-dispatched config/file jobs are now represented in the run job payload
- `files.read`: reads a declared logical file key and returns results through bounded metadata or artifact refs.
- `files.write`: writes content addressed by a logical file key plus scoped `input://...` or `artifact://...` ref.
Plugin-declared remote access jobs use the same job channel and remain bounded metadata envelopes:
- `remote.ftp.read` / `remote.ftp.write`: platform-mediated FTP file transfer requests.
- `remote.rsync.read` / `remote.rsync.write`: platform-mediated rsync file transfer requests.
- `remote.run.files.read` / `remote.run.files.write`: run-mediated logical file operations.
- `remote.run.process.start` / `remote.run.process.stop`: run-mediated remote process lifecycle operations.
- `remote.run.db.mysql.query` / `remote.run.db.sqlite.query`: run-mediated database read envelopes with scoped input refs for query payloads.
- `remote.run.logs.transfer`: run-mediated log transfer through log/artifact channels.
- `remote.run.rcon.command`: run-mediated RCON command envelopes with scoped input refs.
Run distribution and runtime support jobs use the same lightweight job lifecycle:
- `run.self-update`: stages an approved run artifact by `artifact://...` ref, verifies checksum/signature metadata, and reports a rollback-safe status ref.
- `dependencies.check`: runs a plugin-declared typed dependency probe addressed by a logical `dependencies/...` key.
- `dependencies.install`: runs only an approved typed install plan addressed by `dependencies/install/...`; arbitrary shell snippets are rejected by validation.
- `logs.backfill`: advances historical log cursors for declared process, file, FTP, SQL, or plugin-specific sources and returns bounded cursor/result refs instead of log bodies.
The executor resolves lifecycle action templates under the scoped server workspace and runs direct command/argument vectors through the process supervisor. It does not run unrestricted shell strings, execute arbitrary plugin code, expose host paths, return raw credentials, open direct sockets, or embed logs/artifacts in job result payloads.
## Rules
@@ -47,6 +64,8 @@ The executor resolves lifecycle action templates under the scoped server workspa
- Terminal result must be replayable while the journal retains the job.
- Large files must be passed as artifact references, not embedded in job payloads.
- Config/file job payloads must use logical target keys and scoped input/artifact refs.
- Remote database and RCON jobs must use scoped input/artifact refs rather than embedding query or command bodies in job results.
- Run self-update, dependency, and log backfill jobs must use declared capabilities, logical target keys, scoped refs, and bounded result refs.
- Job payloads must not include logs, artifact chunks, raw host paths, raw credentials, direct sockets, or large inline result bodies.
- Process stdout/stderr must be redacted and written to the log spool rather than embedded in progress/result bodies.
- Job ack/progress/result/cancel/reconcile calls are lightweight lifecycle metadata and must be able to complete while artifact/file transfer work is active or retrying.
+70
View File
@@ -23,6 +23,49 @@ func ValidateRunJobAssignment(assignment RunJobAssignment) error {
return ValidationError("inputRef is not allowed")
}
}
if IsRemoteCapability(assignment.Capability) {
if assignment.ServerInstanceID == "" {
return ValidationError("serverInstanceId is required for remote jobs")
}
if RemoteCapabilityRequiresTargetKey(assignment.Capability) && !ValidLogicalFileKey(assignment.TargetKey) {
return ValidationError("targetKey is not allowed")
}
if RemoteCapabilityRequiresInputRef(assignment.Capability) && !ValidScopedInputRef(assignment.InputRef) {
return ValidationError("inputRef is not allowed")
}
}
switch assignment.Capability {
case RunCapabilityRunSelfUpdate:
if assignment.ServerInstanceID == "" {
return ValidationError("serverInstanceId is required for self-update jobs")
}
if assignment.TargetKey != "run/update" {
return ValidationError("targetKey must be run/update")
}
if !ValidScopedInputRef(assignment.InputRef) || !strings.HasPrefix(assignment.InputRef, "artifact://") {
return ValidationError("inputRef must be an artifact ref for self-update")
}
case RunCapabilityDependenciesCheck, RunCapabilityDependenciesInstall:
if assignment.ServerInstanceID == "" {
return ValidationError("serverInstanceId is required for dependency jobs")
}
if !ValidLogicalFileKey(assignment.TargetKey) || !strings.HasPrefix(assignment.TargetKey, "dependencies/") {
return ValidationError("targetKey is not allowed for dependency jobs")
}
if assignment.InputRef != "" {
return ValidationError("dependency jobs must not carry arbitrary input refs")
}
case RunCapabilityLogsBackfill:
if assignment.ServerInstanceID == "" {
return ValidationError("serverInstanceId is required for log backfill jobs")
}
if !ValidLogicalFileKey(assignment.TargetKey) || !strings.HasPrefix(assignment.TargetKey, "logs/") {
return ValidationError("targetKey is not allowed for log backfill jobs")
}
if assignment.InputRef != "" && !ValidScopedInputRef(assignment.InputRef) {
return ValidationError("inputRef is not allowed for log backfill jobs")
}
}
return nil
}
@@ -56,3 +99,30 @@ func ValidScopedInputRef(ref string) bool {
}
return strings.HasPrefix(ref, "input://") || strings.HasPrefix(ref, "artifact://")
}
func IsRemoteCapability(capability string) bool {
return strings.HasPrefix(capability, "remote.")
}
func RemoteCapabilityRequiresTargetKey(capability string) bool {
switch capability {
case RunCapabilityRemoteRunProcessStart, RunCapabilityRemoteRunProcessStop:
return false
default:
return IsRemoteCapability(capability)
}
}
func RemoteCapabilityRequiresInputRef(capability string) bool {
switch capability {
case RunCapabilityRemoteFTPWrite,
RunCapabilityRemoteRsyncWrite,
RunCapabilityRemoteRunFilesWrite,
RunCapabilityRemoteRunDBMySQLQuery,
RunCapabilityRemoteRunDBSQLiteQuery,
RunCapabilityRemoteRunRCONCommand:
return true
default:
return false
}
}
+78
View File
@@ -44,3 +44,81 @@ func TestValidateRunJobAssignmentScopedReadDoesNotRequireInputRef(t *testing.T)
t.Fatalf("expected valid file read assignment: %v", err)
}
}
func TestValidateRunJobAssignmentRemoteCapabilitiesAreBounded(t *testing.T) {
assignment := RunJobAssignment{
JobID: "job-remote-rcon",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityRemoteRunRCONCommand,
TargetKey: "rcon/command",
InputRef: "input://server-1/rcon/command/1",
IdempotencyKey: "idem-rcon",
}
if err := ValidateRunJobAssignment(assignment); err != nil {
t.Fatalf("expected valid remote rcon assignment: %v", err)
}
assignment.InputRef = "password=raw"
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "inputRef") {
t.Fatalf("expected unsafe inputRef rejection, got %v", err)
}
assignment.InputRef = "input://server-1/rcon/command/1"
assignment.TargetKey = "/Users/tasia/server.db"
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "targetKey") {
t.Fatalf("expected unsafe targetKey rejection, got %v", err)
}
}
func TestValidateRunJobAssignmentDistributionCapabilitiesAreBounded(t *testing.T) {
selfUpdate := RunJobAssignment{
JobID: "job-update",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityRunSelfUpdate,
TargetKey: "run/update",
InputRef: "artifact://artifact-run-latest",
IdempotencyKey: "idem-update",
}
if err := ValidateRunJobAssignment(selfUpdate); err != nil {
t.Fatalf("expected valid self-update assignment: %v", err)
}
selfUpdate.InputRef = "input://not-an-artifact"
if err := ValidateRunJobAssignment(selfUpdate); err == nil || !strings.Contains(err.Error(), "artifact") {
t.Fatalf("expected non-artifact self-update ref rejection, got %v", err)
}
check := RunJobAssignment{
JobID: "job-dependency-check",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityDependenciesCheck,
TargetKey: "dependencies/java-21",
IdempotencyKey: "idem-dep-check",
}
if err := ValidateRunJobAssignment(check); err != nil {
t.Fatalf("expected valid dependency check assignment: %v", err)
}
check.TargetKey = "dependencies/install/java;rm"
if err := ValidateRunJobAssignment(check); err == nil || !strings.Contains(err.Error(), "targetKey") {
t.Fatalf("expected shell-like dependency target rejection, got %v", err)
}
backfill := RunJobAssignment{
JobID: "job-log-backfill",
ServerInstanceID: "server-1",
RunEndpointID: "run-local",
Capability: RunCapabilityLogsBackfill,
TargetKey: "logs/latest-log",
InputRef: "artifact://logs/checkpoint/1",
IdempotencyKey: "idem-log-backfill",
}
if err := ValidateRunJobAssignment(backfill); err != nil {
t.Fatalf("expected valid log backfill assignment: %v", err)
}
backfill.InputRef = "password=raw"
if err := ValidateRunJobAssignment(backfill); err == nil || !strings.Contains(err.Error(), "inputRef") {
t.Fatalf("expected unsafe log checkpoint rejection, got %v", err)
}
}