From b6712850c383b0709b6bf5b56cfb81487d41e5d9 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 17 May 2026 06:26:13 +0200 Subject: [PATCH] Define Markitect action vocabulary --- docs/markitect-action-vocabulary.md | 24 ++++ .../markitect/protected_system_manifest.yaml | 86 +++++++++++++ internal/markitect/actions.go | 117 ++++++++++++++++++ internal/markitect/actions_test.go | 55 ++++++++ pkg/api/resource_manifest_test.go | 9 ++ ...-WP-0003-markitect-consumer-integration.md | 2 +- 6 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 docs/markitect-action-vocabulary.md create mode 100644 internal/markitect/actions.go create mode 100644 internal/markitect/actions_test.go diff --git a/docs/markitect-action-vocabulary.md b/docs/markitect-action-vocabulary.md new file mode 100644 index 0000000..e3d3d8a --- /dev/null +++ b/docs/markitect-action-vocabulary.md @@ -0,0 +1,24 @@ +# Markitect Action Vocabulary + +This document defines the action vocabulary for Markitect as a flex-auth +protected system. Actions are normalized before policy evaluation so Markitect +local behavior maps cleanly to CARING capabilities and exposure modes. + +| Action | Markitect policy-gateway meaning | CARING capabilities | CARING planes | Exposure modes | Decision effects | +| --- | --- | --- | --- | --- | --- | +| `read` | Render or fetch one document/resource. | `View` | `Data` | `Metadata`, `Masked`, `Plaintext` | `allow`, `deny`, `redact` | +| `query` | Answer over a bounded resource set. | `ViewCollection`, `Observe` | `Data` | `Metadata`, `Aggregated`, `Masked` | `allow`, `deny`, `redact` | +| `search` | Search index or metadata across resources. | `ViewCollection`, `Observe` | `Data` | `Metadata`, `Aggregated`, `Masked` | `allow`, `deny`, `redact` | +| `package` | Build a context package from selected resources. | `Create`, `Bind`, `ViewCollection` | `Intent`, `Data` | `Metadata`, `Masked` | `allow`, `deny`, `audit_only` | +| `activate_context` | Activate a prepared context package for model/tool use. | `Use`, `Execute` | `Intent`, `Policy` | `Metadata`, `Masked` | `allow`, `deny`, `audit_only` | +| `export` | Materialize or transfer content outside Markitect. | `Export` | `Data`, `Audit` | `Exportable`, `Plaintext` | `allow`, `deny`, `audit_only` | +| `workflow_run` | Execute a workflow using Markitect resources. | `Execute`, `Operate` | `Execution`, `Data`, `Audit` | `Metadata`, `Masked`, `Plaintext` | `allow`, `deny`, `audit_only` | +| `admin` | Configure Markitect policy, identity, or resource controls. | `Configure`, `Grant`, `Revoke`, `Audit` | `Configuration`, `Identity`, `Policy`, `Audit` | `Metadata`, `Plaintext` | `allow`, `deny`, `audit_only` | + +`read`, `query`, and `search` never imply `Export`. Export is separate because +it changes the exposure mode to `Exportable` and usually requires explicit +conditions such as MFA and logging. + +The code-level source of truth is `internal/markitect/actions.go`. The pinned +manifest example in `examples/markitect/protected_system_manifest.yaml` mirrors +that vocabulary as protected-system action definitions. diff --git a/examples/markitect/protected_system_manifest.yaml b/examples/markitect/protected_system_manifest.yaml index 461a3c4..222a3ff 100644 --- a/examples/markitect/protected_system_manifest.yaml +++ b/examples/markitect/protected_system_manifest.yaml @@ -3,6 +3,92 @@ name: Markitect Tool description: Markitect protected-system namespace for flex-auth. caring_profiles: - caring-0.4.0-rc2 +actions: + - name: read + capabilities: + - View + planes: + - Data + exposure_modes: + - Metadata + - Masked + - Plaintext + - name: query + capabilities: + - ViewCollection + - Observe + planes: + - Data + exposure_modes: + - Metadata + - Aggregated + - Masked + - name: search + capabilities: + - ViewCollection + - Observe + planes: + - Data + exposure_modes: + - Metadata + - Aggregated + - Masked + - name: package + capabilities: + - Create + - Bind + - ViewCollection + planes: + - Intent + - Data + exposure_modes: + - Metadata + - Masked + - name: activate_context + capabilities: + - Use + - Execute + planes: + - Intent + - Policy + exposure_modes: + - Metadata + - Masked + - name: export + capabilities: + - Export + planes: + - Data + - Audit + exposure_modes: + - Exportable + - Plaintext + - name: workflow_run + capabilities: + - Execute + - Operate + planes: + - Execution + - Data + - Audit + exposure_modes: + - Metadata + - Masked + - Plaintext + - name: admin + capabilities: + - Configure + - Grant + - Revoke + - Audit + planes: + - Configuration + - Identity + - Policy + - Audit + exposure_modes: + - Metadata + - Plaintext resource_types: - name: knowledge_base scope_level: Workspace diff --git a/internal/markitect/actions.go b/internal/markitect/actions.go new file mode 100644 index 0000000..e8ce959 --- /dev/null +++ b/internal/markitect/actions.go @@ -0,0 +1,117 @@ +package markitect + +import "github.com/netkingdom/flex-auth/pkg/api" + +const ( + ActionRead = "read" + ActionQuery = "query" + ActionSearch = "search" + ActionPackage = "package" + ActionActivateContext = "activate_context" + ActionExport = "export" + ActionWorkflowRun = "workflow_run" + ActionAdmin = "admin" +) + +// ActionMapping describes the Markitect policy-gateway action contract. +type ActionMapping struct { + Action string `json:"action"` + Capabilities []api.Capability `json:"capabilities"` + Planes []api.Plane `json:"planes"` + ExposureModes []api.ExposureMode `json:"exposure_modes"` + AllowedEffects []api.DecisionEffect `json:"allowed_effects"` + RequiredContext []api.Condition `json:"required_context,omitempty"` +} + +// ActionVocabulary is the canonical Markitect action vocabulary. +var ActionVocabulary = []ActionMapping{ + { + Action: ActionRead, + Capabilities: []api.Capability{api.CapabilityView}, + Planes: []api.Plane{api.PlaneData}, + ExposureModes: []api.ExposureMode{api.ExposureModeMetadata, api.ExposureModeMasked, api.ExposureModePlaintext}, + AllowedEffects: []api.DecisionEffect{api.DecisionEffectAllow, api.DecisionEffectDeny, api.DecisionEffectRedact}, + }, + { + Action: ActionQuery, + Capabilities: []api.Capability{api.CapabilityViewCollection, api.CapabilityObserve}, + Planes: []api.Plane{api.PlaneData}, + ExposureModes: []api.ExposureMode{api.ExposureModeMetadata, api.ExposureModeAggregated, api.ExposureModeMasked}, + AllowedEffects: []api.DecisionEffect{api.DecisionEffectAllow, api.DecisionEffectDeny, api.DecisionEffectRedact}, + }, + { + Action: ActionSearch, + Capabilities: []api.Capability{api.CapabilityViewCollection, api.CapabilityObserve}, + Planes: []api.Plane{api.PlaneData}, + ExposureModes: []api.ExposureMode{api.ExposureModeMetadata, api.ExposureModeAggregated, api.ExposureModeMasked}, + AllowedEffects: []api.DecisionEffect{api.DecisionEffectAllow, api.DecisionEffectDeny, api.DecisionEffectRedact}, + }, + { + Action: ActionPackage, + Capabilities: []api.Capability{api.CapabilityCreate, api.CapabilityBind, api.CapabilityViewCollection}, + Planes: []api.Plane{api.PlaneIntent, api.PlaneData}, + ExposureModes: []api.ExposureMode{api.ExposureModeMetadata, api.ExposureModeMasked}, + AllowedEffects: []api.DecisionEffect{api.DecisionEffectAllow, api.DecisionEffectDeny, api.DecisionEffectAuditOnly}, + RequiredContext: []api.Condition{api.ConditionLogged}, + }, + { + Action: ActionActivateContext, + Capabilities: []api.Capability{api.CapabilityUse, api.CapabilityExecute}, + Planes: []api.Plane{api.PlaneIntent, api.PlanePolicy}, + ExposureModes: []api.ExposureMode{api.ExposureModeMetadata, api.ExposureModeMasked}, + AllowedEffects: []api.DecisionEffect{api.DecisionEffectAllow, api.DecisionEffectDeny, api.DecisionEffectAuditOnly}, + RequiredContext: []api.Condition{api.ConditionLogged, api.ConditionPurposeBound}, + }, + { + Action: ActionExport, + Capabilities: []api.Capability{api.CapabilityExport}, + Planes: []api.Plane{api.PlaneData, api.PlaneAudit}, + ExposureModes: []api.ExposureMode{api.ExposureModeExportable, api.ExposureModePlaintext}, + AllowedEffects: []api.DecisionEffect{api.DecisionEffectAllow, api.DecisionEffectDeny, api.DecisionEffectAuditOnly}, + RequiredContext: []api.Condition{api.ConditionMFARequired, api.ConditionLogged}, + }, + { + Action: ActionWorkflowRun, + Capabilities: []api.Capability{api.CapabilityExecute, api.CapabilityOperate}, + Planes: []api.Plane{api.PlaneExecution, api.PlaneData, api.PlaneAudit}, + ExposureModes: []api.ExposureMode{api.ExposureModeMetadata, api.ExposureModeMasked, api.ExposureModePlaintext}, + AllowedEffects: []api.DecisionEffect{api.DecisionEffectAllow, api.DecisionEffectDeny, api.DecisionEffectAuditOnly}, + RequiredContext: []api.Condition{api.ConditionLogged}, + }, + { + Action: ActionAdmin, + Capabilities: []api.Capability{api.CapabilityConfigure, api.CapabilityGrant, api.CapabilityRevoke, api.CapabilityAudit}, + Planes: []api.Plane{api.PlaneConfiguration, api.PlaneIdentity, api.PlanePolicy, api.PlaneAudit}, + ExposureModes: []api.ExposureMode{api.ExposureModeMetadata, api.ExposureModePlaintext}, + AllowedEffects: []api.DecisionEffect{api.DecisionEffectAllow, api.DecisionEffectDeny, api.DecisionEffectAuditOnly}, + RequiredContext: []api.Condition{api.ConditionMFARequired, api.ConditionLogged}, + }, +} + +// LookupAction returns the canonical mapping for an action. +func LookupAction(action string) (ActionMapping, bool) { + for _, mapping := range ActionVocabulary { + if mapping.Action == action { + return mapping, true + } + } + return ActionMapping{}, false +} + +// ActionDefinitions returns protected-system action definitions for Markitect. +func ActionDefinitions() []api.ActionDefinition { + out := make([]api.ActionDefinition, 0, len(ActionVocabulary)) + for _, mapping := range ActionVocabulary { + out = append(out, api.ActionDefinition{ + Name: mapping.Action, + Capabilities: append([]api.Capability(nil), mapping.Capabilities...), + Planes: append([]api.Plane(nil), mapping.Planes...), + ExposureModes: append([]api.ExposureMode(nil), mapping.ExposureModes...), + Metadata: map[string]any{ + "allowed_effects": append([]api.DecisionEffect(nil), mapping.AllowedEffects...), + "required_context": append([]api.Condition(nil), mapping.RequiredContext...), + }, + }) + } + return out +} diff --git a/internal/markitect/actions_test.go b/internal/markitect/actions_test.go new file mode 100644 index 0000000..ecee770 --- /dev/null +++ b/internal/markitect/actions_test.go @@ -0,0 +1,55 @@ +package markitect_test + +import ( + "slices" + "testing" + + "github.com/netkingdom/flex-auth/internal/markitect" + "github.com/netkingdom/flex-auth/pkg/api" +) + +func TestActionVocabularySeparatesReadFromExport(t *testing.T) { + read, ok := markitect.LookupAction(markitect.ActionRead) + if !ok { + t.Fatal("read action not found") + } + if slices.Contains(read.Capabilities, api.CapabilityExport) { + t.Fatalf("read capabilities = %v; must not include Export", read.Capabilities) + } + if !slices.Contains(read.ExposureModes, api.ExposureModeMetadata) || + !slices.Contains(read.ExposureModes, api.ExposureModeMasked) || + !slices.Contains(read.ExposureModes, api.ExposureModePlaintext) { + t.Fatalf("read exposure modes = %v; want metadata, masked, and plaintext", read.ExposureModes) + } + + export, ok := markitect.LookupAction(markitect.ActionExport) + if !ok { + t.Fatal("export action not found") + } + if !slices.Contains(export.Capabilities, api.CapabilityExport) { + t.Fatalf("export capabilities = %v; want Export", export.Capabilities) + } + if !slices.Contains(export.ExposureModes, api.ExposureModeExportable) { + t.Fatalf("export exposure modes = %v; want Exportable", export.ExposureModes) + } + if !slices.Contains(export.RequiredContext, api.ConditionMFARequired) { + t.Fatalf("export required context = %v; want MFARequired", export.RequiredContext) + } +} + +func TestActionDefinitionsMirrorVocabulary(t *testing.T) { + definitions := markitect.ActionDefinitions() + if len(definitions) != len(markitect.ActionVocabulary) { + t.Fatalf("definitions len = %d; want %d", len(definitions), len(markitect.ActionVocabulary)) + } + + for i, mapping := range markitect.ActionVocabulary { + definition := definitions[i] + if definition.Name != mapping.Action { + t.Fatalf("definitions[%d].Name = %q; want %q", i, definition.Name, mapping.Action) + } + if !slices.Equal(definition.Capabilities, mapping.Capabilities) { + t.Fatalf("%s capabilities = %v; want %v", mapping.Action, definition.Capabilities, mapping.Capabilities) + } + } +} diff --git a/pkg/api/resource_manifest_test.go b/pkg/api/resource_manifest_test.go index 6f672fb..a3e9973 100644 --- a/pkg/api/resource_manifest_test.go +++ b/pkg/api/resource_manifest_test.go @@ -106,6 +106,15 @@ func TestMarkitectProtectedSystemNamespaceExampleParses(t *testing.T) { if len(got.ResourceTypes) != 8 { t.Fatalf("ResourceTypes len = %d; want 8", len(got.ResourceTypes)) } + if len(got.Actions) != 8 { + t.Fatalf("Actions len = %d; want 8", len(got.Actions)) + } + if got.Actions[0].Name != "read" || got.Actions[0].Capabilities[0] != api.CapabilityView { + t.Fatalf("first Action = %+v; want read/View", got.Actions[0]) + } + if got.Actions[5].Name != "export" || got.Actions[5].Capabilities[0] != api.CapabilityExport { + t.Fatalf("export Action = %+v; want export/Export", got.Actions[5]) + } if got.ResourceTypes[0].Name != "knowledge_base" || got.ResourceTypes[0].ScopeLevel != api.ScopeLevelWorkspace { t.Fatalf("first ResourceType = %+v; want knowledge_base Workspace", got.ResourceTypes[0]) } diff --git a/workplans/FLEX-WP-0003-markitect-consumer-integration.md b/workplans/FLEX-WP-0003-markitect-consumer-integration.md index 2445263..b1a8036 100644 --- a/workplans/FLEX-WP-0003-markitect-consumer-integration.md +++ b/workplans/FLEX-WP-0003-markitect-consumer-integration.md @@ -78,7 +78,7 @@ manifest cannot be mapped cleanly enough for conformance checks. ```task id: FLEX-WP-0003-T003 -status: todo +status: done priority: high state_hub_task_id: "cfc78bbb-5425-4780-a860-9109df62ea37" ```