diff --git a/AGENTS.md b/AGENTS.md index 282c5aa..b5f5852 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,6 +115,7 @@ JSON-RPC 2.0 over Unix socket at `/cognitiveos/run/raw.sock` (mode 0600, root-ow | `healthcheck` | Return running status | | `version` | Return version, model path, quantization | | `validate_prompt` | Guardrail check — allow/deny/modify prompts before they reach the Wide Model | +| `validate_package_request` | Validate package management operations against OS rules and security conditions | **Full parameter/response schemas**: [product-specs/specs/raw-model.md](https://github.com/CognitiveOS-Project/product-specs/blob/development/specs/raw-model.md#rpc-methods) diff --git a/cmd/cograw/main.go b/cmd/cograw/main.go index ce496e8..36864bc 100644 --- a/cmd/cograw/main.go +++ b/cmd/cograw/main.go @@ -163,12 +163,55 @@ type VersionResult struct { Quant string `json:"quant"` } +type ValidatePromptParams struct { + Prompt string `json:"prompt"` +} + +type ValidatePromptResult struct { + Action string `json:"action"` + ModifiedPrompt string `json:"modified_prompt,omitempty"` + Reason string `json:"reason,omitempty"` +} + +type ValidatePackageRequestParams struct { + Operation string `json:"operation"` + PackageName string `json:"package_name"` + Version string `json:"version,omitempty"` + ManifestMetadata *struct { + HasRawModel bool `json:"has_raw_model,omitempty"` + DiskSpaceMB int64 `json:"disk_space_mb,omitempty"` + Registry string `json:"registry,omitempty"` + IsCritical bool `json:"is_critical,omitempty"` + } `json:"manifest_metadata,omitempty"` +} + +type ValidatePackageRequestResult struct { + Status string `json:"status"` + Reason string `json:"reason"` + Command string `json:"command"` +} + type AuditLogEntry struct { Timestamp string `json:"timestamp"` Event string `json:"event"` Details string `json:"details,omitempty"` } +var ( + packageRateMu sync.Mutex + packageRateCount int + packageRateStart time.Time +) + +const maxPackageOps = 5 +const packageRateWindow = 5 * time.Minute + +var criticalPackages = map[string]bool{ + "cognitiveosd": true, + "raw-model": true, + "base-os": true, +} + var systemCodes = map[string]string{ "wake": "wake_from_idle", "idle": "enter_idle", @@ -313,6 +356,8 @@ func dispatch(call RPCCall, rm *RawModel) RPCResp { return handleVersion(call, rm) case "validate_prompt": return handleValidatePrompt(call, rm) + case "validate_package_request": + return handleValidatePackageRequest(call, rm) default: return RPCResp{ JSONRPC: "2.0", @@ -565,6 +610,109 @@ func handleVersion(call RPCCall, rm *RawModel) RPCResp { } } +func handleValidatePackageRequest(call RPCCall, rm *RawModel) RPCResp { + var params ValidatePackageRequestParams + if err := json.Unmarshal(call.Params, ¶ms); err != nil { + return RPCResp{JSONRPC: "2.0", ID: call.ID, Error: &RPCError{Code: "E_INVALID_PARAMS", Message: err.Error()}} + } + + if params.Operation == "" || params.PackageName == "" { + return RPCResp{JSONRPC: "2.0", ID: call.ID, Error: &RPCError{Code: "E_INVALID_PARAMS", Message: "operation and package_name are required"}} + } + + // Rate limiting + packageRateMu.Lock() + if time.Since(packageRateStart) > packageRateWindow { + packageRateCount = 0 + packageRateStart = time.Now() + } + packageRateCount++ + if packageRateCount > maxPackageOps { + packageRateMu.Unlock() + logAudit("package_rate_limit", + fmt.Sprintf("operation=%s package=%s status=denied reason=rate_limit", params.Operation, params.PackageName)) + return RPCResp{ + JSONRPC: "2.0", + ID: call.ID, + Result: ValidatePackageRequestResult{ + Status: "denied", + Reason: fmt.Sprintf("rate limit exceeded (%d/%d ops per %d minutes)", packageRateCount-1, maxPackageOps, int(packageRateWindow.Minutes())), + Command: "", + }, + } + } + packageRateMu.Unlock() + + // Mutating operations (install, remove, update) have additional checks + isMutating := params.Operation == "install" || params.Operation == "remove" || params.Operation == "update" + + if isMutating { + // Check if package is critical + if criticalPackages[params.PackageName] { + logAudit("package_denied", + fmt.Sprintf("operation=%s package=%s status=denied reason=critical", params.Operation, params.PackageName)) + return RPCResp{ + JSONRPC: "2.0", + ID: call.ID, + Result: ValidatePackageRequestResult{ + Status: "denied", + Reason: fmt.Sprintf("cannot %s critical system package: %s", params.Operation, params.PackageName), + Command: "", + }, + } + } + + // Check manifest metadata if available + if params.ManifestMetadata != nil { + if params.ManifestMetadata.HasRawModel { + logAudit("package_denied", + fmt.Sprintf("operation=%s package=%s status=denied reason=has_raw_model", params.Operation, params.PackageName)) + return RPCResp{ + JSONRPC: "2.0", + ID: call.ID, + Result: ValidatePackageRequestResult{ + Status: "denied", + Reason: "package contains a raw_model descriptor and cannot be auto-installed", + Command: "", + }, + } + } + + if params.ManifestMetadata.IsCritical { + logAudit("package_denied", + fmt.Sprintf("operation=%s package=%s status=denied reason=critical_metadata", params.Operation, params.PackageName)) + return RPCResp{ + JSONRPC: "2.0", + ID: call.ID, + Result: ValidatePackageRequestResult{ + Status: "denied", + Reason: fmt.Sprintf("cannot %s critical system package: %s", params.Operation, params.PackageName), + Command: "", + }, + } + } + } + } + + logAudit("package_approved", + fmt.Sprintf("operation=%s package=%s version=%s status=approved", params.Operation, params.PackageName, params.Version)) + + command := fmt.Sprintf("cpm %s %s", params.Operation, params.PackageName) + if params.Version != "" { + command += " --version " + params.Version + } + + return RPCResp{ + JSONRPC: "2.0", + ID: call.ID, + Result: ValidatePackageRequestResult{ + Status: "approved", + Reason: "", + Command: command, + }, + } +} + func (r *RawModel) classifyPrompt(input string) (string, string, error) { classifyInstruction := `You are a prompt guardrail for CognitiveOS. Your only job is to classify user input.