feat: ship single-file run distribution and list key reset

Compile-time run auth replaces zip sidecars, lengthens run keys, revokes
active sessions on reset, and exposes run-key reset in the server list.
This commit is contained in:
npc0-hue
2026-07-24 14:14:26 +08:00
parent 2b1f553df0
commit 292b380f3c
17 changed files with 397 additions and 9 deletions
@@ -0,0 +1,60 @@
## Context
The existing distribution pipeline already queues real `distribution.build` jobs and keeps raw component keys out of platform APIs. The weak point is the generated package shape: Run artifacts still contain `config.json`, which makes accidental token disclosure easy when a ZIP is shared or inspected. The operator expectation is also a single `run.exe` on Windows, not an archive.
## Goals / Non-Goals
**Goals:**
- Produce Windows Run downloads as `run-windows-amd64.exe` and Linux Run downloads as raw executable files such as `run-linux-amd64`.
- Compile server identity and the current Run key into the Run binary using Go native `-ldflags -X`.
- Keep `RUN_PLATFORM_URL` and other explicit environment overrides working for local development and diagnostics.
- Support raw-executable Run self-update artifacts with checksum verification and the existing staging/rollback flow.
- Make run-key reset reachable from the server list while preserving the compact action popover.
- Immediately revoke an online Run control session after its key is reset.
**Non-Goals:**
- Do not add device authorization, enrollment binding, or a token exchange ceremony.
- Do not claim compile-time embedded tokens are unrecoverable from the executable; possession of the executable remains a trust boundary.
- Do not change Client Manager packaging or plugin-declared client-manager build semantics.
- Do not modify billing, cloud host sales, AI provider, plugin marketplace, or unrelated server workflows.
## Decisions
### Decision 1: Compile-time Run identity is a build-input contract
Platform extends authenticated distribution build input with the public Run platform URL and the existing identity fields. The trusted Run worker passes those values to `go build -ldflags -X browser.local/run/config.<Var>=<value>`. Run config loading prefers explicit environment variables, then compile-time build values, then developer defaults.
This uses the Go-native mechanism the user requested. It avoids `go:embed`, generated source files, temporary code rewrites, or sidecar config files for Run. The compile-time value is still recoverable by someone holding the binary, but it removes the casual ZIP/config leak.
### Decision 2: Raw executable is a Run-only package format
Run distributions use a new `raw-executable` package format. The worker uploads the compiled binary bytes directly and does not call the archive writer for Run. Client-manager distributions keep the existing ZIP/tar.gz packaging and config injection because they are plugin-declared companion builds with their own lifecycle.
### Decision 3: Self-update treats raw executable as first-class
Run update input may carry `raw-executable`. The self-update executor still downloads through the artifact channel, verifies the full artifact checksum, writes the staged executable under the transaction workspace, records its binary checksum, and uses the existing activation and rollback logic.
### Decision 4: Run keys get a dedicated generator
The existing `randomToken()` remains 32 random bytes because it is also used for auth sessions, user ID suffixes, and job leases. Run component keys use a new 64-byte URL-safe generator, increasing key length only for Run authorization.
### Decision 5: Reset revokes the active Run session
The reset service already revokes old distribution artifacts and increments key generation. This change also removes the active Run control session for that server endpoint when the Run component key is reset, forcing a freshly compiled binary to authenticate before more control/job traffic is accepted.
## Risks / Trade-offs
- Embedded authorization can be extracted from a binary by a determined operator or attacker with file access. This is acceptable for the requested distribution model and is explicitly not a DRM or device-binding system.
- Compile-time values can be visible to privileged users on the build worker. The build worker remains part of the trusted platform boundary.
- Raw executables lose the convenience of multi-file package payloads, so any future service installer/systemd wrapper should be a separate explicit distribution profile rather than hidden in this change.
## Verification
- `openspec validate secure-single-file-run-distribution --strict`
- `cd run && go test ./...`
- `cd platform && go test ./...`
- `cd platform_web && npm test`
- `cd platform_web && npm run typecheck`
- `scripts/check-structure.sh`
@@ -0,0 +1,29 @@
## Why
Run distributions currently produce a secret-bearing archive with a sidecar `config.json`, and downloaded Windows packages are ZIP files. Operators need the platform to deliver one server-scoped executable whose platform URL and runtime authorization are compiled into the binary.
## What Changes
- Change platform-managed Run distributions to publish a single raw executable instead of a ZIP/tarball plus `config.json`.
- Inject Run platform URL, worker mode, runtime identity, key generation, and authorization token through Go `-ldflags -X` during the trusted Run build.
- Increase Run component key entropy without changing the global session/job token generator.
- Allow Run self-update jobs to consume raw executable artifacts for Windows and Linux targets.
- Add run-key reset to the server-list "运行操作" dangerous menu, using the existing reset API.
- Revoke the active Run control session when the run key is reset so old deployed binaries stop immediately.
## Capabilities
### New Capabilities
- `secure-single-file-run-distribution`: Server-scoped Run executable distribution, compile-time authorization injection, raw-binary self-update, and list-level run-key reset.
### Modified Capabilities
- None.
## Impact
- Affects `platform/` distribution build input, run-key generation/reset behavior, artifact naming, package-format validation, and tests.
- Affects `run/` config loading, distribution build packaging, self-update extraction, protocol DTOs, and tests.
- Affects `platform_web/` server-list runtime action menu and tests.
- Does not touch plugins, client-manager package format, AI provider flows, billing/cloud features, or unrelated UI systems.
@@ -0,0 +1,73 @@
## ADDED Requirements
### Requirement: Platform publishes single-file Run executables
The platform SHALL publish Run distributions as one raw executable per server, target OS, and architecture instead of an archive containing a sidecar configuration file.
#### Scenario: Operator generates Windows Run
- **WHEN** an authorized operator generates Run for a Windows target
- **THEN** platform MUST queue a distribution build job that uploads a single executable artifact and presents it as `run-windows-<arch>.exe` with `application/octet-stream`
- **AND** the artifact MUST NOT contain a downloadable `config.json` sidecar
#### Scenario: Operator generates Linux Run
- **WHEN** an authorized operator generates Run for a Linux target
- **THEN** platform MUST queue a distribution build job that uploads a single executable artifact and presents it as `run-linux-<arch>` with `application/octet-stream`
### Requirement: Run authorization is compiled into the executable
Run distribution builds SHALL inject server-scoped identity and the active Run authorization key into the executable at Go build time.
#### Scenario: Build input is consumed by trusted Run builder
- **WHEN** a Run worker receives authenticated distribution build input for component kind `run`
- **THEN** it MUST compile the target with Go `-ldflags -X` values for worker mode, platform URL, run endpoint ID, server instance ID, plugin ID, component kind, key generation, target release, and authorization token
- **AND** explicit runtime environment variables such as `RUN_PLATFORM_URL` MUST remain able to override compiled defaults for local development
#### Scenario: Compiled executable starts without sidecar config
- **WHEN** the generated Run executable starts with no `config.json`
- **THEN** it MUST load the compiled identity and key, register as a worker by default, and authenticate against the current platform key generation
### Requirement: Run keys use increased entropy
Platform SHALL generate Run component keys with more entropy than general-purpose platform tokens while preserving existing global token behavior.
#### Scenario: Run key is created or reset
- **WHEN** platform creates or resets a Run component key
- **THEN** it MUST use a dedicated Run key generator of at least 64 random bytes before URL-safe encoding
- **AND** it MUST NOT change auth session or job lease token generation
### Requirement: Raw executable self-update is supported
Run self-update SHALL accept raw executable artifacts for supported Run targets.
#### Scenario: Raw update artifact is staged
- **WHEN** Run receives a self-update job whose package format is `raw-executable`
- **THEN** Run MUST download the artifact through the artifact channel, verify the full checksum, stage it as an executable file, record the staged binary checksum, and use the existing activation/rollback flow
#### Scenario: Unsupported update package is requested
- **WHEN** Run receives a self-update job with an unsupported package format
- **THEN** Run MUST reject the update before activation and keep the current executable
### Requirement: Server list exposes run-key reset
The server-list runtime action menu SHALL expose run-key reset as a destructive runtime operation when the platform reports it available.
#### Scenario: Operator resets from server list
- **WHEN** an authorized operator chooses run-key reset from the server-list `运行操作` menu and confirms the destructive action
- **THEN** platform_web MUST call the existing run-key reset API, show a tracked operation result, refresh server state, and indicate that a new Run executable must be generated
### Requirement: Run key reset revokes active control sessions
Resetting a Run key SHALL immediately invalidate the active Run control session for the server's assigned Run endpoint.
#### Scenario: Online Run key is reset
- **WHEN** platform successfully resets the Run key for a server instance with an active Run control session
- **THEN** platform MUST remove that session so old deployed executables cannot continue heartbeat or job traffic under the stale key
@@ -0,0 +1,47 @@
## Prompt Boundaries
正向提示词: Deliver a secure single-file Run distribution flow for 服务器管理, where generated Run artifacts are raw executables with compile-time platform URL and authorization, self-update accepts raw executables, and server-list run-key reset is available with confirmation.
方向提示词: Preserve existing platform/run/platform_web boundaries; use platform distribution build input and Run Go `-ldflags -X` injection; keep Client Manager packaging unchanged; verify with focused Go/frontend tests, OpenSpec validation, and `scripts/check-structure.sh`.
任务边界: Do not add device authorization binding, billing, cloud host sales, AI-provider changes, plugin marketplace expansion, Client Manager packaging changes, raw credential exposure in APIs/UI, or unrelated visual-system changes.
## 1. OpenSpec
- [x] 1.1 Define proposal, design, requirements, and task boundaries for single-file Run distribution.
- [x] 1.2 Validate the OpenSpec change before implementation completion.
## 2. Platform
- [x] 2.1 Add Run-only raw executable package format, artifact filename/content-type presentation, and build-input platform URL.
- [x] 2.2 Generate longer Run component keys without changing global token generation.
- [x] 2.3 Revoke active Run control sessions when Run key reset succeeds.
- [x] 2.4 Update platform tests for raw Run artifacts, key length, build input, reset revocation, and self-update input.
## 3. Run
- [x] 3.1 Add compile-time config variables with environment override precedence.
- [x] 3.2 Inject Run identity and authorization through Go `-ldflags -X` and upload raw executable bytes for Run builds.
- [x] 3.3 Support raw executable self-update staging while preserving archive handling for compatibility/tests where needed.
- [x] 3.4 Update Run tests for raw build output, compiled smoke identity, config precedence, and raw self-update.
## 4. platform_web
- [x] 4.1 Add run-key reset to the server-list `运行操作` dangerous menu with confirmation and API execution.
- [x] 4.2 Update frontend tests and fixtures for raw Run download metadata and reset action coverage.
## 5. Verification
- [x] 5.1 Run focused `run`, `platform`, and `platform_web` verification.
- [x] 5.2 Run `scripts/check-structure.sh`.
## Verification Evidence
- `openspec validate secure-single-file-run-distribution --strict`: passed
- `cd platform && go test ./service -run 'TestCoreService(ResetRunKey|GeneratesRun|RunDistribution|DistributionBuild)' -count=1`: passed
- `cd platform && go test ./validator ./domain ./dto -count=1`: passed
- `cd run && go test ./config ./protocol ./runtime -run 'TestLoad|TestWorkerDistribution|TestDistributionBuild|TestValidateDistribution|TestRunSelfUpdate|TestPrepareSelfUpdate|TestWorkerDispatchesSelfUpdate' -count=1`: passed
- `cd platform_web && npm test -- --run pages/ConsolePages.test.tsx api/client.test.ts`: passed, 2 files / 25 tests
- `scripts/check-structure.sh`: passed
- Note: full `run/runtime` suite still needs network bind permissions for unrelated Source RCON/httptest fixtures; focused distribution/self-update coverage was used for this change.
+1
View File
@@ -116,6 +116,7 @@ type DistributionBuildInput struct {
TargetOS string
TargetArch string
TargetRelease string
PlatformURL string
PackageFormat string
RepositoryURL string
SourceRevision string
+2
View File
@@ -153,6 +153,7 @@ type DistributionBuildInputResponse struct {
TargetOS string `json:"targetOs"`
TargetArch string `json:"targetArch"`
TargetRelease string `json:"targetRelease"`
PlatformURL string `json:"platformUrl,omitempty"`
PackageFormat string `json:"packageFormat"`
RepositoryURL string `json:"repositoryUrl,omitempty"`
SourceRevision string `json:"sourceRevision,omitempty"`
@@ -468,6 +469,7 @@ func DistributionBuildInputFromDomain(input domain.DistributionBuildInput) Distr
TargetOS: input.TargetOS,
TargetArch: input.TargetArch,
TargetRelease: input.TargetRelease,
PlatformURL: input.PlatformURL,
PackageFormat: input.PackageFormat,
RepositoryURL: input.RepositoryURL,
SourceRevision: input.SourceRevision,
+8
View File
@@ -250,6 +250,12 @@ func distributionPackageFilename(base string, targetOS string, targetArch string
if arch != "" {
name += "-" + arch
}
if format == "raw-executable" {
if os == "windows" {
return name + ".exe"
}
return name
}
if format == "" {
format = "bin"
}
@@ -262,6 +268,8 @@ func packageContentType(packageFormat string) string {
return "application/zip"
case "tar.gz", "tgz":
return "application/gzip"
case "raw-executable":
return "application/octet-stream"
default:
return "application/octet-stream"
}
+43
View File
@@ -210,6 +210,49 @@ func (svc *CoreService) currentRunSession(runEndpointID string, sessionToken str
return domain.CopyRunControlSession(session), nil
}
func (svc *CoreService) revokeRunControlSessionForInstance(instance domain.ServerInstance) error {
if strings.TrimSpace(instance.RunEndpointID) == "" {
return nil
}
svc.controlMu.Lock()
defer svc.controlMu.Unlock()
session, err := svc.store.RunControlSessions().Get(instance.RunEndpointID)
if err != nil {
if errors.Is(err, repo.ErrNotFound) {
delete(svc.runSessions, instance.RunEndpointID)
return nil
}
return err
}
if session.Status == domain.AuthSessionStatusActive {
stamp := svc.now()
session.Status = domain.AuthSessionStatusRevoked
session.RevokedAt = stamp
session.UpdatedAt = stamp
if err := validator.ValidateRunControlSession(session); err != nil {
return err
}
if err := svc.store.RunControlSessions().Update(session); err != nil {
return err
}
}
delete(svc.runSessions, instance.RunEndpointID)
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
if err != nil {
if errors.Is(err, repo.ErrNotFound) {
return nil
}
return err
}
endpoint.Status = domain.RunEndpointStatusOffline
if err := validator.ValidateRunEndpoint(endpoint); err != nil {
return err
}
return svc.store.RunEndpoints().Update(endpoint)
}
func runAuthenticationError(requireSigned bool) error {
if !requireSigned {
return validationError("sessionToken is invalid")
@@ -60,6 +60,7 @@ func (svc *CoreService) GetDistributionBuildInput(request domain.DistributionBui
TargetOS: distribution.TargetOS,
TargetArch: distribution.TargetArch,
TargetRelease: distribution.ID,
PlatformURL: runReleasePlatformURL(),
PackageFormat: distribution.PackageFormat,
ArtifactID: distribution.ArtifactID,
OutputFilename: executableFilename("run", distribution.TargetOS),
+22 -1
View File
@@ -6,6 +6,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"os"
"strings"
"browser.local/platform/domain"
@@ -73,7 +74,7 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
RunEndpointID: instance.RunEndpointID,
TargetOS: request.TargetOS,
TargetArch: request.TargetArch,
PackageFormat: packageFormatForTarget(request.TargetOS),
PackageFormat: runPackageFormatForTarget(request.TargetOS),
BuildJobID: buildJobID,
ArtifactID: artifactID,
KeyGeneration: key.Generation,
@@ -374,6 +375,11 @@ func (svc *CoreService) ResetComponentKeyForSession(sessionID string, request do
return domain.EncryptedComponentKey{}, err
}
}
if request.ComponentKind == domain.DistributionComponentRun {
if err := svc.revokeRunControlSessionForInstance(instance); err != nil {
return domain.EncryptedComponentKey{}, err
}
}
if err := svc.recordAuditEvent(user.ID, "runtime-key.reset", "server-instance", instance.ID, domain.AuditResultSuccess, "reset "+string(request.ComponentKind)+" key; previous packages revoked"); err != nil {
return domain.EncryptedComponentKey{}, err
}
@@ -773,6 +779,9 @@ func (svc *CoreService) activeComponentKey(serverInstanceID string, kind domain.
func (svc *CoreService) createEncryptedComponentKey(serverInstanceID string, kind domain.DistributionComponentKind, componentKey string, generation int) (domain.EncryptedComponentKey, string, error) {
plainKey, err := randomToken()
if kind == domain.DistributionComponentRun {
plainKey, err = randomRunComponentKey()
}
if err != nil {
return domain.EncryptedComponentKey{}, "", err
}
@@ -1057,6 +1066,11 @@ func validatePluginTarget(plugin domain.GamePlugin, targetOS string) error {
return validationError("targetOs is not declared by plugin")
}
func runPackageFormatForTarget(targetOS string) string {
_ = targetOS
return "raw-executable"
}
func packageFormatForTarget(targetOS string) string {
if targetOS == "windows" {
return "zip"
@@ -1064,6 +1078,13 @@ func packageFormatForTarget(targetOS string) string {
return "tar.gz"
}
func runReleasePlatformURL() string {
if value := strings.TrimSpace(os.Getenv("PLATFORM_RUN_RELEASE_URL")); value != "" {
return value
}
return "https://scum.npc0.com"
}
func distributionID(prefix string, parts ...interface{}) string {
values := make([]string, 0, len(parts))
for _, part := range parts {
+53 -4
View File
@@ -35,7 +35,7 @@ func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing
if err != nil {
t.Fatalf("generate run distribution: %v", err)
}
if distribution.KeyGeneration != 1 || distribution.SecretRef == "" || distribution.Status != domain.DistributionStatusBuilding || distribution.BuildJobID == "" || distribution.Checksum != "" {
if distribution.KeyGeneration != 1 || distribution.SecretRef == "" || distribution.PackageFormat != "raw-executable" || distribution.Status != domain.DistributionStatusBuilding || distribution.BuildJobID == "" || distribution.Checksum != "" {
t.Fatalf("unexpected run distribution: %+v", distribution)
}
job, err := svc.GetJob(distribution.BuildJobID)
@@ -62,6 +62,9 @@ func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing
if config.AuthKey == "" || config.AuthKey == keys[0].EncryptedKey || strings.Contains(distribution.SecretRef, config.AuthKey) {
t.Fatalf("run package key leaked through metadata or was not encrypted, config=%+v key=%+v distribution=%+v", config, keys[0], distribution)
}
if len(config.AuthKey) < 80 {
t.Fatalf("expected longer run component key, got length %d", len(config.AuthKey))
}
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
ServerInstanceID: instance.ID,
ComponentKind: domain.DistributionComponentRun,
@@ -159,6 +162,7 @@ func TestCoreServiceRuntimeActionsGateDependenciesOnPluginPermission(t *testing.
}
func TestCoreServiceDistributionBuildRejectsPrematureSuccessAndCanRetryAfterUpload(t *testing.T) {
t.Setenv("PLATFORM_RUN_RELEASE_URL", "https://scum.npc0.com")
svc, session, instance := newDistributionTestFixture(t)
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
@@ -185,6 +189,19 @@ func TestCoreServiceDistributionBuildRejectsPrematureSuccessAndCanRetryAfterUplo
if err != nil || !claim.HasJob || claim.Job.JobID != distribution.BuildJobID {
t.Fatalf("claim distribution build job: claim=%+v err=%v", claim, err)
}
buildInput, err := svc.GetDistributionBuildInput(domain.DistributionBuildInputRequest{
RunEndpointID: claim.Job.RunEndpointID,
SessionToken: hello.SessionToken,
JobID: claim.Job.JobID,
LeaseToken: claim.Job.LeaseToken,
Attempt: claim.Job.Attempt,
})
if err != nil {
t.Fatalf("get distribution build input: %v", err)
}
if buildInput.PlatformURL != "https://scum.npc0.com" || buildInput.PackageFormat != "raw-executable" || buildInput.AuthKey == "" || len(buildInput.AuthKey) < 80 {
t.Fatalf("expected raw executable build input with release URL and long key, got %+v", buildInput)
}
result := domain.RunJobResult{
RunEndpointID: instance.RunEndpointID,
SessionToken: hello.SessionToken,
@@ -227,14 +244,14 @@ func TestCoreServiceRunDistributionDownloadUsesTargetPackageName(t *testing.T) {
if err != nil {
t.Fatalf("generate run distribution: %v", err)
}
payload := []byte("windows zipped run package")
payload := []byte("windows raw run executable")
completeDistributionBuild(t, svc, distribution, payload)
reference, err := svc.OpenArtifactDownloadForSession(session, domain.ArtifactDownloadReferenceRequest{ArtifactID: distribution.ArtifactID})
if err != nil {
t.Fatalf("open run artifact download: %v", err)
}
if reference.Filename != "run-windows-amd64.zip" || reference.ContentType != "application/zip" {
if reference.Filename != "run-windows-amd64.exe" || reference.ContentType != "application/octet-stream" {
t.Fatalf("expected windows run package metadata, got %+v", reference)
}
@@ -275,7 +292,7 @@ func TestCoreServiceRunDistributionRetryReusesPartialArtifact(t *testing.T) {
if distribution.ID != distributionID || distribution.ArtifactID == partialArtifact.ID || distribution.Checksum != "" {
t.Fatalf("expected real binary build to ignore legacy config artifact, distribution=%+v artifact=%+v", distribution, partialArtifact)
}
if distribution.PackageFormat != "zip" || distribution.Status != domain.DistributionStatusBuilding || distribution.BuildJobID == "" {
if distribution.PackageFormat != "raw-executable" || distribution.Status != domain.DistributionStatusBuilding || distribution.BuildJobID == "" {
t.Fatalf("unexpected recovered distribution: %+v", distribution)
}
recoveredConfig := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
@@ -332,6 +349,17 @@ func TestCoreServiceResetRunKeyRevokesOldPackagesAndRequiresRegeneration(t *test
}
oldConfig := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
distribution = completeDistributionBuild(t, svc, distribution, []byte("compiled run archive before reset"))
helloRequest := validRunControlHello()
helloRequest.RunEndpointID = instance.RunEndpointID
helloRequest.RegistrationToken = oldConfig.AuthKey
helloRequest.ServerInstanceID = instance.ID
helloRequest.PluginID = instance.PluginID
helloRequest.ComponentKind = domain.DistributionComponentRun
helloRequest.KeyGeneration = oldConfig.KeyGeneration
hello, err := svc.RegisterRunHello(helloRequest)
if err != nil || !hello.Accepted {
t.Fatalf("register old run before reset: hello=%+v err=%v", hello, err)
}
reset, err := svc.ResetComponentKeyForSession(session, domain.ComponentKeyResetRequest{
ServerInstanceID: instance.ID,
@@ -343,6 +371,27 @@ func TestCoreServiceResetRunKeyRevokesOldPackagesAndRequiresRegeneration(t *test
if reset.Generation != 2 || reset.Status != domain.ComponentKeyStatusActive {
t.Fatalf("expected reset key generation 2, got %+v", reset)
}
if _, err := svc.AcceptRunHeartbeat(domain.RunControlHeartbeat{
RunEndpointID: instance.RunEndpointID,
SessionToken: hello.SessionToken,
Status: domain.RunEndpointStatusOnline,
Version: "stale-run",
Capacity: domain.RunCapacity{MaxJobs: 1},
CapabilityFingerprint: helloRequest.CapabilityReport.Fingerprint,
}); err == nil {
t.Fatal("expected reset to revoke the active run control session")
}
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
if err != nil {
t.Fatalf("get endpoint after reset: %v", err)
}
if endpoint.Status != domain.RunEndpointStatusOffline {
t.Fatalf("expected reset endpoint to be marked offline, got %+v", endpoint)
}
endpoint.Status = domain.RunEndpointStatusOnline
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("restore endpoint for regeneration: %v", err)
}
oldDistribution, err := svc.store.RunDistributions().Get(distribution.ID)
if err != nil {
t.Fatalf("get old distribution: %v", err)
+8
View File
@@ -2401,6 +2401,14 @@ func randomToken() (string, error) {
return base64.RawURLEncoding.EncodeToString(token), nil
}
func randomRunComponentKey() (string, error) {
token := make([]byte, 64)
if _, err := rand.Read(token); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(token), nil
}
func verifyPassword(hash string, password string) bool {
parts := strings.Split(hash, "$")
if len(parts) != 4 || parts[0] != "pbkdf2-sha256" {
+1 -1
View File
@@ -110,7 +110,7 @@ func ValidateRunDistribution(distribution domain.RunDistribution) error {
if distribution.KeyGeneration <= 0 {
violations = append(violations, "keyGeneration must be positive")
}
if distribution.PackageFormat != "zip" && distribution.PackageFormat != "tar.gz" {
if distribution.PackageFormat != "zip" && distribution.PackageFormat != "tar.gz" && distribution.PackageFormat != "raw-executable" {
violations = append(violations, "packageFormat is invalid")
}
if !strings.HasPrefix(distribution.SecretRef, "secret://runtime-keys/") {
+3 -3
View File
@@ -132,8 +132,8 @@ const runtimeDownload: ArtifactDownloadReferenceResponse = {
artifactId: "artifact-run-1",
ownerKind: "server-instance",
ownerId: server.id,
filename: "run-linux-amd64.zip",
contentType: "application/zip",
filename: "run-linux-amd64",
contentType: "application/octet-stream",
sizeBytes: 128,
checksum: "sha256:runchecksum",
state: "available",
@@ -398,7 +398,7 @@ describe("PlatformApiClient AI providers", () => {
runEndpointId: endpoint.id,
targetOs: "linux",
targetArch: "amd64",
packageFormat: "zip",
packageFormat: "raw-executable",
artifactId: "artifact-run-1",
checksum: "sha256:runchecksum",
keyGeneration: 1,
@@ -77,6 +77,13 @@ export const runtimeUpdateStages: RuntimeTaskStage[] = [
{ key: "job_track", label: "等待确认", description: "记录 job id 并刷新后台任务状态。" }
];
export const runtimeKeyResetStages: RuntimeTaskStage[] = [
{ key: "scope_check", label: "权限校验", description: "确认当前账号可以重置该服务器的 run 密钥。" },
{ key: "confirm_reset", label: "确认重置", description: "记录本次密钥轮换并撤销旧 run 会话。" },
{ key: "key_rotate", label: "轮换密钥", description: "生成新密钥代际并撤销旧发行物。" },
{ key: "regenerate_hint", label: "等待重发", description: "提示重新生成并部署新的 run 可执行文件。" }
];
export const runtimeDependencyStages: RuntimeTaskStage[] = [
{ key: "profile_read", label: "读取声明", description: "读取插件声明的 probe 和 install plan。" },
{ key: "env_probe", label: "环境检查", description: "让 run 节点评估当前运行环境。" },
+8
View File
@@ -159,6 +159,14 @@ describe("first-party console pages", () => {
expect(serversPageSource).toContain("onClick={() => void refreshMetrics()}");
});
it("exposes run key reset from the compact server list danger menu", () => {
expect(serversPageSource).toContain("reset-run-key");
expect(serversPageSource).toContain("重置 run 密钥");
expect(serversPageSource).toContain("runtimeKeyResetStages");
expect(serversPageSource).toContain("platformApiClient.resetRunKey");
expect(serversPageSource).toContain("旧 run 会话已失效");
});
it("submits declared runtime profiles and logical bindings from the create workflow", () => {
expect(serversPageSource).toContain("<ManagementDialog");
expect(serversPageSource).toContain('className="provider-form dialog-form"');
+31
View File
@@ -10,6 +10,7 @@ import {
runtimeBuildStages,
runtimeDependencyStages,
runtimeDownloadStages,
runtimeKeyResetStages,
runtimeLogStages,
runtimeRunBuildStages,
runtimeUpdateStages,
@@ -271,6 +272,12 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
openRunTargetSelection(instance);
return;
}
if (action === "reset-run-key") {
const confirmed = typeof window === "undefined" || window.confirm("重置 run 密钥会立刻断开已部署 run,并要求重新生成新的 run 可执行文件。继续?");
if (!confirmed) {
return;
}
}
const defaults = quickRuntimeDefaultsForPlugin(instance.pluginId);
const intent = quickRuntimeActionLabel(action);
const operationId = operations.begin({ intent, targetKind: "server", targetId: `${instance.id}:${action}`, requester: session.displayName });
@@ -319,6 +326,10 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
const update = await platformApiClient.pushRunUpdate(instance.id, runUpdateRequest(instance.id, reference.artifactId, reference.checksum));
return `run 更新任务已排队,job ${update.jobId ?? update.id}`;
}
if (action === "reset-run-key") {
const key = await platformApiClient.resetRunKey(instance.id);
return `run 密钥已重置到第 ${key.generation} 代,旧 run 会话已失效,请重新生成并部署 run`;
}
if (action === "dependencies-check") {
const job = await platformApiClient.checkDependencies(instance.id, dependencyJobRequest(instance.id, defaults.probeKey));
return `依赖检查任务已排队,job ${job.id}`;
@@ -691,6 +702,7 @@ type ServerQuickRuntimeAction =
| "generate-run"
| "download-run"
| "push-run-update"
| "reset-run-key"
| "generate-client-manager"
| "dependencies-check"
| "dependencies-install"
@@ -873,6 +885,17 @@ function ServerCard({ card, metricsPending, metricsUnavailable, canManage, delet
<section className="runtime-action-group" aria-label="危险操作">
<span className="runtime-action-group-label"></span>
<div className="runtime-action-grid">
<button
type="button"
className="runtime-action-item danger-command"
role="menuitem"
disabled={!canManage}
title={canManage ? "重置 run 密钥" : "当前账号没有运行操作权限"}
onClick={() => chooseQuickAction("reset-run-key")}
>
<AlertTriangle size={13} />
<span> run </span>
</button>
<button
type="button"
className="runtime-action-item danger-command"
@@ -915,6 +938,8 @@ function quickRuntimeActionLabel(action: ServerQuickRuntimeAction): string {
return "下载 run";
case "push-run-update":
return "更新 run";
case "reset-run-key":
return "重置 run 密钥";
case "generate-client-manager":
return "生成客户端";
case "dependencies-check":
@@ -941,6 +966,9 @@ function quickRuntimeStages(action: ServerQuickRuntimeAction) {
if (action === "push-run-update") {
return runtimeUpdateStages;
}
if (action === "reset-run-key") {
return runtimeKeyResetStages;
}
if (action === "dependencies-check" || action === "dependencies-install") {
return runtimeDependencyStages;
}
@@ -954,6 +982,9 @@ function quickRuntimeExecuteStageIndex(action: ServerQuickRuntimeAction): number
if (action === "push-run-update") {
return 2;
}
if (action === "reset-run-key") {
return 2;
}
return 1;
}