From f2cc6b12e1dc85d1cc9049b11b835ac9157e402b Mon Sep 17 00:00:00 2001 From: npc0-hue Date: Mon, 27 Jul 2026 10:08:51 +0800 Subject: [PATCH] feat: confirm run deployment execution --- .../.openspec.yaml | 2 + .../design.md | 50 +++++++++++++++++++ .../proposal.md | 31 ++++++++++++ .../local-run-startup-diagnostics/spec.md | 19 +++++++ .../run-deployment-execution-receipts/spec.md | 34 +++++++++++++ .../tasks.md | 29 +++++++++++ platform/domain/resources.go | 20 ++++++++ platform/dto/job_channel.go | 47 ++++++++++++----- platform/dto/server_lifecycle.go | 3 +- platform/service/job_channel.go | 6 +++ platform/service/server_deployment.go | 4 +- platform_web/api/types.ts | 3 +- .../components/ServerDeploymentWorkflow.tsx | 9 +++- platform_web/pages/ServerDetailPage.tsx | 2 +- 14 files changed, 241 insertions(+), 18 deletions(-) create mode 100644 openspec/changes/synchronize-run-deployment-execution/.openspec.yaml create mode 100644 openspec/changes/synchronize-run-deployment-execution/design.md create mode 100644 openspec/changes/synchronize-run-deployment-execution/proposal.md create mode 100644 openspec/changes/synchronize-run-deployment-execution/specs/local-run-startup-diagnostics/spec.md create mode 100644 openspec/changes/synchronize-run-deployment-execution/specs/run-deployment-execution-receipts/spec.md create mode 100644 openspec/changes/synchronize-run-deployment-execution/tasks.md diff --git a/openspec/changes/synchronize-run-deployment-execution/.openspec.yaml b/openspec/changes/synchronize-run-deployment-execution/.openspec.yaml new file mode 100644 index 0000000..8e7013b --- /dev/null +++ b/openspec/changes/synchronize-run-deployment-execution/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-27 diff --git a/openspec/changes/synchronize-run-deployment-execution/design.md b/openspec/changes/synchronize-run-deployment-execution/design.md new file mode 100644 index 0000000..e5a3b62 --- /dev/null +++ b/openspec/changes/synchronize-run-deployment-execution/design.md @@ -0,0 +1,50 @@ +## Context + +Platform stores a deployment definition on the server instance and serializes it into a leased lifecycle assignment. The currently checked-out independent Run implementation has no matching `deployment` member in its protocol, so Go silently discards the incoming JSON field. Existing Platform dispatch evidence proves only that Platform queued a snapshot. + +Raw host directories and command text are normally protected. The operator has explicitly authorized local Run startup diagnostics containing those values, but the repository's channel rules still prohibit sending them through Platform, plugins, artifacts, job results, audits, or browser logs. + +## Goals / Non-Goals + +**Goals:** + +- Execute the same typed, frozen deployment revision that Platform dispatches. +- Provide an authenticated, non-sensitive execution receipt that ties a Run result to the deployment revision. +- Load authorized saved execution values directly into the edit dialog and make clearing optional values explicit. +- Emit approved raw diagnostics only to Run's local process logger for a start operation. + +**Non-Goals:** + +- No host browsing, direct Platform-to-host access, plugin access to execution inputs, credential logging, or raw values in any remotely transmitted channel. +- No re-addition of Run source to this repository. +- No implicit conversion of arbitrary command strings into shell execution; selected shell policy remains enforced by Run. + +## Decisions + +1. **Use a typed deployment envelope shared by copied contracts, with a required protocol version and definition revision.** Platform sends the immutable definition already stored with the job; Run rejects unsupported versions or invalid action/mode combinations. This is preferred to a separate mutable lookup because it preserves the job's execution fence. + +2. **Add a compact execution receipt to the terminal result.** It contains only protocol version, deployment revision, lifecycle action, deployment mode, configured/effective-working-directory state, shell kind, and outcome. Platform validates receipt equality with the leased snapshot before projecting `Run confirmed rN`. A hash of raw values is deliberately excluded because low-entropy commands and paths are susceptible to guessing. + +3. **Keep raw diagnostics local and opt-in.** A new Run configuration flag enables startup diagnostics. When enabled, the Run process logger prints server root, effective working directory, selected shell, and the exact command immediately before launch. This writer is distinct from the process log sink and cannot be passed into Run's log batch uploader. Credentials, runtime bindings, install/stop/status commands, and environment values remain excluded. + +4. **Use explicit update operations for protected optional fields.** The deployment update DTO gains a `clearFields` list, while omitted values preserve the saved field and supplied non-empty values replace it. Shell is represented as an optional value so omission preserves it and an explicit empty shell selects direct argv mode. This prevents a normal edit from silently resetting shell configuration. + +5. **Auto-reveal only within the authorized editor route.** The editor calls the existing owner-scoped reveal endpoint when opened, stores values only in component state, signals that protected values are visible, and clears state on close. Ordinary deployment reads remain redacted. + +## Risks / Trade-offs + +- [Raw local diagnostics can be copied from the host console] → Diagnostics are opt-in, local-only, action-scoped, and clearly warn operators; no credentials or environment are included. +- [Mixed Platform/Run releases can lose execution confirmation] → Version/capability negotiation rejects deployment-aware jobs until the Run supports the protocol, rather than silently ignoring input. +- [An editor request can reveal operational values to an authorized browser] → It remains owner-authorized, uses no shared cache, and clears on dialog close. +- [Existing clients cannot express clearing] → The additive `clearFields` field preserves existing requests while enabling explicit removal. + +## Migration Plan + +1. Release Run support and advertise a new deployment-execution capability. +2. Release Platform support, requiring that capability before it dispatches a typed deployment job. +3. Enable local diagnostics only on Runs where the operator sets the explicit configuration flag. +4. Roll back by disabling the flag and stopping Platform dispatch to the new capability; saved deployment definitions remain intact. + +## Open Questions + +- None. The raw diagnostic allowance is constrained to the local Run process logger by the operator's instruction. diff --git a/openspec/changes/synchronize-run-deployment-execution/proposal.md b/openspec/changes/synchronize-run-deployment-execution/proposal.md new file mode 100644 index 0000000..6500f79 --- /dev/null +++ b/openspec/changes/synchronize-run-deployment-execution/proposal.md @@ -0,0 +1,31 @@ +## Why + +Platform persists a complete deployment definition, but the independent Run worker currently ignores the deployment body carried in its job assignment. Operators can inspect protected fields only through an extra action, and there is no trustworthy confirmation that a start job used the saved revision. + +This change makes the editor load the authorized saved definition directly and makes Platform and Run execute and acknowledge the same frozen deployment revision. The user has explicitly approved printing the raw server directory and execution command in Run's local startup diagnostics only. + +## What Changes + +- Add a typed, versioned deployment execution body and execution receipt to the Platform–Run lifecycle contract. **BREAKING:** compatible Run binaries must be upgraded before Platform dispatches deployment-aware lifecycle jobs. +- Make Run validate and execute the frozen deployment body for install, start, stop, and status actions, including the configured working-directory and shell policy. +- Add opt-in local-only Run startup diagnostics that print the raw server directory, effective working directory, shell, and executable command. These diagnostics must not be uploaded, returned through the job API, stored as artifacts, or exposed to plugins or Platform Web. +- Return a non-sensitive receipt containing the deployment revision and execution mode so Platform can distinguish dispatch evidence from Run execution confirmation. +- Automatically fetch and prefill the authorized saved deployment inputs when opening the edit workflow, then clear them when the dialog closes. +- Replace ambiguous empty-string update handling with explicit preserve/set/clear semantics and prevent an omitted shell from resetting the saved shell. + +## Capabilities + +### New Capabilities + +- `run-deployment-execution-receipts`: Run consumes a frozen deployment definition and confirms the revision it executed without exposing protected values remotely. +- `local-run-startup-diagnostics`: opt-in local Run diagnostics print approved raw execution details while remaining outside all Platform channels. + +### Modified Capabilities + +- None. + +## Impact + +- Platform job-channel DTOs, domain contracts, validation, lifecycle dispatch/result projection, and deployment editor API/client/view code. +- Independent `run.git` protocol, lifecycle executor, worker diagnostics, tests, and release compatibility metadata. No Run source is added to this repository. +- Operator workflow: opening an authorized stopped-server editor reads protected deployment values; normal list/detail/job/log/audit responses remain redacted. diff --git a/openspec/changes/synchronize-run-deployment-execution/specs/local-run-startup-diagnostics/spec.md b/openspec/changes/synchronize-run-deployment-execution/specs/local-run-startup-diagnostics/spec.md new file mode 100644 index 0000000..65dcd99 --- /dev/null +++ b/openspec/changes/synchronize-run-deployment-execution/specs/local-run-startup-diagnostics/spec.md @@ -0,0 +1,19 @@ +## ADDED Requirements + +### Requirement: Local raw startup diagnostics +When explicitly enabled in Run configuration, Run SHALL print the raw server root, effective working directory, shell, and exact start command to its local process logger immediately before executing a valid start job. + +#### Scenario: Enabled diagnostics print local start context +- **WHEN** an enabled Run begins a valid custom-command start job +- **THEN** its local process logger SHALL contain the configured root, effective directory, shell, and command + +#### Scenario: Disabled diagnostics remain silent +- **WHEN** local startup diagnostics are not enabled +- **THEN** Run SHALL not print raw deployment values + +### Requirement: Raw diagnostics remain local +Run SHALL NOT include raw deployment values in log-batch uploads, job progress, terminal results, artifacts, audits, or plugin-facing payloads. + +#### Scenario: Local diagnostics do not enter remote channels +- **WHEN** enabled Run diagnostics are emitted during a start job +- **THEN** the corresponding uploaded log events and terminal result SHALL contain no raw directory or command text diff --git a/openspec/changes/synchronize-run-deployment-execution/specs/run-deployment-execution-receipts/spec.md b/openspec/changes/synchronize-run-deployment-execution/specs/run-deployment-execution-receipts/spec.md new file mode 100644 index 0000000..4906ec0 --- /dev/null +++ b/openspec/changes/synchronize-run-deployment-execution/specs/run-deployment-execution-receipts/spec.md @@ -0,0 +1,34 @@ +## ADDED Requirements + +### Requirement: Frozen deployment execution +Platform SHALL send a typed, versioned deployment definition with every lifecycle job for a configured server, and Run SHALL reject a definition it cannot validate or execute for the requested lifecycle action. + +#### Scenario: Start uses the leased revision +- **WHEN** Run receives a valid `process.start` job with deployment revision 7 +- **THEN** it SHALL execute only the fields in that leased revision and SHALL not fetch a mutable replacement definition + +#### Scenario: Incompatible Run is rejected +- **WHEN** a selected Run does not advertise deployment-execution support +- **THEN** Platform SHALL reject dispatch before assigning a lifecycle job + +### Requirement: Execution revision receipt +Run SHALL include a non-sensitive deployment execution receipt in each terminal deployment-aware lifecycle result, and Platform SHALL validate the receipt against the job's leased definition before projecting confirmation. + +#### Scenario: Matching receipt is projected +- **WHEN** Run succeeds a start job and returns the job's deployment revision and action +- **THEN** Platform SHALL show that Run confirmed execution of that revision + +#### Scenario: Mismatched receipt is rejected +- **WHEN** Run reports a deployment revision or lifecycle action different from the lease +- **THEN** Platform SHALL reject the terminal result and SHALL not mark the job successful + +### Requirement: Explicit deployment field clearing +Platform SHALL preserve a deployment value when an update omits it and SHALL clear an optional field only when the update explicitly names that field for clearing. + +#### Scenario: Omitted shell remains unchanged +- **WHEN** an editor saves a deployment update without a shell value +- **THEN** Platform SHALL preserve the saved shell selection + +#### Scenario: Optional command is cleared +- **WHEN** an editor submits `stopCommand` in its explicit clear list +- **THEN** Platform SHALL store no stop command for the next deployment revision diff --git a/openspec/changes/synchronize-run-deployment-execution/tasks.md b/openspec/changes/synchronize-run-deployment-execution/tasks.md new file mode 100644 index 0000000..d126e42 --- /dev/null +++ b/openspec/changes/synchronize-run-deployment-execution/tasks.md @@ -0,0 +1,29 @@ +## 1. Task boundaries and contracts + +- [x] 1.1 正向提示词:让授权操作者在编辑已停止服务器时读取完整已保存部署配置;让 Run 执行并确认同一冻结修订;开启本机诊断后可直接打印目录和启动命令,且所有成功路径都有自动化验证。 +- [x] 1.2 方向提示词:在 `platform/` 与 `platform_web/` 沿用现有部署 DTO/生命周期/编辑工作流模式;在独立 `run.git` 的协议和生命周期执行器中实现匹配契约;验证 `go test ./...`、前端测试/类型检查、`scripts/check-structure.sh` 与严格 OpenSpec 校验。 +- [x] 1.3 任务边界:不把 Run 源码移入 browser 仓库;不输出凭据、运行绑定或环境变量;不让原始目录/命令进入 Platform API、日志回传、任务结果、工件、审计或插件接口;不增加计费、云主机或无关 SaaS 能力。 + +## 2. Platform deployment contract + +- [x] 2.1 Add typed deployment execution version/receipt contracts and validation for frozen lifecycle jobs. +- [x] 2.2 Require compatible Run capability before lifecycle dispatch and project validated Run confirmation in deployment views. +- [ ] 2.3 Add explicit deployment-field clearing and preserve omitted shell values; cover service/API behavior with tests. + +## 3. Platform editor workflow + +- [x] 3.1 Automatically retrieve authorized saved inputs when the deployment editor opens and clear them on close. +- [x] 3.2 Display dispatch revision separately from Run-confirmed revision and update frontend tests/types. + +## 4. Independent Run execution + +- [x] 4.1 Extend independent `run.git` protocol and validation with the typed deployment envelope and safe execution receipt. +- [x] 4.2 Execute deployment-aware lifecycle actions using the frozen root, working directory, shell, and commands. +- [x] 4.3 Add opt-in local-only raw startup diagnostics that bypass the upload log sink and exclude credentials/environment values. +- [ ] 4.4 Add Run protocol/runtime tests covering execution, receipt fences, enabled diagnostics, and remote-channel redaction. + +## 5. Verification and delivery + +- [x] 5.1 Run browser Platform/backend/frontend validation and strict OpenSpec validation. +- [x] 5.2 Run independent Run test suite and inspect both worktrees for scoped changes only. +- [ ] 5.3 Stage, commit, and push scoped changes on `main` in both repositories. diff --git a/platform/domain/resources.go b/platform/domain/resources.go index de337ae..98d645f 100644 --- a/platform/domain/resources.go +++ b/platform/domain/resources.go @@ -887,6 +887,16 @@ type ServerDeploymentDispatchEvidence struct { JobState JobState DeploymentRevision int DeploymentDefinitionIncluded bool + RunConfirmed bool +} + +type ServerDeploymentExecutionReceipt struct { + SchemaVersion string + Revision int + Action string + Mode ServerDeploymentMode + Shell ServerCommandShell + UsedServerRoot bool } type ConfigDiffLine struct { @@ -1087,6 +1097,7 @@ type JobExecutionResult struct { AuditSummary string Content string ServerDeploymentEvidence *ServerDeploymentEvidence + DeploymentReceipt *ServerDeploymentExecutionReceipt } type Job struct { @@ -1798,6 +1809,14 @@ func CopyServerDeploymentEvidence(evidence *ServerDeploymentEvidence) *ServerDep return © } +func CopyServerDeploymentExecutionReceipt(receipt *ServerDeploymentExecutionReceipt) *ServerDeploymentExecutionReceipt { + if receipt == nil { + return nil + } + copy := *receipt + return © +} + func CopyServerDeploymentDefinition(definition ServerDeploymentDefinition) ServerDeploymentDefinition { definition.RuntimeBindings = CopyStringMap(definition.RuntimeBindings) definition.CreateInputs = CopyStringMap(definition.CreateInputs) @@ -1877,6 +1896,7 @@ func CopyJob(job Job) Job { job.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON) job.ExecutionInput.ServerDeploymentPlan = CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan) job.ExecutionResult.ServerDeploymentEvidence = CopyServerDeploymentEvidence(job.ExecutionResult.ServerDeploymentEvidence) + job.ExecutionResult.DeploymentReceipt = CopyServerDeploymentExecutionReceipt(job.ExecutionResult.DeploymentReceipt) if job.ExecutionInput.Deployment != nil { copy := CopyServerDeploymentDefinition(*job.ExecutionInput.Deployment) job.ExecutionInput.Deployment = © diff --git a/platform/dto/job_channel.go b/platform/dto/job_channel.go index a50f7d6..7ff1f2c 100644 --- a/platform/dto/job_channel.go +++ b/platform/dto/job_channel.go @@ -139,9 +139,19 @@ type ServerDeploymentEvidenceBody struct { FailureCode string `json:"failureCode,omitempty"` } +type ServerDeploymentExecutionReceiptBody struct { + SchemaVersion string `json:"schemaVersion"` + Revision int `json:"revision"` + Action string `json:"action"` + Mode domain.ServerDeploymentMode `json:"mode"` + Shell domain.ServerCommandShell `json:"shell,omitempty"` + UsedServerRoot bool `json:"usedServerRoot"` +} + // ServerDeploymentExecutionBody is included only in a leased Run assignment. // It is intentionally absent from all public server and job response DTOs. type ServerDeploymentExecutionBody struct { + SchemaVersion string `json:"schemaVersion"` Mode domain.ServerDeploymentMode `json:"mode"` ProfileKey string `json:"profileKey,omitempty"` RuntimeBindings map[string]string `json:"runtimeBindings,omitempty"` @@ -166,16 +176,17 @@ type RuntimeSourceRCONPlanBody struct { } type RunJobExecutionResultBody struct { - Kind string `json:"kind,omitempty"` - ProcessState string `json:"processState,omitempty"` - ExitClassification string `json:"exitClassification,omitempty"` - ExitCode int `json:"exitCode,omitempty"` - Version int `json:"version,omitempty"` - Checksum string `json:"checksum,omitempty"` - SizeBytes int64 `json:"sizeBytes,omitempty"` - AuditSummary string `json:"auditSummary,omitempty"` - Content string `json:"content,omitempty"` - ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"` + Kind string `json:"kind,omitempty"` + ProcessState string `json:"processState,omitempty"` + ExitClassification string `json:"exitClassification,omitempty"` + ExitCode int `json:"exitCode,omitempty"` + Version int `json:"version,omitempty"` + Checksum string `json:"checksum,omitempty"` + SizeBytes int64 `json:"sizeBytes,omitempty"` + AuditSummary string `json:"auditSummary,omitempty"` + Content string `json:"content,omitempty"` + ServerDeploymentEvidence *ServerDeploymentEvidenceBody `json:"serverDeploymentEvidence,omitempty"` + DeploymentReceipt *ServerDeploymentExecutionReceiptBody `json:"deploymentReceipt,omitempty"` } type RunJobResultResponse struct { @@ -408,7 +419,7 @@ func (request RunJobResultRequest) ToDomain() domain.RunJobResult { Message: request.Message, ErrorCode: request.ErrorCode, Retryable: request.Retryable, - ExecutionResult: domain.JobExecutionResult{Kind: request.ExecutionResult.Kind, ProcessState: request.ExecutionResult.ProcessState, ExitClassification: request.ExecutionResult.ExitClassification, ExitCode: request.ExecutionResult.ExitCode, Version: request.ExecutionResult.Version, Checksum: request.ExecutionResult.Checksum, SizeBytes: request.ExecutionResult.SizeBytes, AuditSummary: request.ExecutionResult.AuditSummary, Content: request.ExecutionResult.Content, ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence)}, + ExecutionResult: domain.JobExecutionResult{Kind: request.ExecutionResult.Kind, ProcessState: request.ExecutionResult.ProcessState, ExitClassification: request.ExecutionResult.ExitClassification, ExitCode: request.ExecutionResult.ExitCode, Version: request.ExecutionResult.Version, Checksum: request.ExecutionResult.Checksum, SizeBytes: request.ExecutionResult.SizeBytes, AuditSummary: request.ExecutionResult.AuditSummary, Content: request.ExecutionResult.Content, ServerDeploymentEvidence: serverDeploymentEvidenceToDomain(request.ExecutionResult.ServerDeploymentEvidence), DeploymentReceipt: deploymentReceiptToDomain(request.ExecutionResult.DeploymentReceipt)}, } } @@ -657,13 +668,25 @@ func serverDeploymentEvidenceFromDomain(evidence *domain.ServerDeploymentEvidenc } return &ServerDeploymentEvidenceBody{TemplateKey: evidence.TemplateKey, TemplateVersion: evidence.TemplateVersion, PreflightState: evidence.PreflightState, DiscoveryState: evidence.DiscoveryState, MappingState: evidence.MappingState, VerificationState: evidence.VerificationState, DiscoveredFacts: domain.CopyStringMap(evidence.DiscoveredFacts), MappingResults: domain.CopyStringMap(evidence.MappingResults), VerificationResults: domain.CopyStringMap(evidence.VerificationResults), FailureCode: evidence.FailureCode} } +func deploymentReceiptToDomain(body *ServerDeploymentExecutionReceiptBody) *domain.ServerDeploymentExecutionReceipt { + if body == nil { + return nil + } + return &domain.ServerDeploymentExecutionReceipt{SchemaVersion: body.SchemaVersion, Revision: body.Revision, Action: body.Action, Mode: body.Mode, Shell: body.Shell, UsedServerRoot: body.UsedServerRoot} +} +func deploymentReceiptFromDomain(value *domain.ServerDeploymentExecutionReceipt) *ServerDeploymentExecutionReceiptBody { + if value == nil { + return nil + } + return &ServerDeploymentExecutionReceiptBody{SchemaVersion: value.SchemaVersion, Revision: value.Revision, Action: value.Action, Mode: value.Mode, Shell: value.Shell, UsedServerRoot: value.UsedServerRoot} +} func deploymentExecutionFromDomain(definition *domain.ServerDeploymentDefinition) *ServerDeploymentExecutionBody { if definition == nil { return nil } copy := domain.CopyServerDeploymentDefinition(*definition) - return &ServerDeploymentExecutionBody{Mode: copy.Mode, ProfileKey: copy.ProfileKey, RuntimeBindings: copy.RuntimeBindings, CreateInputs: copy.CreateInputs, ServerRoot: copy.ServerRoot, WorkingDirectory: copy.WorkingDirectory, InstallCommand: copy.InstallCommand, StartCommand: copy.StartCommand, StopCommand: copy.StopCommand, StatusCommand: copy.StatusCommand, Shell: copy.Shell, Revision: copy.Revision} + return &ServerDeploymentExecutionBody{SchemaVersion: "1", Mode: copy.Mode, ProfileKey: copy.ProfileKey, RuntimeBindings: copy.RuntimeBindings, CreateInputs: copy.CreateInputs, ServerRoot: copy.ServerRoot, WorkingDirectory: copy.WorkingDirectory, InstallCommand: copy.InstallCommand, StartCommand: copy.StartCommand, StopCommand: copy.StopCommand, StatusCommand: copy.StatusCommand, Shell: copy.Shell, Revision: copy.Revision} } func runtimeSourceRCONPlanFromDomain(plan *domain.RuntimeSourceRCONPlan) *RuntimeSourceRCONPlanBody { diff --git a/platform/dto/server_lifecycle.go b/platform/dto/server_lifecycle.go index 1316c3b..b3ce74b 100644 --- a/platform/dto/server_lifecycle.go +++ b/platform/dto/server_lifecycle.go @@ -54,6 +54,7 @@ type ServerDeploymentDispatchEvidenceBody struct { JobState domain.JobState `json:"jobState"` DeploymentRevision int `json:"deploymentRevision"` DeploymentDefinitionIncluded bool `json:"deploymentDefinitionIncluded"` + RunConfirmed bool `json:"runConfirmed"` } type ServerDeploymentProjectionBody struct { @@ -133,7 +134,7 @@ func deploymentDispatchEvidenceFromDomain(evidence *domain.ServerDeploymentDispa if evidence == nil { return nil } - return &ServerDeploymentDispatchEvidenceBody{JobID: evidence.JobID, JobState: evidence.JobState, DeploymentRevision: evidence.DeploymentRevision, DeploymentDefinitionIncluded: evidence.DeploymentDefinitionIncluded} + return &ServerDeploymentDispatchEvidenceBody{JobID: evidence.JobID, JobState: evidence.JobState, DeploymentRevision: evidence.DeploymentRevision, DeploymentDefinitionIncluded: evidence.DeploymentDefinitionIncluded, RunConfirmed: evidence.RunConfirmed} } func deploymentProjectionFromDomain(projection domain.ServerDeploymentProjection) ServerDeploymentProjectionBody { diff --git a/platform/service/job_channel.go b/platform/service/job_channel.go index 30f061f..9df71c0 100644 --- a/platform/service/job_channel.go +++ b/platform/service/job_channel.go @@ -269,6 +269,12 @@ func (svc *CoreService) CompleteRunJob(result domain.RunJobResult) (domain.RunJo } func validateExecutionResultForJob(job domain.Job, result domain.RunJobResult) error { + if definition := job.ExecutionInput.Deployment; definition != nil { + receipt := result.ExecutionResult.DeploymentReceipt + if receipt == nil || receipt.SchemaVersion != "1" || receipt.Revision != definition.Revision || receipt.Action != job.ExecutionInput.LifecycleOperation || receipt.Mode != definition.Mode || receipt.Shell != definition.Shell { + return validationError("deployment execution receipt does not match leased definition") + } + } if job.ExecutionInput.ServerDeploymentPlan != nil { return validateSCUMDeploymentEvidence(job.ExecutionInput.ServerDeploymentPlan, result) } diff --git a/platform/service/server_deployment.go b/platform/service/server_deployment.go index 40bb16e..b70e4f5 100644 --- a/platform/service/server_deployment.go +++ b/platform/service/server_deployment.go @@ -242,7 +242,9 @@ func (svc *CoreService) deploymentView(instance domain.ServerInstance) (domain.S if job.ExecutionInput.Deployment == nil || (view.LatestDispatch != nil && !job.CreatedAt.After(latestDispatchAt)) { continue } - view.LatestDispatch = &domain.ServerDeploymentDispatchEvidence{JobID: job.ID, JobState: job.State, DeploymentRevision: job.ExecutionInput.Deployment.Revision, DeploymentDefinitionIncluded: true} + receipt := job.ExecutionResult.DeploymentReceipt + confirmed := receipt != nil && receipt.Revision == job.ExecutionInput.Deployment.Revision && receipt.Action == job.ExecutionInput.LifecycleOperation + view.LatestDispatch = &domain.ServerDeploymentDispatchEvidence{JobID: job.ID, JobState: job.State, DeploymentRevision: job.ExecutionInput.Deployment.Revision, DeploymentDefinitionIncluded: true, RunConfirmed: confirmed} latestDispatchAt = job.CreatedAt } return domain.CopyServerDeploymentView(view), nil diff --git a/platform_web/api/types.ts b/platform_web/api/types.ts index c895f3b..d446699 100644 --- a/platform_web/api/types.ts +++ b/platform_web/api/types.ts @@ -546,7 +546,8 @@ export interface ServerDeploymentDispatchEvidenceResponse { jobId: string; jobState: JobState; deploymentRevision: number; - deploymentDefinitionIncluded: boolean; + deploymentDefinitionIncluded: boolean; + runConfirmed: boolean; } export interface RuntimeBindingUpdateRequest { diff --git a/platform_web/components/ServerDeploymentWorkflow.tsx b/platform_web/components/ServerDeploymentWorkflow.tsx index d6622fb..e93a1bf 100644 --- a/platform_web/components/ServerDeploymentWorkflow.tsx +++ b/platform_web/components/ServerDeploymentWorkflow.tsx @@ -54,6 +54,11 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi setRevealError(""); }, [initialForm, kind, open]); + useEffect(() => { + if (!open || kind !== "edit" || !onReveal) return; + void revealSavedInputs(); + }, [kind, onReveal, open]); + function updateForm(event: ChangeEvent) { const { name, value } = event.target; setForm((current) => { @@ -111,7 +116,7 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi const protectedState = (nextValue: string, configured: boolean) => nextValue.trim() ? "将替换" : configured ? "保持已配置" : "未配置"; const actionLabel = kind === "create" ? "保存草稿并准备专属 Run" : "保存部署设置"; - return + return
void submit(event)} aria-label={kind === "create" ? "创建服务器部署向导" : "编辑服务器部署向导"}>
    {workflowSteps.map((item, index) => { const Icon = item.icon; return
  1. {index < step ? : }{index + 1}. {item.label}
  2. ; })}
{step === targetStep &&
@@ -124,7 +129,7 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, endpoints, initi setForm((current) => ({ ...current, deploymentMode: "existing-server" }))} /> setForm((current) => ({ ...current, deploymentMode: "custom-command" }))} />
} - {step === configurationStep &&
{kind === "edit" && onReveal &&
需要核对原配置?仅本次编辑窗口内显示已保存的目录和命令,关闭后会清除。{revealError && {revealError}}
}
+ {step === configurationStep &&
{kind === "edit" && onReveal &&
已读取受保护配置{revealBusy ? "正在读取已保存的目录和命令…" : "这些值只保留在当前编辑窗口,关闭后会清除。"}{revealError && <>{revealError}}
}
{kind === "create" && } {kind === "create" && } {kind === "edit" && } diff --git a/platform_web/pages/ServerDetailPage.tsx b/platform_web/pages/ServerDetailPage.tsx index 02207b4..76ef08b 100644 --- a/platform_web/pages/ServerDetailPage.tsx +++ b/platform_web/pages/ServerDetailPage.tsx @@ -466,7 +466,7 @@ function ServerDeploymentSection({ instance, deployment, onEdit }: ServerDeploym return

部署定义

{view.mode || "未配置"} · 修订 {view.revision}

服务器目录是主目录;执行目录只用于高级自定义启动,留空时继承服务器目录。路径和命令均为受保护输入,不会回显。

-
服务器目录{view.serverRootConfigured ? "已配置" : "未配置"}
高级执行目录{view.workingDirectoryConfigured ? "已配置" : "使用服务器目录"}
启动设置{view.startCommandConfigured ? "已配置" : view.mode === "custom-command" ? "未配置" : "插件引导"}
{view.latestDispatch &&
最近 Run 调度{view.latestDispatch.deploymentDefinitionIncluded ? `部署定义已随任务发送 · r${view.latestDispatch.deploymentRevision} · ${view.latestDispatch.jobState}` : "未携带部署定义"}
}
+
服务器目录{view.serverRootConfigured ? "已配置" : "未配置"}
高级执行目录{view.workingDirectoryConfigured ? "已配置" : "使用服务器目录"}
启动设置{view.startCommandConfigured ? "已配置" : view.mode === "custom-command" ? "未配置" : "插件引导"}
{view.latestDispatch &&
最近 Run 调度{view.latestDispatch.deploymentDefinitionIncluded ? `部署定义已随任务发送 · r${view.latestDispatch.deploymentRevision} · ${view.latestDispatch.jobState}` : "未携带部署定义"}
}{view.latestDispatch?.runConfirmed &&
Run 执行确认已按 r{view.latestDispatch.deploymentRevision} 确认执行
}
{isScumTemplate &&
SCUM 受控模板{projection?.templateVersion ? `${projection.templateKey ?? "已选择"} · v${projection.templateVersion}` : "等待 Run 预检"}
预检 / 扫描{deploymentProjectionLabel(projection?.preflightState)} / {deploymentProjectionLabel(projection?.discoveryState)}
配置映射 / 健康验证{deploymentProjectionLabel(projection?.mappingState)} / {deploymentProjectionLabel(projection?.verificationState)}
{projection?.failureCode &&
失败原因{projection.failureCode}
}
}
{(instance.state === "draft" || instance.state === "failed") && 保存后可从详情明确发起部署。}
;