Complete platform management workflows
This commit is contained in:
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user