DB-backed repo_path_allowlist reader (deny-all until admin seeds), per-prefix admin test_commands (goose 00075), execution evidence table (goose 00074), isolated git-worktree patch executor with command allowlist/time/output caps and failure-stops evidence; controlled_patch creation/preview unlocked only with an allowlisted repo binding (projectworkflow gate + catalog flag); creator-only server endpoints for bindings/executions; merge/push/deploy stay behind a typed separate-authorization boundary. Module boundary: app->swboot composition import registered.
287 lines
12 KiB
Go
287 lines
12 KiB
Go
package swboot
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Product domains and enumerations (SWBOOT-00 contract).
|
|
const (
|
|
DomainNonMedicalSoftware = "non_medical_software"
|
|
DomainMedicalDevice = "medical_device"
|
|
|
|
ProjectTypeGreenfield = "greenfield"
|
|
ProjectTypeExistingProduct = "existing_product"
|
|
ProjectTypeLegacyModernization = "legacy_modernization"
|
|
ProjectTypeExistingSelfBootstrap = "existing_self_bootstrap"
|
|
|
|
GovernanceLean = "lean"
|
|
GovernanceStandard = "standard"
|
|
GovernanceControlled = "controlled"
|
|
|
|
TeamModeSolo = "solo"
|
|
TeamModeCollaborative = "collaborative"
|
|
|
|
ExecutionDocsOnly = "docs_only"
|
|
ExecutionReadOnlyRepo = "read_only_repo"
|
|
ExecutionControlledPatch = "controlled_patch"
|
|
|
|
ApprovalIndependentThreeDuties = "independent_three_duties"
|
|
ApprovalCreatorAcknowledgement = "creator_acknowledgement"
|
|
)
|
|
|
|
// ProjectTypeCatalogItem is a create-wizard selectable project type.
|
|
type ProjectTypeCatalogItem struct {
|
|
Key string `json:"key"`
|
|
Name string `json:"name"`
|
|
ProductDomain string `json:"product_domain"`
|
|
Description string `json:"description"`
|
|
DefaultGovernance string `json:"default_governance"`
|
|
DefaultWorkflowKey string `json:"default_workflow_key"`
|
|
RecommendedWorkflows []string `json:"recommended_workflows"`
|
|
}
|
|
|
|
// ComplianceProfileCatalogItem is a create-wizard compliance option.
|
|
type ComplianceProfileCatalogItem struct {
|
|
Key string `json:"key"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
RequiresIndependentReview bool `json:"requires_independent_review"`
|
|
}
|
|
|
|
// CreationPreviewInput is the seven-step wizard confirmation payload.
|
|
type CreationPreviewInput struct {
|
|
Name string `json:"name"`
|
|
Code string `json:"code,omitempty"`
|
|
ProductDomain string `json:"product_domain"`
|
|
ProjectTypeKey string `json:"project_type_key"`
|
|
ProductShape string `json:"product_shape,omitempty"`
|
|
ProblemStatement string `json:"problem_statement"`
|
|
TargetUsers string `json:"target_users"`
|
|
TargetOutcome string `json:"target_outcome"`
|
|
GovernanceProfile string `json:"governance_profile"`
|
|
TeamMode string `json:"team_mode"`
|
|
ComplianceProfileKey string `json:"compliance_profile_key"`
|
|
DataSensitivity string `json:"data_sensitivity,omitempty"`
|
|
NetworkPolicy string `json:"network_policy,omitempty"`
|
|
ExecutionMode string `json:"execution_mode"`
|
|
// RepoAllowlisted is set by the transport layer after checking the admin
|
|
// repo_path_allowlist for RepoPath; controlled_patch is only valid when a
|
|
// bound repo path passed that check (fail-closed otherwise).
|
|
RepoAllowlisted bool `json:"repo_allowlisted,omitempty"`
|
|
RepoPath string `json:"repo_path,omitempty"`
|
|
BudgetClass string `json:"budget_class,omitempty"`
|
|
InitialWorkflowKey string `json:"initial_workflow_key,omitempty"`
|
|
CatalogVersionHint string `json:"expected_catalog_version,omitempty"`
|
|
}
|
|
|
|
// CreationPreviewResult is returned by POST /projects/creation-preview.
|
|
type CreationPreviewResult struct {
|
|
CreationToken string `json:"creation_token"`
|
|
PlanHash string `json:"plan_hash"`
|
|
SourceHash string `json:"source_hash"`
|
|
ExpectedCatalogVersion string `json:"expected_catalog_version"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
ApprovalPolicy string `json:"approval_policy"`
|
|
RecommendedWorkflow string `json:"recommended_workflow"`
|
|
Warnings []string `json:"warnings,omitempty"`
|
|
Issues []string `json:"issues,omitempty"`
|
|
Valid bool `json:"valid"`
|
|
}
|
|
|
|
// ProjectTypesCatalog returns seeded software project types.
|
|
// Wave 5 X-3: goose 00048 tables project_types / project_type_workflows exist with
|
|
// seed rows, but HTTP catalog still serves this hard-coded list. DB-backed
|
|
// consumption is deferred to SWBOOT-10 / W5b. See
|
|
// .doc/ops/short-term/2026-09-18-wave5-deadcode-decisions.md#x-3.
|
|
func ProjectTypesCatalog() []ProjectTypeCatalogItem {
|
|
return []ProjectTypeCatalogItem{
|
|
{
|
|
Key: ProjectTypeGreenfield, Name: "绿地立项", ProductDomain: DomainNonMedicalSoftware,
|
|
Description: "从零规划新软件", DefaultGovernance: GovernanceStandard,
|
|
DefaultWorkflowKey: "software_bootstrap", RecommendedWorkflows: []string{"software_bootstrap", "software_delivery"},
|
|
},
|
|
{
|
|
Key: ProjectTypeExistingProduct, Name: "既有产品梳理", ProductDomain: DomainNonMedicalSoftware,
|
|
Description: "梳理已有产品与仓库", DefaultGovernance: GovernanceStandard,
|
|
DefaultWorkflowKey: "software_bootstrap", RecommendedWorkflows: []string{"software_bootstrap", "software_delivery"},
|
|
},
|
|
{
|
|
Key: ProjectTypeLegacyModernization, Name: "遗留现代化", ProductDomain: DomainNonMedicalSoftware,
|
|
Description: "遗留系统改造与迁移", DefaultGovernance: GovernanceStandard,
|
|
DefaultWorkflowKey: "software_bootstrap", RecommendedWorkflows: []string{"software_bootstrap", "software_delivery"},
|
|
},
|
|
{
|
|
Key: ProjectTypeExistingSelfBootstrap, Name: "系统自举", ProductDomain: DomainNonMedicalSoftware,
|
|
Description: "用 Agent-System 梳理并建设自身", DefaultGovernance: GovernanceStandard,
|
|
DefaultWorkflowKey: "software_bootstrap", RecommendedWorkflows: []string{"software_bootstrap", "software_delivery"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ComplianceProfilesCatalog returns seeded compliance profiles.
|
|
// Wave 5 X-3: goose 00048 table compliance_profiles is seeded but unused at
|
|
// runtime; this hard-coded catalog remains authoritative until SWBOOT-10 / W5b.
|
|
// See .doc/ops/short-term/2026-09-18-wave5-deadcode-decisions.md#x-3.
|
|
func ComplianceProfilesCatalog() []ComplianceProfileCatalogItem {
|
|
return []ComplianceProfileCatalogItem{
|
|
{Key: "none", Name: "无外部合规预设", Description: "不预设外部法规步骤;鉴权/审计/安全基线仍生效"},
|
|
{Key: "internal_control", Name: "内部控制", Description: "内部流程、权限与审计约束"},
|
|
{Key: "security_baseline", Name: "安全基线", Description: "威胁建模、依赖与 secret 边界"},
|
|
{Key: "privacy", Name: "隐私保护", Description: "数据分类与最小权限", RequiresIndependentReview: true},
|
|
{Key: "custom", Name: "自定义合规", Description: "项目自定义合规包", RequiresIndependentReview: true},
|
|
{Key: "internal_control+security_baseline", Name: "内部控制+安全基线", Description: "首期推荐组合"},
|
|
}
|
|
}
|
|
|
|
// BuildCreationPreview validates wizard input and issues a short-lived token.
|
|
func BuildCreationPreview(in CreationPreviewInput, now time.Time) CreationPreviewResult {
|
|
res := CreationPreviewResult{
|
|
Valid: true,
|
|
ExpectedCatalogVersion: strings.TrimSpace(in.CatalogVersionHint),
|
|
ExpiresAt: now.Add(15 * time.Minute),
|
|
SourceHash: "sha256:empty",
|
|
}
|
|
if res.ExpectedCatalogVersion == "" {
|
|
res.ExpectedCatalogVersion = "software-v1"
|
|
}
|
|
|
|
name := strings.TrimSpace(in.Name)
|
|
if name == "" {
|
|
res.Valid = false
|
|
res.Issues = append(res.Issues, "name is required")
|
|
}
|
|
domain := strings.TrimSpace(in.ProductDomain)
|
|
if domain == "" {
|
|
res.Valid = false
|
|
res.Issues = append(res.Issues, "product_domain is required")
|
|
}
|
|
if domain != "" && domain != DomainNonMedicalSoftware && domain != DomainMedicalDevice {
|
|
res.Valid = false
|
|
res.Issues = append(res.Issues, "unsupported product_domain")
|
|
}
|
|
ptype := strings.TrimSpace(in.ProjectTypeKey)
|
|
if domain == DomainNonMedicalSoftware {
|
|
if !isKnownProjectType(ptype) {
|
|
res.Valid = false
|
|
res.Issues = append(res.Issues, "project_type_key is required for non_medical_software")
|
|
}
|
|
if strings.TrimSpace(in.ProblemStatement) == "" || strings.TrimSpace(in.TargetUsers) == "" || strings.TrimSpace(in.TargetOutcome) == "" {
|
|
res.Valid = false
|
|
res.Issues = append(res.Issues, "problem_statement, target_users and target_outcome are required")
|
|
}
|
|
}
|
|
|
|
gov := strings.TrimSpace(in.GovernanceProfile)
|
|
if gov == "" {
|
|
gov = GovernanceStandard
|
|
}
|
|
if gov != GovernanceLean && gov != GovernanceStandard && gov != GovernanceControlled {
|
|
res.Valid = false
|
|
res.Issues = append(res.Issues, "invalid governance_profile")
|
|
}
|
|
|
|
team := strings.TrimSpace(in.TeamMode)
|
|
if team == "" {
|
|
team = TeamModeCollaborative
|
|
}
|
|
compliance := strings.TrimSpace(in.ComplianceProfileKey)
|
|
if compliance == "" {
|
|
compliance = "internal_control+security_baseline"
|
|
}
|
|
if requiresIndependentReview(compliance) && team == TeamModeSolo {
|
|
res.Valid = false
|
|
res.Issues = append(res.Issues, "compliance profile requiring independent review forbids solo")
|
|
}
|
|
if gov == GovernanceControlled && team == TeamModeSolo {
|
|
res.Valid = false
|
|
res.Issues = append(res.Issues, "controlled governance forbids solo")
|
|
}
|
|
|
|
execMode := strings.TrimSpace(in.ExecutionMode)
|
|
if execMode == "" {
|
|
execMode = ExecutionDocsOnly
|
|
}
|
|
switch execMode {
|
|
case ExecutionDocsOnly, ExecutionReadOnlyRepo:
|
|
// ok for SWBOOT-00..09
|
|
case ExecutionControlledPatch:
|
|
if !in.RepoAllowlisted || strings.TrimSpace(in.RepoPath) == "" {
|
|
res.Valid = false
|
|
res.Issues = append(res.Issues, "ERR_EXECUTION_MODE_DISABLED: controlled_patch requires an allowlisted repo binding (SWBOOT-10)")
|
|
}
|
|
default:
|
|
res.Valid = false
|
|
res.Issues = append(res.Issues, "invalid execution_mode")
|
|
}
|
|
|
|
wf := strings.TrimSpace(in.InitialWorkflowKey)
|
|
if wf == "" && domain == DomainNonMedicalSoftware {
|
|
wf = "software_bootstrap"
|
|
}
|
|
if domain == DomainNonMedicalSoftware && wf != "" && wf != "software_bootstrap" && wf != "software_delivery" {
|
|
res.Warnings = append(res.Warnings, "non-software workflow selected for non_medical_software project")
|
|
}
|
|
res.RecommendedWorkflow = wf
|
|
if team == TeamModeSolo {
|
|
res.ApprovalPolicy = ApprovalCreatorAcknowledgement
|
|
res.Warnings = append(res.Warnings, "未独立评审:solo 使用 creator_acknowledgement")
|
|
} else {
|
|
res.ApprovalPolicy = ApprovalIndependentThreeDuties
|
|
}
|
|
|
|
payload := map[string]any{
|
|
"name": name, "code": strings.TrimSpace(in.Code),
|
|
"product_domain": domain, "project_type_key": ptype,
|
|
"governance_profile": gov, "team_mode": team,
|
|
"compliance_profile_key": compliance, "execution_mode": execMode,
|
|
"workflow": wf, "catalog": res.ExpectedCatalogVersion,
|
|
"problem_statement": strings.TrimSpace(in.ProblemStatement),
|
|
"target_users": strings.TrimSpace(in.TargetUsers),
|
|
"target_outcome": strings.TrimSpace(in.TargetOutcome),
|
|
}
|
|
raw, _ := json.Marshal(payload)
|
|
sum := sha256.Sum256(raw)
|
|
res.PlanHash = "sha256:" + hex.EncodeToString(sum[:])
|
|
res.CreationToken = fmt.Sprintf("crt_%s", hex.EncodeToString(sum[:8]))
|
|
if !res.Valid {
|
|
res.CreationToken = ""
|
|
res.PlanHash = ""
|
|
}
|
|
return res
|
|
}
|
|
|
|
// DeliverySuggestion is the post-bootstrap recommendation (never auto-activates).
|
|
type DeliverySuggestion struct {
|
|
WorkflowKey string `json:"workflow_key"`
|
|
AutoActivate bool `json:"auto_activate"`
|
|
Summary string `json:"summary"`
|
|
}
|
|
|
|
// SuggestDeliveryAfterBootstrap recommends software_delivery without activating it.
|
|
func SuggestDeliveryAfterBootstrap() DeliverySuggestion {
|
|
return DeliverySuggestion{
|
|
WorkflowKey: "software_delivery",
|
|
AutoActivate: false,
|
|
Summary: "bootstrap 完成后仅建议 software_delivery,须创建者确认后新建实例,禁止自动激活",
|
|
}
|
|
}
|
|
|
|
func isKnownProjectType(key string) bool {
|
|
switch key {
|
|
case ProjectTypeGreenfield, ProjectTypeExistingProduct, ProjectTypeLegacyModernization, ProjectTypeExistingSelfBootstrap:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func requiresIndependentReview(profile string) bool {
|
|
p := strings.ToLower(profile)
|
|
return strings.Contains(p, "privacy") || strings.Contains(p, "custom") || p == "controlled"
|
|
}
|