feat(api): SWBOOT-05..08 wizard hooks and SWBOOT-10 allowlist stub

Add project-types/compliance catalogs, creation-preview, software create
fields, and fail-closed repo allowlist stub without real patch/merge.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-16 15:43:13 +00:00
co-authored by Cursor
parent 909384c335
commit f6034fad37
11 changed files with 636 additions and 8 deletions
@@ -193,6 +193,7 @@ imports:
- squad
- stream
- stream/transport
- swboot
- taskrun
- upload
- workflowdef
@@ -209,6 +210,7 @@ imports:
stream: []
stream/transport:
- stream
swboot: []
taskrun:
- app/uow
- artifactvault
+24 -2
View File
@@ -28,11 +28,11 @@ type memoryCatalog struct {
order []string
}
// NewDefaultCatalog 创建默认工作流目录(内建 main、product_manager、test_manager)
// NewDefaultCatalog 创建默认工作流目录(医疗三 + 软件自举/交付)
func NewDefaultCatalog() Catalog {
c := &memoryCatalog{
definitions: make(map[string]*Definition),
order: []string{"main", "product_manager", "test_manager"},
order: []string{"main", "product_manager", "test_manager", "software_bootstrap", "software_delivery"},
}
c.definitions["main"] = &Definition{
@@ -69,6 +69,28 @@ func NewDefaultCatalog() Catalog {
Enabled: true,
}
c.definitions["software_bootstrap"] = &Definition{
Key: "software_bootstrap",
Name: "软件项目自举流程",
Description: "S0–S4:章程、现状、需求、架构、风险、路线与就绪评审",
StageKey: "S0_boundary",
TaskKey: "software_charter",
OutputDocType: "software_charter",
OutputDocTitle: "项目章程",
Enabled: true,
}
c.definitions["software_delivery"] = &Definition{
Key: "software_delivery",
Name: "软件交付迭代流程",
Description: "D0–D4:变更定义、实施证据、验证、发布与复盘",
StageKey: "D0_change_definition",
TaskKey: "change_brief",
OutputDocType: "change_brief",
OutputDocTitle: "变更简报",
Enabled: true,
}
return c
}
+22 -1
View File
@@ -61,6 +61,14 @@ func (s *Service) CreateProject(ctx context.Context, user *auth.UserAccount, in
}
}
stage := "V0.1_triage"
if strings.TrimSpace(in.ProductDomain) == "non_medical_software" {
stage = "S0_boundary"
if strings.TrimSpace(in.ExecutionMode) == "controlled_patch" {
return nil, fmt.Errorf("%w: controlled_patch requires SWBOOT-10 executor", ErrInvalidInput)
}
}
p := &Project{
Code: code,
Name: name,
@@ -68,7 +76,20 @@ func (s *Service) CreateProject(ctx context.Context, user *auth.UserAccount, in
DeviceType: in.DeviceType,
IntendedUse: in.IntendedUse,
TargetMarket: in.TargetMarket,
CurrentStage: "V0.1_triage",
ProductDomain: strings.TrimSpace(in.ProductDomain),
ProjectTypeKey: strings.TrimSpace(in.ProjectTypeKey),
ComplianceProfileKey: strings.TrimSpace(in.ComplianceProfileKey),
GovernanceProfile: strings.TrimSpace(in.GovernanceProfile),
TeamMode: strings.TrimSpace(in.TeamMode),
ExecutionMode: strings.TrimSpace(in.ExecutionMode),
DataSensitivity: strings.TrimSpace(in.DataSensitivity),
NetworkPolicy: strings.TrimSpace(in.NetworkPolicy),
BudgetClass: strings.TrimSpace(in.BudgetClass),
ProblemStatement: strings.TrimSpace(in.ProblemStatement),
TargetUsers: strings.TrimSpace(in.TargetUsers),
TargetOutcome: strings.TrimSpace(in.TargetOutcome),
ProductShape: strings.TrimSpace(in.ProductShape),
CurrentStage: stage,
Environment: "dev",
Status: "active",
CreatorUserID: user.ID,
+48 -3
View File
@@ -72,7 +72,7 @@ func TestProjectWorkflowService_CreateAndCreatorACL(t *testing.T) {
if err != nil {
t.Fatalf("list keyed workflows: %v", err)
}
if len(keyedWFs) != 3 {
if len(keyedWFs) != 5 {
t.Fatalf("catalog must still mount after keyed create, got %d", len(keyedWFs))
}
@@ -81,8 +81,8 @@ func TestProjectWorkflowService_CreateAndCreatorACL(t *testing.T) {
if err != nil {
t.Fatalf("ListProjectWorkflows failed: %v", err)
}
if len(wfs) != 3 {
t.Fatalf("expected 3 default workflows, got %d", len(wfs))
if len(wfs) != 5 {
t.Fatalf("expected 5 default workflows, got %d", len(wfs))
}
// 4. 非创建人尝试切换工作流 -> 必须被拒绝
@@ -230,3 +230,48 @@ func TestProjectWorkflowService_LineageAndStale(t *testing.T) {
t.Errorf("expected downstream 'doc_test_plan_001', got '%s'", staleUpdates[0].DownstreamDocID)
}
}
func TestCreateSoftwareProject_NoMedicalDefaults(t *testing.T) {
ctx := context.Background()
svc := NewService(NewStore(nil), NewDefaultCatalog())
creator := &auth.UserAccount{ID: 201, Username: "dev", Name: "开发者"}
proj, err := svc.CreateProject(ctx, creator, CreateProjectInput{
Name: "Agent-System 自举",
ProductDomain: "non_medical_software",
ProjectTypeKey: "existing_self_bootstrap",
ComplianceProfileKey: "internal_control+security_baseline",
GovernanceProfile: "standard",
TeamMode: "solo",
ExecutionMode: "docs_only",
ProblemStatement: "建立可信路线",
TargetUsers: "内部团队",
TargetOutcome: "可执行发布路线",
InitialWorkflowKey: "software_bootstrap",
})
if err != nil {
t.Fatalf("CreateProject: %v", err)
}
if proj.ProductDomain != "non_medical_software" || proj.ProjectTypeKey != "existing_self_bootstrap" {
t.Fatalf("domain/type not persisted: %+v", proj)
}
if proj.CurrentStage != "S0_boundary" {
t.Fatalf("expected S0_boundary stage, got %s", proj.CurrentStage)
}
if proj.DeviceType != "" || proj.IntendedUse != "" {
t.Fatalf("software create must not invent medical device fields: device=%q use=%q", proj.DeviceType, proj.IntendedUse)
}
if proj.ActiveWorkflowKey != "" {
t.Fatal("must remain planning without activation")
}
_, err = svc.CreateProject(ctx, creator, CreateProjectInput{
Name: "bad",
ProductDomain: "non_medical_software",
ExecutionMode: "controlled_patch",
})
if err == nil || !errors.Is(err, ErrInvalidInput) {
t.Fatalf("expected controlled_patch rejection, got %v", err)
}
}
+24 -1
View File
@@ -73,15 +73,29 @@ func (s *pgStore) CreateProject(ctx context.Context, p *Project) (*Project, erro
query := `
INSERT INTO projects (
code, name, customer_name, device_type, intended_use, target_market,
product_domain, project_type_key, compliance_profile_key, governance_profile,
team_mode, execution_mode, data_sensitivity, network_policy, budget_class,
problem_statement, target_users, target_outcome, product_shape,
current_stage, environment, status, creator_user_id, lifecycle_status,
active_workflow_key, workflow_selection_version, workflow_plan_revision,
created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
) VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10,
$11, $12, $13, $14, $15,
$16, $17, $18, $19,
$20, $21, $22, $23, $24,
$25, $26, $27,
$28, $29
)
RETURNING id
`
err := s.pool.QueryRow(
ctx, query,
p.Code, p.Name, p.CustomerName, p.DeviceType, p.IntendedUse, p.TargetMarket,
p.ProductDomain, p.ProjectTypeKey, p.ComplianceProfileKey, p.GovernanceProfile,
p.TeamMode, p.ExecutionMode, p.DataSensitivity, p.NetworkPolicy, p.BudgetClass,
p.ProblemStatement, p.TargetUsers, p.TargetOutcome, p.ProductShape,
p.CurrentStage, p.Environment, p.Status, p.CreatorUserID, p.LifecycleStatus,
p.ActiveWorkflowKey, p.WorkflowSelectionVersion, p.WorkflowPlanRevision,
p.CreatedAt, p.UpdatedAt,
@@ -96,6 +110,11 @@ func (s *pgStore) CreateProject(ctx context.Context, p *Project) (*Project, erro
func (s *pgStore) GetProjectByID(ctx context.Context, id int64) (*Project, error) {
query := `
SELECT id, code, name, customer_name, device_type, intended_use, target_market,
COALESCE(product_domain, ''), COALESCE(project_type_key, ''), COALESCE(compliance_profile_key, ''),
COALESCE(governance_profile, ''), COALESCE(team_mode, ''), COALESCE(execution_mode, ''),
COALESCE(data_sensitivity, ''), COALESCE(network_policy, ''), COALESCE(budget_class, ''),
COALESCE(problem_statement, ''), COALESCE(target_users, ''), COALESCE(target_outcome, ''),
COALESCE(product_shape, ''),
current_stage, environment, status, COALESCE(creator_user_id, 1),
COALESCE(lifecycle_status, 'planning'), COALESCE(active_workflow_key, ''),
COALESCE(workflow_selection_version, 1), COALESCE(workflow_plan_revision, 0),
@@ -106,6 +125,10 @@ func (s *pgStore) GetProjectByID(ctx context.Context, id int64) (*Project, error
p := &Project{}
err := s.pool.QueryRow(ctx, query, id).Scan(
&p.ID, &p.Code, &p.Name, &p.CustomerName, &p.DeviceType, &p.IntendedUse, &p.TargetMarket,
&p.ProductDomain, &p.ProjectTypeKey, &p.ComplianceProfileKey,
&p.GovernanceProfile, &p.TeamMode, &p.ExecutionMode,
&p.DataSensitivity, &p.NetworkPolicy, &p.BudgetClass,
&p.ProblemStatement, &p.TargetUsers, &p.TargetOutcome, &p.ProductShape,
&p.CurrentStage, &p.Environment, &p.Status, &p.CreatorUserID,
&p.LifecycleStatus, &p.ActiveWorkflowKey, &p.WorkflowSelectionVersion, &p.WorkflowPlanRevision,
&p.ActiveWorkflowChangedAt, &p.ActiveWorkflowChangedBy, &p.CreatedAt, &p.UpdatedAt,
+29 -1
View File
@@ -13,6 +13,19 @@ type Project struct {
DeviceType string `json:"device_type"`
IntendedUse string `json:"intended_use"`
TargetMarket string `json:"target_market"`
ProductDomain string `json:"product_domain,omitempty"`
ProjectTypeKey string `json:"project_type_key,omitempty"`
ComplianceProfileKey string `json:"compliance_profile_key,omitempty"`
GovernanceProfile string `json:"governance_profile,omitempty"`
TeamMode string `json:"team_mode,omitempty"`
ExecutionMode string `json:"execution_mode,omitempty"`
DataSensitivity string `json:"data_sensitivity,omitempty"`
NetworkPolicy string `json:"network_policy,omitempty"`
BudgetClass string `json:"budget_class,omitempty"`
ProblemStatement string `json:"problem_statement,omitempty"`
TargetUsers string `json:"target_users,omitempty"`
TargetOutcome string `json:"target_outcome,omitempty"`
ProductShape string `json:"product_shape,omitempty"`
CurrentStage string `json:"current_stage"`
Environment string `json:"environment"`
Status string `json:"status"`
@@ -85,7 +98,7 @@ type DocumentLineage struct {
CreatedAt time.Time `json:"created_at"`
}
// CreateProjectInput 创建项目输入参数
// CreateProjectInput 创建项目输入参数(兼容医疗字段 + 软件向导钩子)。
type CreateProjectInput struct {
Name string `json:"name"`
Code string `json:"code,omitempty"`
@@ -93,6 +106,21 @@ type CreateProjectInput struct {
DeviceType string `json:"device_type,omitempty"`
IntendedUse string `json:"intended_use,omitempty"`
TargetMarket string `json:"target_market,omitempty"`
ProductDomain string `json:"product_domain,omitempty"`
ProjectTypeKey string `json:"project_type_key,omitempty"`
ComplianceProfileKey string `json:"compliance_profile_key,omitempty"`
GovernanceProfile string `json:"governance_profile,omitempty"`
TeamMode string `json:"team_mode,omitempty"`
ExecutionMode string `json:"execution_mode,omitempty"`
DataSensitivity string `json:"data_sensitivity,omitempty"`
NetworkPolicy string `json:"network_policy,omitempty"`
BudgetClass string `json:"budget_class,omitempty"`
ProblemStatement string `json:"problem_statement,omitempty"`
TargetUsers string `json:"target_users,omitempty"`
TargetOutcome string `json:"target_outcome,omitempty"`
ProductShape string `json:"product_shape,omitempty"`
CreationToken string `json:"creation_token,omitempty"`
PlanHash string `json:"plan_hash,omitempty"`
// Deprecated: accepted for catalog existence checks only; never activates the project.
InitialWorkflowKey string `json:"initial_workflow_key,omitempty"`
}
+3
View File
@@ -166,6 +166,9 @@ func (s *Server) Routes() http.Handler {
// 2. 项目与多工作流控制面
mux.HandleFunc("POST /api/v1/projects", s.handleCreateProject)
mux.HandleFunc("POST /api/v1/projects/creation-preview", s.handleProjectCreationPreview)
mux.HandleFunc("GET /api/v1/project-types/catalog", s.handleProjectTypesCatalog)
mux.HandleFunc("GET /api/v1/compliance-profiles/catalog", s.handleComplianceProfilesCatalog)
mux.HandleFunc("GET /api/v1/projects", s.handleListProjects)
mux.HandleFunc("GET /api/v1/projects/{id}", s.handleGetProjectByID)
mux.HandleFunc("GET /api/v1/projects/{id}/members", s.handleProjectMembers)
@@ -0,0 +1,60 @@
package server
import (
"encoding/json"
"net/http"
"time"
"chat-agent-system/internal/swboot"
)
// handleProjectTypesCatalog GET /api/v1/project-types/catalog
func (s *Server) handleProjectTypesCatalog(w http.ResponseWriter, r *http.Request) {
if _, ok := s.requirePrincipal(w, r); !ok {
return
}
writeJSONResponse(w, http.StatusOK, map[string]any{
"project_types": swboot.ProjectTypesCatalog(),
})
}
// handleComplianceProfilesCatalog GET /api/v1/compliance-profiles/catalog
func (s *Server) handleComplianceProfilesCatalog(w http.ResponseWriter, r *http.Request) {
if _, ok := s.requirePrincipal(w, r); !ok {
return
}
writeJSONResponse(w, http.StatusOK, map[string]any{
"compliance_profiles": swboot.ComplianceProfilesCatalog(),
})
}
// handleProjectCreationPreview POST /api/v1/projects/creation-preview
func (s *Server) handleProjectCreationPreview(w http.ResponseWriter, r *http.Request) {
user, err := s.authenticateUser(r)
if err != nil || user == nil {
writeSafeError(w, r, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`)
return
}
if ok, permErr := s.userHasSystemPermission(r.Context(), user, "project:create"); permErr != nil {
writeSafeError(w, r, http.StatusInternalServerError, `{"error":"ERR_INTERNAL","message":"`+permErr.Error()+`"}`)
return
} else if !ok {
writeJSONResponse(w, http.StatusForbidden, map[string]string{
"error_code": "ERR_FORBIDDEN",
"message": "无权创建项目",
})
return
}
var req swboot.CreationPreviewInput
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeSafeError(w, r, http.StatusBadRequest, `{"error":"ERR_INVALID_PARAM","message":"无效的 JSON 请求体"}`)
return
}
res := swboot.BuildCreationPreview(req, time.Now().UTC())
status := http.StatusOK
if !res.Valid {
status = http.StatusBadRequest
}
writeJSONResponse(w, status, res)
}
+256
View File
@@ -0,0 +1,256 @@
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"`
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.
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.
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:
res.Valid = false
res.Issues = append(res.Issues, "ERR_EXECUTION_MODE_DISABLED: controlled_patch requires SWBOOT-10 executor")
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
}
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"
}
+92
View File
@@ -0,0 +1,92 @@
package swboot
import (
"errors"
"strings"
"testing"
"time"
)
func TestProjectTypesCatalog(t *testing.T) {
items := ProjectTypesCatalog()
if len(items) != 4 {
t.Fatalf("expected 4 project types, got %d", len(items))
}
for _, it := range items {
if it.ProductDomain != DomainNonMedicalSoftware {
t.Fatalf("project type %s polluted domain %q", it.Key, it.ProductDomain)
}
if it.DefaultWorkflowKey != "software_bootstrap" {
t.Fatalf("expected software_bootstrap default for %s", it.Key)
}
blob := strings.ToLower(it.Name + " " + it.Description)
for _, bad := range []string{"临床", "nmpa", "cdmo"} {
if strings.Contains(blob, bad) {
t.Fatalf("medical term in project type %s", it.Key)
}
}
}
}
func TestBuildCreationPreview_SelfBootstrapSolo(t *testing.T) {
res := BuildCreationPreview(CreationPreviewInput{
Name: "Agent-System 自举", ProductDomain: DomainNonMedicalSoftware,
ProjectTypeKey: ProjectTypeExistingSelfBootstrap,
ProblemStatement: "建立可信路线", TargetUsers: "内部团队", TargetOutcome: "可执行发布路线",
GovernanceProfile: GovernanceStandard, TeamMode: TeamModeSolo,
ComplianceProfileKey: "internal_control+security_baseline",
ExecutionMode: ExecutionDocsOnly,
}, time.Date(2026, 9, 16, 12, 0, 0, 0, time.UTC))
if !res.Valid {
t.Fatalf("expected valid preview, issues=%v", res.Issues)
}
if res.ApprovalPolicy != ApprovalCreatorAcknowledgement {
t.Fatalf("expected creator acknowledgement, got %s", res.ApprovalPolicy)
}
if res.RecommendedWorkflow != "software_bootstrap" {
t.Fatalf("expected software_bootstrap, got %s", res.RecommendedWorkflow)
}
if res.CreationToken == "" || res.PlanHash == "" {
t.Fatal("expected token and plan hash")
}
}
func TestBuildCreationPreview_RejectsControlledPatchAndSoloPrivacy(t *testing.T) {
res := BuildCreationPreview(CreationPreviewInput{
Name: "x", ProductDomain: DomainNonMedicalSoftware,
ProjectTypeKey: ProjectTypeGreenfield,
ProblemStatement: "p", TargetUsers: "u", TargetOutcome: "o",
TeamMode: TeamModeSolo, ComplianceProfileKey: "privacy",
ExecutionMode: ExecutionControlledPatch,
}, time.Now().UTC())
if res.Valid {
t.Fatal("expected invalid")
}
joined := strings.Join(res.Issues, ";")
if !strings.Contains(joined, "solo") {
t.Fatalf("expected solo/privacy issue, got %v", res.Issues)
}
if !strings.Contains(joined, "ERR_EXECUTION_MODE_DISABLED") {
t.Fatalf("expected controlled_patch disabled, got %v", res.Issues)
}
}
func TestRepoAllowlistStub(t *testing.T) {
al := NewRepoAllowlist([]string{"/home/weichen/projects/Agent-System"})
if err := al.CheckReadOnlyAccess("/home/weichen/projects/Agent-System/cmd"); err != nil {
t.Fatalf("expected allowlisted path ok: %v", err)
}
if err := al.CheckReadOnlyAccess("/etc/passwd"); !errors.Is(err, ErrRepoPathNotAllowlisted) {
t.Fatalf("expected not allowlisted, got %v", err)
}
if err := al.ExecuteControlledPatch("/home/weichen/projects/Agent-System", nil); !errors.Is(err, ErrPatchExecutionUnimplemented) {
t.Fatalf("expected stub rejection, got %v", err)
}
if err := ApproveMerge("feature/x"); !errors.Is(err, ErrPatchExecutionUnimplemented) {
t.Fatalf("expected merge stub rejection, got %v", err)
}
empty := DefaultRepoAllowlist()
if empty.IsAllowlisted("/home/weichen/projects/Agent-System") {
t.Fatal("empty allowlist must deny all")
}
}
+76
View File
@@ -0,0 +1,76 @@
package swboot
import (
"errors"
"fmt"
"path/filepath"
"strings"
)
// SWBOOT-10 stub errors — real patch/merge is intentionally unimplemented.
var (
ErrRepoPathNotAllowlisted = errors.New("ERR_REPO_PATH_NOT_ALLOWLISTED")
ErrControlledPatchDisabled = errors.New("ERR_EXECUTION_MODE_DISABLED")
ErrPatchExecutionUnimplemented = errors.New("ERR_SWBOOT10_STUB_ONLY")
)
// RepoAllowlist is an in-memory / admin-seeded path prefix allowlist stub.
type RepoAllowlist struct {
prefixes []string
}
// NewRepoAllowlist builds a stub allowlist from configured path prefixes.
func NewRepoAllowlist(prefixes []string) *RepoAllowlist {
out := make([]string, 0, len(prefixes))
for _, p := range prefixes {
p = strings.TrimSpace(p)
if p == "" {
continue
}
out = append(out, filepath.Clean(p))
}
return &RepoAllowlist{prefixes: out}
}
// DefaultRepoAllowlist returns an empty allowlist (deny-all until admin seeds paths).
func DefaultRepoAllowlist() *RepoAllowlist {
return NewRepoAllowlist(nil)
}
// IsAllowlisted reports whether absPath is under an enabled prefix.
func (a *RepoAllowlist) IsAllowlisted(absPath string) bool {
if a == nil || len(a.prefixes) == 0 {
return false
}
clean := filepath.Clean(strings.TrimSpace(absPath))
if !filepath.IsAbs(clean) {
return false
}
for _, prefix := range a.prefixes {
if clean == prefix || strings.HasPrefix(clean, prefix+string(filepath.Separator)) {
return true
}
}
return false
}
// CheckReadOnlyAccess validates a path for docs_only / read_only_repo modes.
func (a *RepoAllowlist) CheckReadOnlyAccess(absPath string) error {
if !a.IsAllowlisted(absPath) {
return fmt.Errorf("%w: path %q not in admin allowlist", ErrRepoPathNotAllowlisted, absPath)
}
return nil
}
// ExecuteControlledPatch is the SWBOOT-10 stub: always rejects real patch/merge.
func (a *RepoAllowlist) ExecuteControlledPatch(absPath string, _ []string) error {
if err := a.CheckReadOnlyAccess(absPath); err != nil {
return err
}
return fmt.Errorf("%w: controlled patch/merge not implemented; docs_only/read_only_repo only", ErrPatchExecutionUnimplemented)
}
// ApproveMerge is the SWBOOT-10 stub: approve-execution never implies merge.
func ApproveMerge(_ string) error {
return fmt.Errorf("%w: merge/push/deploy require a separate future approval path", ErrPatchExecutionUnimplemented)
}