mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Classify action routes and operation transports
This commit is contained in:
@@ -116,7 +116,7 @@ jobs:
|
||||
run: python3 scripts/release_control/registry_audit.py --check
|
||||
|
||||
- name: Run canonical mutation registry audits
|
||||
run: go test ./internal/mutationregistry ./internal/ai/tools -run 'Test(EveryRegisteredMutationHasDisposition|InfrastructureAPIRoutesResolveToRegistry|TransportCommandCatalogsResolveToRegistry|PatrolJobRegistrationResolvesToRegistry|RuntimeCandidateAuditNegativeFixtures|RegisteredModelMutationSchemasResolveToClosedRegistry|RetiredMutationAliasesCannotShadowExtensions)' -count=1
|
||||
run: go test ./internal/mutationregistry ./internal/ai/tools -run 'Test(EveryRegisteredMutationHasDisposition|InfrastructureAPIRoutesResolveToRegistry|TransportCommandCatalogsResolveToRegistry|PatrolJobRegistrationResolvesToRegistry|RuntimeCandidateAuditNegativeFixtures|ActionRouteMethodAuthorityIsExactAndLookalikesFailClosed|NonAdmittingTransportMessagesCannotCarryDispatchAuthority|UnknownTransportLookalikeFailsClosed|RegisteredModelMutationSchemasResolveToClosedRegistry|RetiredMutationAliasesCannotShadowExtensions)' -count=1
|
||||
|
||||
- name: Run contract audit
|
||||
run: python3 scripts/release_control/contract_audit.py --check
|
||||
|
||||
@@ -5778,6 +5778,18 @@ Production code contains no command or rollback execution algorithm; exported
|
||||
approve/execute/rollback interfaces and HTTP endpoints are permanently inert
|
||||
even when a command executor is injected.
|
||||
|
||||
The agent transport catalogue now classifies wire roles independently from
|
||||
mutation identity. `docker_container_lifecycle` is the typed mutation request
|
||||
for the existing `resource.docker.container-lifecycle` capability and may be
|
||||
sent only after committed action-lifecycle authority.
|
||||
`agent_operation_query` is query-only reconciliation;
|
||||
`agent_operation_query_result` and `docker_container_lifecycle_result` are
|
||||
non-admitting receipt/result messages. Query, result, receipt, and general
|
||||
protocol roles are forbidden from carrying a mutation registry id or durable
|
||||
authority reference, so response-shaped lookalikes cannot become dispatch
|
||||
entry points. Task 10 remains the sole owner of `ActionResultV2` execution,
|
||||
verification, evidence, and compensation truth.
|
||||
|
||||
Action planning and approval attribution are now server-owned across Assistant,
|
||||
Patrol, and MCP projections. Trusted brokers use explicit service/policy actor
|
||||
contexts; public `requestedBy` content cannot become audit authority. The agent
|
||||
|
||||
@@ -14,6 +14,73 @@ type RuntimeCandidate struct {
|
||||
DurableAuthorityID string
|
||||
}
|
||||
|
||||
type TransportRole string
|
||||
|
||||
const (
|
||||
TransportRoleMutationRequest TransportRole = "mutation_request"
|
||||
TransportRoleAdministrativeRequest TransportRole = "administrative_request"
|
||||
TransportRoleOperationQuery TransportRole = "operation_query"
|
||||
TransportRoleOperationResult TransportRole = "operation_result"
|
||||
TransportRoleOperationReceipt TransportRole = "operation_receipt"
|
||||
TransportRoleProtocol TransportRole = "protocol"
|
||||
)
|
||||
|
||||
// TransportSurface classifies one actual wire-catalog member. Only mutation
|
||||
// requests may bind lifecycle authority. Queries, results, receipts, and
|
||||
// protocol messages are explicitly non-admitting and cannot carry a mutation
|
||||
// registry identity or durable-authority reference.
|
||||
type TransportSurface struct {
|
||||
Name string
|
||||
WireValue string
|
||||
Role TransportRole
|
||||
MutationID string
|
||||
DurableAuthorityID string
|
||||
}
|
||||
|
||||
func AuditTransportSurfaces(surfaces []TransportSurface) error {
|
||||
seenNames := make(map[string]struct{}, len(surfaces))
|
||||
seenWireValues := make(map[string]struct{}, len(surfaces))
|
||||
for _, surface := range surfaces {
|
||||
name := strings.TrimSpace(surface.Name)
|
||||
wireValue := strings.TrimSpace(surface.WireValue)
|
||||
if name == "" || wireValue == "" {
|
||||
return fmt.Errorf("transport surface has empty name or wire value: %+v", surface)
|
||||
}
|
||||
if _, duplicate := seenNames[name]; duplicate {
|
||||
return fmt.Errorf("transport surface name %q is classified more than once", name)
|
||||
}
|
||||
if _, duplicate := seenWireValues[wireValue]; duplicate {
|
||||
return fmt.Errorf("transport wire value %q is classified more than once", wireValue)
|
||||
}
|
||||
seenNames[name] = struct{}{}
|
||||
seenWireValues[wireValue] = struct{}{}
|
||||
|
||||
switch surface.Role {
|
||||
case TransportRoleMutationRequest:
|
||||
if err := AuditRuntimeCandidates([]RuntimeCandidate{{
|
||||
Surface: "transport:" + wireValue,
|
||||
MutationID: surface.MutationID,
|
||||
Transport: true,
|
||||
DurableAuthorityID: surface.DurableAuthorityID,
|
||||
}}); err != nil {
|
||||
return err
|
||||
}
|
||||
case TransportRoleAdministrativeRequest:
|
||||
entry, ok := Lookup(surface.MutationID)
|
||||
if !ok || entry.Disposition != DispositionAdministrativeException || surface.DurableAuthorityID != "" {
|
||||
return fmt.Errorf("administrative transport %q has invalid registry authority", wireValue)
|
||||
}
|
||||
case TransportRoleOperationQuery, TransportRoleOperationResult, TransportRoleOperationReceipt, TransportRoleProtocol:
|
||||
if strings.TrimSpace(surface.MutationID) != "" || strings.TrimSpace(surface.DurableAuthorityID) != "" {
|
||||
return fmt.Errorf("non-admitting transport %q cannot carry mutation authority", wireValue)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("transport %q has unknown role %q", wireValue, surface.Role)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuditRuntimeCandidates requires every mechanically discovered mutation
|
||||
// candidate to resolve to exactly one closed registry disposition. Transport
|
||||
// candidates additionally name the lifecycle authority that must exist before
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
{"id":"resource.host.package-update","origin":"api","resource_class":"customer_infrastructure","resource_kind":"agent","capability":"install_os_updates","entrypoint":"host.package_updates","disposition":"lifecycle","lifecycle_executor":"hostUpdateActionExecutor","approval_floor":"policy_or_admin","delivery":"committed_lifecycle_before_transport","verification":"required","rollback":"task_10_truth_and_compensation","residual_owners":["task_10_truth_and_compensation"]},
|
||||
{"id":"resource.proxmox.guest-lifecycle","origin":"api","resource_class":"customer_infrastructure","resource_kind":"vm_or_system-container","capability":"start_shutdown_reboot_stop","entrypoint":"proxmox.vm.lifecycle/proxmox.ct.lifecycle","disposition":"lifecycle","lifecycle_executor":"proxmoxGuestActionExecutor","approval_floor":"admin","delivery":"committed_lifecycle_before_transport","verification":"required","rollback":"task_10_truth_and_compensation","residual_owners":["task_10_truth_and_compensation"]},
|
||||
|
||||
{"id":"transport.agent.docker-container-lifecycle","origin":"transport","resource_class":"customer_infrastructure","resource_kind":"app-container","capability":"start_stop_restart","entrypoint":"agentexec docker_container_lifecycle","disposition":"lifecycle","lifecycle_executor":"dockerContainerActionExecutor","approval_floor":"admin","delivery":"committed_lifecycle_before_transport","verification":"required","rollback":"task_10_truth_and_compensation","residual_owners":["task_10_truth_and_compensation"]},
|
||||
{"id":"transport.agent.host-package-cache-cleanup","origin":"transport","resource_class":"customer_infrastructure","resource_kind":"agent","capability":"clean_package_cache","entrypoint":"agentexec host_storage_cleanup","disposition":"lifecycle","lifecycle_executor":"hostStorageCleanupActionExecutor","approval_floor":"policy_or_admin","delivery":"committed_lifecycle_before_transport","verification":"required","rollback":"unsupported"},
|
||||
{"id":"transport.agent.host-package-update","origin":"transport","resource_class":"customer_infrastructure","resource_kind":"agent","capability":"install_os_updates","entrypoint":"agentexec host_update","disposition":"lifecycle","lifecycle_executor":"hostUpdateActionExecutor","approval_floor":"policy_or_admin","delivery":"committed_lifecycle_before_transport","verification":"required","rollback":"task_10_truth_and_compensation","residual_owners":["task_10_truth_and_compensation"]},
|
||||
{"id":"transport.agent.raw-command","origin":"transport","resource_class":"customer_infrastructure","resource_kind":"agent-target","capability":"executor_owned_command","entrypoint":"agentexec execute_command","disposition":"lifecycle","lifecycle_executor":"dockerContainerActionExecutor/proxmoxGuestActionExecutor","approval_floor":"admin","delivery":"committed_lifecycle_before_transport","verification":"required","rollback":"task_10_truth_and_compensation","residual_owners":["task_10_truth_and_compensation"]},
|
||||
|
||||
@@ -39,6 +39,7 @@ var generatedEntries = []Entry{
|
||||
{ID: "transport.agent.deploy-cancel", Origin: Origin("transport"), ResourceClass: ResourceClass("pulse_administration"), ResourceKind: "agent-enrollment", Capability: "deploy_agent", Entrypoint: "agentexec deploy_cancel", Disposition: Disposition("administrative_exception"), LifecycleExecutor: "", Approval: ApprovalFloor("admin"), Delivery: DeliveryClass("administrative_transaction"), Verification: VerificationClass("administrative"), Rollback: RollbackClass("unsupported"), ResidualOwners: []string{"task_12_final_governance"}},
|
||||
{ID: "transport.agent.deploy-install", Origin: Origin("transport"), ResourceClass: ResourceClass("pulse_administration"), ResourceKind: "agent-enrollment", Capability: "deploy_agent", Entrypoint: "agentexec deploy_install", Disposition: Disposition("administrative_exception"), LifecycleExecutor: "", Approval: ApprovalFloor("admin"), Delivery: DeliveryClass("administrative_transaction"), Verification: VerificationClass("administrative"), Rollback: RollbackClass("unsupported"), ResidualOwners: []string{"task_12_final_governance"}},
|
||||
{ID: "transport.agent.deploy-preflight", Origin: Origin("transport"), ResourceClass: ResourceClass("pulse_administration"), ResourceKind: "agent-enrollment", Capability: "deploy_agent", Entrypoint: "agentexec deploy_preflight", Disposition: Disposition("administrative_exception"), LifecycleExecutor: "", Approval: ApprovalFloor("admin"), Delivery: DeliveryClass("administrative_transaction"), Verification: VerificationClass("administrative"), Rollback: RollbackClass("unsupported"), ResidualOwners: []string{"task_12_final_governance"}},
|
||||
{ID: "transport.agent.docker-container-lifecycle", Origin: Origin("transport"), ResourceClass: ResourceClass("customer_infrastructure"), ResourceKind: "app-container", Capability: "start_stop_restart", Entrypoint: "agentexec docker_container_lifecycle", Disposition: Disposition("lifecycle"), LifecycleExecutor: "dockerContainerActionExecutor", Approval: ApprovalFloor("admin"), Delivery: DeliveryClass("committed_lifecycle_before_transport"), Verification: VerificationClass("required"), Rollback: RollbackClass("task_10_truth_and_compensation"), ResidualOwners: []string{"task_10_truth_and_compensation"}},
|
||||
{ID: "transport.agent.host-package-cache-cleanup", Origin: Origin("transport"), ResourceClass: ResourceClass("customer_infrastructure"), ResourceKind: "agent", Capability: "clean_package_cache", Entrypoint: "agentexec host_storage_cleanup", Disposition: Disposition("lifecycle"), LifecycleExecutor: "hostStorageCleanupActionExecutor", Approval: ApprovalFloor("policy_or_admin"), Delivery: DeliveryClass("committed_lifecycle_before_transport"), Verification: VerificationClass("required"), Rollback: RollbackClass("unsupported"), ResidualOwners: []string(nil)},
|
||||
{ID: "transport.agent.host-package-update", Origin: Origin("transport"), ResourceClass: ResourceClass("customer_infrastructure"), ResourceKind: "agent", Capability: "install_os_updates", Entrypoint: "agentexec host_update", Disposition: Disposition("lifecycle"), LifecycleExecutor: "hostUpdateActionExecutor", Approval: ApprovalFloor("policy_or_admin"), Delivery: DeliveryClass("committed_lifecycle_before_transport"), Verification: VerificationClass("required"), Rollback: RollbackClass("task_10_truth_and_compensation"), ResidualOwners: []string{"task_10_truth_and_compensation"}},
|
||||
{ID: "transport.agent.raw-command", Origin: Origin("transport"), ResourceClass: ResourceClass("customer_infrastructure"), ResourceKind: "agent-target", Capability: "executor_owned_command", Entrypoint: "agentexec execute_command", Disposition: Disposition("lifecycle"), LifecycleExecutor: "dockerContainerActionExecutor/proxmoxGuestActionExecutor", Approval: ApprovalFloor("admin"), Delivery: DeliveryClass("committed_lifecycle_before_transport"), Verification: VerificationClass("required"), Rollback: RollbackClass("task_10_truth_and_compensation"), ResidualOwners: []string{"task_10_truth_and_compensation"}},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package mutationregistry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -9,6 +10,37 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type routeClassification struct {
|
||||
MutationID string
|
||||
}
|
||||
|
||||
var infrastructureRouteCatalog = map[string]routeClassification{
|
||||
"POST /api/actions/plan": {MutationID: "action.api.plan"},
|
||||
"GET /api/actions/pending": {},
|
||||
"GET /api/actions": {},
|
||||
"GET /api/actions/{id}": {},
|
||||
"POST /api/actions/{id}/decision": {MutationID: "action.api.decision"},
|
||||
"POST /api/actions/{id}/execute": {MutationID: "action.api.execute"},
|
||||
"/api/agents/docker/report": {},
|
||||
"/api/agents/docker/commands/": {},
|
||||
"/api/agents/docker/runtimes/": {MutationID: "docker.api.runtime-stop"},
|
||||
"/api/agents/docker/containers/update": {MutationID: "docker.api.update-container"},
|
||||
"/api/updates/check": {},
|
||||
"/api/updates/apply": {MutationID: "admin.pulse.self-update"},
|
||||
"/api/updates/rollback": {MutationID: "admin.pulse.self-update"},
|
||||
"/api/updates/status": {},
|
||||
"/api/updates/stream": {},
|
||||
"/api/updates/plan": {},
|
||||
"/api/updates/history": {},
|
||||
"/api/updates/history/entry": {},
|
||||
"/api/ai/run-command": {MutationID: "legacy.api.run-command"},
|
||||
"/api/ai/remediation/plans": {},
|
||||
"/api/ai/remediation/plan": {},
|
||||
"/api/ai/remediation/approve": {MutationID: "legacy.enterprise.remediation-approve"},
|
||||
"/api/ai/remediation/execute": {MutationID: "legacy.enterprise.remediation-execute"},
|
||||
"/api/ai/remediation/rollback": {MutationID: "legacy.enterprise.remediation-rollback"},
|
||||
}
|
||||
|
||||
// TestInfrastructureAPIRoutesResolveToRegistry scans the real router sources,
|
||||
// not a duplicate ID inventory. Its scope is deliberately limited to the
|
||||
// infrastructure mutation namespaces below; settings/metadata CRUD is outside
|
||||
@@ -20,31 +52,7 @@ func TestInfrastructureAPIRoutesResolveToRegistry(t *testing.T) {
|
||||
"internal/api/router_routes_registration.go",
|
||||
"internal/api/router_routes_ai_relay.go",
|
||||
}
|
||||
catalog := map[string]string{
|
||||
"/api/actions/plan": "action.api.plan",
|
||||
"/api/actions/pending": "",
|
||||
"/api/actions/{id}/decision": "action.api.decision",
|
||||
"/api/actions/{id}/execute": "action.api.execute",
|
||||
"/api/agents/docker/report": "",
|
||||
"/api/agents/docker/commands/": "",
|
||||
"/api/agents/docker/runtimes/": "docker.api.runtime-stop",
|
||||
"/api/agents/docker/containers/update": "docker.api.update-container",
|
||||
"/api/updates/check": "",
|
||||
"/api/updates/apply": "admin.pulse.self-update",
|
||||
"/api/updates/rollback": "admin.pulse.self-update",
|
||||
"/api/updates/status": "",
|
||||
"/api/updates/stream": "",
|
||||
"/api/updates/plan": "",
|
||||
"/api/updates/history": "",
|
||||
"/api/updates/history/entry": "",
|
||||
"/api/ai/run-command": "legacy.api.run-command",
|
||||
"/api/ai/remediation/plans": "",
|
||||
"/api/ai/remediation/plan": "",
|
||||
"/api/ai/remediation/approve": "legacy.enterprise.remediation-approve",
|
||||
"/api/ai/remediation/execute": "legacy.enterprise.remediation-execute",
|
||||
"/api/ai/remediation/rollback": "legacy.enterprise.remediation-rollback",
|
||||
}
|
||||
routeRE := regexp.MustCompile(`"(?:GET |POST |PUT |DELETE )?(/api/[^"]+)"`)
|
||||
routeRE := regexp.MustCompile(`"(?:(GET|POST|PUT|DELETE) )?(/api/[^"]+)"`)
|
||||
found := map[string]bool{}
|
||||
for _, relative := range files {
|
||||
data, err := os.ReadFile(filepath.Join(root, relative))
|
||||
@@ -52,26 +60,27 @@ func TestInfrastructureAPIRoutesResolveToRegistry(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, match := range routeRE.FindAllStringSubmatch(string(data), -1) {
|
||||
route := match[1]
|
||||
method, route := match[1], match[2]
|
||||
if !auditedInfrastructurePrefix(route) {
|
||||
continue
|
||||
}
|
||||
found[route] = true
|
||||
id, classified := catalog[route]
|
||||
key := routeCatalogKey(method, route)
|
||||
found[key] = true
|
||||
classification, classified := infrastructureRouteCatalog[key]
|
||||
if !classified {
|
||||
t.Errorf("runtime infrastructure route %q is unclassified", route)
|
||||
t.Errorf("runtime infrastructure route %q is unclassified", key)
|
||||
continue
|
||||
}
|
||||
if id != "" {
|
||||
if err := AuditRuntimeCandidates([]RuntimeCandidate{{Surface: "http:" + route, MutationID: id}}); err != nil {
|
||||
if classification.MutationID != "" {
|
||||
if err := AuditRuntimeCandidates([]RuntimeCandidate{{Surface: "http:" + key, MutationID: classification.MutationID}}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for route := range catalog {
|
||||
if !found[route] {
|
||||
t.Errorf("classified route %q is no longer registered", route)
|
||||
for key := range infrastructureRouteCatalog {
|
||||
if !found[key] {
|
||||
t.Errorf("classified route %q is no longer registered", key)
|
||||
}
|
||||
}
|
||||
// The shared Docker gateway contains a second mutation sub-route.
|
||||
@@ -84,7 +93,17 @@ func TestInfrastructureAPIRoutesResolveToRegistry(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func routeCatalogKey(method, route string) string {
|
||||
if strings.TrimSpace(method) == "" {
|
||||
return route
|
||||
}
|
||||
return method + " " + route
|
||||
}
|
||||
|
||||
func auditedInfrastructurePrefix(route string) bool {
|
||||
if route == "/api/actions" {
|
||||
return true
|
||||
}
|
||||
for _, prefix := range []string{"/api/actions/", "/api/agents/docker/", "/api/updates/", "/api/ai/remediation/", "/api/ai/run-command"} {
|
||||
if strings.HasPrefix(route, prefix) {
|
||||
return true
|
||||
@@ -93,32 +112,90 @@ func auditedInfrastructurePrefix(route string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func TestActionRouteMethodAuthorityIsExactAndLookalikesFailClosed(t *testing.T) {
|
||||
tests := []struct {
|
||||
method, route, mutationID string
|
||||
classified bool
|
||||
}{
|
||||
{method: "GET", route: "/api/actions/{id}", classified: true},
|
||||
{method: "POST", route: "/api/actions/{id}/execute", mutationID: "action.api.execute", classified: true},
|
||||
{method: "DELETE", route: "/api/actions/{id}"},
|
||||
{method: "POST", route: "/api/actions/{id}"},
|
||||
{method: "GET", route: "/api/actions/{id}/execute"},
|
||||
{method: "POST", route: "/api/actions/{id}/execute-copy"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
key := routeCatalogKey(tt.method, tt.route)
|
||||
classification, ok := infrastructureRouteCatalog[key]
|
||||
if ok != tt.classified {
|
||||
t.Errorf("route %q classified=%v, want %v", key, ok, tt.classified)
|
||||
continue
|
||||
}
|
||||
if ok && classification.MutationID != tt.mutationID {
|
||||
t.Errorf("route %q mutation=%q, want %q", key, classification.MutationID, tt.mutationID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type transportClassification struct {
|
||||
Role TransportRole
|
||||
MutationID string
|
||||
DurableAuthorityID string
|
||||
}
|
||||
|
||||
func TestTransportCommandCatalogsResolveToRegistry(t *testing.T) {
|
||||
root := repoRoot(t)
|
||||
docker := scanNamedStringConstants(t, filepath.Join(root, "internal/monitoring/docker_commands.go"), `DockerCommandType\w+`)
|
||||
dockerCatalog := map[string]string{
|
||||
"DockerCommandTypeStop": "transport.docker.runtime-stop",
|
||||
"DockerCommandTypeUpdateContainer": "transport.docker.update-container",
|
||||
"DockerCommandTypeUpdateAll": "transport.docker.update-all",
|
||||
"DockerCommandTypeCheckUpdates": "",
|
||||
dockerCatalog := map[string]transportClassification{
|
||||
"DockerCommandTypeStop": {Role: TransportRoleMutationRequest, MutationID: "transport.docker.runtime-stop"},
|
||||
"DockerCommandTypeUpdateContainer": {Role: TransportRoleMutationRequest, MutationID: "transport.docker.update-container"},
|
||||
"DockerCommandTypeUpdateAll": {Role: TransportRoleMutationRequest, MutationID: "transport.docker.update-all"},
|
||||
"DockerCommandTypeCheckUpdates": {Role: TransportRoleProtocol},
|
||||
}
|
||||
auditConstantCatalog(t, docker, dockerCatalog, nil)
|
||||
auditConstantCatalog(t, docker, dockerCatalog)
|
||||
|
||||
agent := scanNamedStringConstants(t, filepath.Join(root, "internal/agentexec/types.go"), `MsgType\w+`)
|
||||
agentCatalog := map[string]string{
|
||||
"MsgTypeAgentRegister": "", "MsgTypeAgentPing": "", "MsgTypeCommandResult": "",
|
||||
"MsgTypeHostStorageCleanupResult": "", "MsgTypeHostUpdateResult": "", "MsgTypeRegistered": "", "MsgTypePong": "",
|
||||
"MsgTypeExecuteCmd": "transport.agent.raw-command", "MsgTypeHostStorageCleanup": "transport.agent.host-package-cache-cleanup",
|
||||
"MsgTypeReadFile": "", "MsgTypeHostUpdate": "transport.agent.host-package-update",
|
||||
"MsgTypeDeployPreflight": "transport.agent.deploy-preflight", "MsgTypeDeployInstall": "transport.agent.deploy-install",
|
||||
"MsgTypeDeployCancelJob": "transport.agent.deploy-cancel", "MsgTypeDeployProgress": "",
|
||||
agentCatalog := map[string]transportClassification{
|
||||
"MsgTypeAgentRegister": {Role: TransportRoleProtocol}, "MsgTypeAgentPing": {Role: TransportRoleProtocol},
|
||||
"MsgTypeCommandResult": {Role: TransportRoleOperationResult},
|
||||
"MsgTypeHostStorageCleanupResult": {Role: TransportRoleOperationResult}, "MsgTypeHostUpdateResult": {Role: TransportRoleOperationResult},
|
||||
"MsgTypeDockerContainerLifecycleResult": {Role: TransportRoleOperationResult},
|
||||
"MsgTypeOperationQueryResult": {Role: TransportRoleOperationReceipt},
|
||||
"MsgTypeRegistered": {Role: TransportRoleProtocol}, "MsgTypePong": {Role: TransportRoleProtocol},
|
||||
"MsgTypeExecuteCmd": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.raw-command", DurableAuthorityID: "assistant.resource-action"},
|
||||
"MsgTypeHostStorageCleanup": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.host-package-cache-cleanup", DurableAuthorityID: "resource.host.package-cache-cleanup"},
|
||||
"MsgTypeReadFile": {Role: TransportRoleProtocol},
|
||||
"MsgTypeHostUpdate": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.host-package-update", DurableAuthorityID: "resource.host.package-update"},
|
||||
"MsgTypeDockerContainerLifecycle": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.docker-container-lifecycle", DurableAuthorityID: "resource.docker.container-lifecycle"},
|
||||
"MsgTypeOperationQuery": {Role: TransportRoleOperationQuery},
|
||||
"MsgTypeDeployPreflight": {Role: TransportRoleAdministrativeRequest, MutationID: "transport.agent.deploy-preflight"},
|
||||
"MsgTypeDeployInstall": {Role: TransportRoleAdministrativeRequest, MutationID: "transport.agent.deploy-install"},
|
||||
"MsgTypeDeployCancelJob": {Role: TransportRoleAdministrativeRequest, MutationID: "transport.agent.deploy-cancel"},
|
||||
"MsgTypeDeployProgress": {Role: TransportRoleOperationResult},
|
||||
}
|
||||
authority := map[string]string{
|
||||
"MsgTypeExecuteCmd": "assistant.resource-action",
|
||||
"MsgTypeHostStorageCleanup": "resource.host.package-cache-cleanup",
|
||||
"MsgTypeHostUpdate": "resource.host.package-update",
|
||||
auditConstantCatalog(t, agent, agentCatalog)
|
||||
}
|
||||
|
||||
func TestNonAdmittingTransportMessagesCannotCarryDispatchAuthority(t *testing.T) {
|
||||
for _, role := range []TransportRole{TransportRoleOperationQuery, TransportRoleOperationResult, TransportRoleOperationReceipt, TransportRoleProtocol} {
|
||||
err := AuditTransportSurfaces([]TransportSurface{{
|
||||
Name: "lookalike", WireValue: "docker_container_lifecycle_result_retry", Role: role,
|
||||
MutationID: "transport.agent.docker-container-lifecycle", DurableAuthorityID: "resource.docker.container-lifecycle",
|
||||
}})
|
||||
if err == nil {
|
||||
t.Errorf("non-admitting role %q accepted dispatch authority", role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownTransportLookalikeFailsClosed(t *testing.T) {
|
||||
actual := map[string]string{"MsgTypeDockerContainerLifecycle": "docker_container_lifecycle", "MsgTypeDockerContainerLifecycleRetry": "docker_container_lifecycle_retry"}
|
||||
catalog := map[string]transportClassification{
|
||||
"MsgTypeDockerContainerLifecycle": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.docker-container-lifecycle", DurableAuthorityID: "resource.docker.container-lifecycle"},
|
||||
}
|
||||
if unknown := unclassifiedTransportConstants(actual, catalog); len(unknown) != 1 || unknown[0] != `MsgTypeDockerContainerLifecycleRetry="docker_container_lifecycle_retry"` {
|
||||
t.Fatalf("unknown lookalike classification = %v", unknown)
|
||||
}
|
||||
auditConstantCatalog(t, agent, agentCatalog, authority)
|
||||
}
|
||||
|
||||
func TestPatrolJobRegistrationResolvesToRegistry(t *testing.T) {
|
||||
@@ -147,21 +224,19 @@ func scanNamedStringConstants(t *testing.T, path, namePattern string) map[string
|
||||
return out
|
||||
}
|
||||
|
||||
func auditConstantCatalog(t *testing.T, actual, catalog, authority map[string]string) {
|
||||
func auditConstantCatalog(t *testing.T, actual map[string]string, catalog map[string]transportClassification) {
|
||||
t.Helper()
|
||||
var surfaces []TransportSurface
|
||||
for name, value := range actual {
|
||||
id, ok := catalog[name]
|
||||
classification, ok := catalog[name]
|
||||
if !ok {
|
||||
t.Errorf("transport command %s=%q is unclassified", name, value)
|
||||
continue
|
||||
}
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
candidate := RuntimeCandidate{Surface: "transport:" + value, MutationID: id, Transport: true, DurableAuthorityID: authority[name]}
|
||||
if err := AuditRuntimeCandidates([]RuntimeCandidate{candidate}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
surfaces = append(surfaces, TransportSurface{Name: name, WireValue: value, Role: classification.Role, MutationID: classification.MutationID, DurableAuthorityID: classification.DurableAuthorityID})
|
||||
}
|
||||
if err := AuditTransportSurfaces(surfaces); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
var stale []string
|
||||
for name := range catalog {
|
||||
@@ -174,3 +249,14 @@ func auditConstantCatalog(t *testing.T, actual, catalog, authority map[string]st
|
||||
t.Errorf("classified transport constants no longer exist: %v", stale)
|
||||
}
|
||||
}
|
||||
|
||||
func unclassifiedTransportConstants(actual map[string]string, catalog map[string]transportClassification) []string {
|
||||
var unknown []string
|
||||
for name, value := range actual {
|
||||
if _, ok := catalog[name]; !ok {
|
||||
unknown = append(unknown, fmt.Sprintf(`%s=%q`, name, value))
|
||||
}
|
||||
}
|
||||
sort.Strings(unknown)
|
||||
return unknown
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user