contract

package
v1.6.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 11, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package contract defines the declarative, single-endpoint contract for the admin dashboard: contributor manifests, request/response envelopes, the permission model, the slot/graph composition rules, and the per-contributor version negotiation protocol.

See DESIGN.md in this directory for the spec this implements.

envelope.go

graph.go

manifest.go

registry.go

slots.go

Index

Constants

View Source
const MaxSlotDepth = 8

MaxSlotDepth is the maximum nesting depth of graph nodes; trees deeper than this are rejected at registration.

Variables

View Source
var (
	ErrBadRequest         = &Error{Code: CodeBadRequest}
	ErrUnauthenticated    = &Error{Code: CodeUnauthenticated}
	ErrPermissionDenied   = &Error{Code: CodePermissionDenied}
	ErrNotFound           = &Error{Code: CodeNotFound}
	ErrConflict           = &Error{Code: CodeConflict}
	ErrRateLimited        = &Error{Code: CodeRateLimited}
	ErrUnsupportedVersion = &Error{Code: CodeUnsupportedVersion}
	ErrUnavailable        = &Error{Code: CodeUnavailable}
	ErrInternal           = &Error{Code: CodeInternal}
)

Sentinel errors for use with errors.Is.

View Source
var DefaultSlotCatalog = map[string]IntentKindDef{
	"page.shell": {
		Slots: map[string]SlotDef{
			"header": {
				Accepts:     inlineAdornmentIntents,
				Cardinality: CardinalityOne,
			},
			"main": {
				Accepts:     pageMainIntents,
				Cardinality: CardinalityMany,
			},
		},
	},
	"resource.list": {
		Slots: map[string]SlotDef{
			"rowActions":   {Accepts: []string{"action.button", "action.menu", "action.divider"}, Cardinality: CardinalityMany},
			"detailDrawer": {Accepts: []string{"form.edit", "resource.detail", "custom"}, Cardinality: CardinalityOne, Extensible: true},
			"filters": {
				Accepts: []string{
					"filter.search", "filter.select", "filter.date",
					"molecule.search-bar", "molecule.combobox", "molecule.date-picker",
					"custom",
				},
				Cardinality: CardinalityMany,
				Extensible:  true,
			},
			"bulkActions": {
				Accepts:     []string{"action.button", "action.menu"},
				Cardinality: CardinalityMany,
				Extensible:  true,
			},
			"header": {
				Accepts:     inlineAdornmentIntents,
				Cardinality: CardinalityOne,
			},
			"pagination": {
				Accepts:     []string{"molecule.pagination", "pagination.cursor", "pagination.offset"},
				Cardinality: CardinalityOne,
			},
		},
	},

	"resource.detail": {
		Slots: map[string]SlotDef{
			"header": {
				Accepts:     []string{"detail.header", "layout.section", "atom.heading", "molecule.breadcrumb", "custom"},
				Cardinality: CardinalityOne,
			},
			"sections": {
				Accepts: []string{
					"detail.section", "layout.section", "layout.card", "layout.tabs", "layout.accordion",
					"resource.list", "resource.detail", "form.edit",
					"organism.data-grid", "organism.dynamic-form", "organism.timeline", "organism.chart",
					"molecule.alert", "molecule.empty-state",
					"audit.tail", "custom",
				},
				Cardinality: CardinalityMany,
				Extensible:  true,
			},
			"actions": {
				Accepts:     []string{"action.button", "action.menu", "action.divider", "atom.button", "confirm.dialog"},
				Cardinality: CardinalityMany,
				Extensible:  true,
			},
			"extensions": {
				Accepts: []string{
					"detail.section", "layout.section", "layout.card",
					"resource.list", "resource.detail", "form.edit",
					"custom",
				},
				Cardinality: CardinalityMany,
				Extensible:  true,
			},
		},
	},
	"dashboard.grid": {
		Slots: map[string]SlotDef{
			"widgets": {
				Accepts: []string{
					"metric.counter", "metric.gauge", "audit.tail",
					"dashboard.stat", "dashboard.recentlist",
					"molecule.stat-card",
					"organism.chart", "organism.timeline", "organism.calendar",
					"custom",
				},
				Cardinality: CardinalityMany,
				Extensible:  true,
			},
		},
	},
	"form.edit": {
		Slots: map[string]SlotDef{
			"fields": {Accepts: formFieldIntents, Cardinality: CardinalityMany, Extensible: true},
			"actions": {
				Accepts:     []string{"action.button", "atom.button"},
				Cardinality: CardinalityMany,
				Extensible:  true,
			},
			"sections": {
				Accepts:     []string{"layout.section", "layout.card"},
				Cardinality: CardinalityMany,
			},
		},
	},

	"auth.login.form":           {Slots: map[string]SlotDef{}},
	"auth.signin-form":          {Slots: map[string]SlotDef{}},
	"auth.signup-form":          {Slots: map[string]SlotDef{}},
	"auth.magic-link-form":      {Slots: map[string]SlotDef{}},
	"auth.oauth-buttons":        {Slots: map[string]SlotDef{}},
	"auth.forgot-password-form": {Slots: map[string]SlotDef{}},
	"auth.reset-password-form":  {Slots: map[string]SlotDef{}},
	"auth.setup-form":           {Slots: map[string]SlotDef{}},
	"auth.dynamic-signup-form":  {Slots: map[string]SlotDef{}},
	"auth.two-factor-setup":     {Slots: map[string]SlotDef{}},
	"auth.passkey-prompt":       {Slots: map[string]SlotDef{}},
	"auth.session-list":         {Slots: map[string]SlotDef{}},
	"auth.user-button":          {Slots: map[string]SlotDef{}},
	"auth.account-menu":         {Slots: map[string]SlotDef{}},
	"auth.org-switcher":         {Slots: map[string]SlotDef{}},
	"auth.org-profile":          {Slots: map[string]SlotDef{}},

	"auth.tabs": {
		Slots: map[string]SlotDef{
			"tabs": {
				Accepts: []string{
					"auth.signin-form", "auth.signup-form",
					"auth.magic-link-form", "auth.oauth-buttons",
					"auth.passkey-prompt", "custom",
				},
				Cardinality: CardinalityMany,
				Extensible:  true,
			},
		},
	},

	"layout.tabs": {
		Slots: map[string]SlotDef{
			"panels": {
				Accepts: []string{
					"layout.section", "layout.card", "layout.stack",
					"resource.list", "resource.detail", "form.edit",
					"organism.data-grid", "organism.dynamic-form", "organism.chart",
					"custom",
				},
				Cardinality: CardinalityMany,
				Extensible:  true,
			},
		},
	},
	"layout.accordion": {
		Slots: map[string]SlotDef{
			"items": {
				Accepts:     []string{"layout.section", "layout.card", "custom"},
				Cardinality: CardinalityMany,
				Extensible:  true,
			},
		},
	},
	"layout.card": {
		Slots: map[string]SlotDef{
			"header":  {Accepts: inlineAdornmentIntents, Cardinality: CardinalityOne},
			"content": {Accepts: broadContentIntents, Cardinality: CardinalityMany, Extensible: true},
			"footer":  {Accepts: inlineAdornmentIntents, Cardinality: CardinalityOne},
		},
	},
	"layout.section": {
		Slots: map[string]SlotDef{
			"content": {Accepts: broadContentIntents, Cardinality: CardinalityMany, Extensible: true},
		},
	},
	"layout.page": {
		Slots: map[string]SlotDef{
			"header": {Accepts: inlineAdornmentIntents, Cardinality: CardinalityOne},
			"body":   {Accepts: broadContentIntents, Cardinality: CardinalityMany, Extensible: true},
			"footer": {Accepts: inlineAdornmentIntents, Cardinality: CardinalityOne},
		},
	},
	"layout.stack":     {Slots: map[string]SlotDef{"children": {Accepts: broadContentIntents, Cardinality: CardinalityMany, Extensible: true}}},
	"layout.row":       {Slots: map[string]SlotDef{"children": {Accepts: broadContentIntents, Cardinality: CardinalityMany, Extensible: true}}},
	"layout.column":    {Slots: map[string]SlotDef{"children": {Accepts: broadContentIntents, Cardinality: CardinalityMany, Extensible: true}}},
	"layout.grid":      {Slots: map[string]SlotDef{"children": {Accepts: broadContentIntents, Cardinality: CardinalityMany, Extensible: true}}},
	"layout.container": {Slots: map[string]SlotDef{"children": {Accepts: broadContentIntents, Cardinality: CardinalityMany, Extensible: true}}},
	"layout.split":     {Slots: map[string]SlotDef{"children": {Accepts: broadContentIntents, Cardinality: CardinalityMany, Extensible: true}}},

	"detail.header": {Slots: map[string]SlotDef{}},
	"detail.section": {
		Slots: map[string]SlotDef{
			"content": {Accepts: broadContentIntents, Cardinality: CardinalityMany, Extensible: true},
		},
	},

	"editor.code":        {Slots: map[string]SlotDef{}},
	"editor.formBuilder": {Slots: map[string]SlotDef{}},

	"confirm.dialog": {
		Slots: map[string]SlotDef{
			"trigger": {Accepts: []string{"action.button", "atom.button"}, Cardinality: CardinalityOne},
			"body":    {Accepts: []string{"layout.section", "atom.text", "form.edit", "custom"}, Cardinality: CardinalityMany},
		},
	},

	"metric.gauge":         {Slots: map[string]SlotDef{}},
	"dashboard.stat":       {Slots: map[string]SlotDef{}},
	"dashboard.recentlist": {Slots: map[string]SlotDef{}},

	"filter.search": {Slots: map[string]SlotDef{}},
	"filter.select": {Slots: map[string]SlotDef{}},
	"filter.date":   {Slots: map[string]SlotDef{}},

	"pagination.cursor": {Slots: map[string]SlotDef{}},
	"pagination.offset": {Slots: map[string]SlotDef{}},

	"field.json":        {Slots: map[string]SlotDef{}},
	"field.permissions": {Slots: map[string]SlotDef{}},

	"settings.tabs":  {Slots: map[string]SlotDef{}},
	"settings.panel": {Slots: map[string]SlotDef{}},

	"feature.toggles": {Slots: map[string]SlotDef{}},
}

DefaultSlotCatalog is the v1 catalog of built-in intent kinds and their slots. Adding a new built-in intent kind here is a shell-version bump (adds new renderer behavior). The catalog mirrors the React renderer registry in /shell/src/intents/register.ts — every intent kind exposed to manifest authors should appear in both places.

Functions

func PermissionsHash

func PermissionsHash(user *dashauth.UserInfo) string

PermissionsHash returns a stable, order-independent hash of a user's roles and scopes. Used as part of the graph cache key so that users with the same effective permissions share a cache entry. Claims are NOT included because the contract treats only role/scope as graph-shape-determining.

func UnmarshalManifestForTest

func UnmarshalManifestForTest(b []byte, m *ContractManifest) error

UnmarshalManifestForTest is a test helper exposed for use by sibling packages. It is not part of the package's runtime API; production code should not call it.

Types

type Action

type Action struct {
	Contributor string
	Intent      string
	Kind        Kind
	Capability  Capability
	Resource    map[string]any
}

Action is the operation being authorized. Kind is the wire-side envelope discriminator (graph/query/command/subscribe) so HTTP and SSE callers can pass req.Kind directly. Note this is the wire Kind, not the manifest's IntentKind — the values mostly overlap but "subscription" (manifest) is "subscribe" (wire).

type AppInfo added in v1.6.6

type AppInfo struct {
	DisplayName string `yaml:"displayName" json:"displayName"`
	Slug        string `yaml:"slug,omitempty" json:"slug,omitempty"`
	Root        bool   `yaml:"root,omitempty" json:"root,omitempty"`
	Icon        string `yaml:"icon,omitempty" json:"icon,omitempty"`
	Priority    int    `yaml:"priority,omitempty" json:"priority,omitempty"`
	Home        string `yaml:"home,omitempty" json:"home,omitempty"`
}

AppInfo describes how a contributor presents itself in the app switcher. All fields are display-only — the contract dispatch path is unaffected.

contributor:
  name: core-contract
  app:
    displayName: Forge
    root: true         # this app owns the bare URL; no /@slug prefix
    icon: forge
    priority: 0
    home: /

contributor:
  name: auth
  app:
    displayName: Authsome
    slug: authsome     # routes become /@authsome/...
    icon: shield
    priority: 10
    home: /users

Root marks the platform app: its routes are NOT URL-prefixed, so /, /health, etc. stay bare. There must be at most one root app per dashboard deployment (the registry doesn't enforce this today; behaviour on conflict is "first registered wins" via the natural ordering in apps.list).

Slug controls URL namespacing for non-root apps: when set, every top-level graph route the contributor declares is projected to /@<slug><route> on the wire, and Home is projected the same way. Defaults to Contributor.Name when unset. Has no effect on a root app — root URLs are always bare regardless of slug.

func (*AppInfo) ResolvedSlug added in v1.6.6

func (a *AppInfo) ResolvedSlug(contributorName string) string

ResolvedSlug returns the slug that should be used for URL prefixing, falling back to the contributor name when no explicit slug is set. Returns "" for root apps so callers can rely on a non-empty slug signalling "this app gets URL prefixing" without a separate `if app.Root` branch.

type AuditEmitter

type AuditEmitter interface {
	Emit(ctx context.Context, rec AuditRecord)
}

AuditEmitter ships audit records to durable storage. Slice (b) wires the chronicle implementation; slice (a) ships log-based and noop variants.

func NewLogAuditEmitter

func NewLogAuditEmitter(w io.Writer) AuditEmitter

NewLogAuditEmitter returns an emitter that writes a stable line format to w. Suitable for development and as a fallback when no chronicle backend is wired.

func NewRecordingAuditEmitter

func NewRecordingAuditEmitter(inner AuditEmitter, store AuditStore) AuditEmitter

NewRecordingAuditEmitter returns an emitter that fans out to inner (typically the log emitter) and also persists to store. Either may be nil; both nil is a noop.

type AuditFilter

type AuditFilter struct {
	Limit       int
	Contributor string
	Intent      string
	User        string
	Result      string
}

AuditFilter narrows audit.list results. All fields are optional. Limit is clamped to [1, 1000]; zero defaults to 200.

type AuditRecord

type AuditRecord struct {
	Time          time.Time
	Contributor   string
	Intent        string
	IntentVersion int
	Subject       string // resource id when known
	User          string // user identity (subject from UserInfo)
	Result        string // ok | error
	LatencyMs     int64
	Payload       map[string]any // pre-redaction; subject to per-intent redaction list
	CorrelationID string
}

AuditRecord is one auditable command invocation.

type AuditStore

type AuditStore interface {
	Append(rec AuditRecord)
	List(filter AuditFilter) []AuditRecord
	// Subscribe returns a channel that receives every Append from now on, plus
	// a cancel func that closes the channel and unregisters the subscriber.
	// Slow subscribers drop events rather than block writers — audit is
	// telemetry, not the source of truth.
	Subscribe() (<-chan AuditRecord, func())
}

AuditStore is the persistent (process-local for slice (k)) view of audit records. It exists to back the audit.list query and audit.tail subscription the dashboard exposes; production deployments swap the in-memory impl for a durable backend when one is wired.

func NewInMemoryAuditStore

func NewInMemoryAuditStore(cap int) AuditStore

NewInMemoryAuditStore returns a store that keeps the most recent `cap` records in a ring buffer. cap <= 0 defaults to 1000.

type CacheHint

type CacheHint struct {
	StaleTime string `json:"staleTime,omitempty"`
}

CacheHint communicates how long the shell can serve stale data for a query.

type Capability

type Capability string

Capability is the data-classification of an intent's effects. It composes with IntentKind: a command must be capability=write; a query/subscription must be capability=read; a graph must be capability=render.

const (
	CapRead   Capability = "read"
	CapWrite  Capability = "write"
	CapRender Capability = "render"
)

type Cardinality

type Cardinality string

Cardinality describes how many fills a slot accepts.

const (
	CardinalityOne  Cardinality = "one"
	CardinalityMany Cardinality = "many"
)

type ContractManifest

type ContractManifest struct {
	SchemaVersion int              `yaml:"schemaVersion" json:"schemaVersion"`
	Contributor   Contributor      `yaml:"contributor"   json:"contributor"`
	Queries       map[string]Query `yaml:"queries,omitempty" json:"queries,omitempty"`
	Intents       []Intent         `yaml:"intents"       json:"intents"`
	Graph         []GraphNode      `yaml:"graph,omitempty" json:"graph,omitempty"`
	Extends       []Extension      `yaml:"extends,omitempty" json:"extends,omitempty"`
}

ContractManifest is the top-level YAML each contributor publishes.

type Contributor

type Contributor struct {
	Name         string          `yaml:"name"         json:"name"`
	Envelope     EnvelopeSupport `yaml:"envelope"     json:"envelope"`
	Capabilities []string        `yaml:"capabilities,omitempty" json:"capabilities,omitempty"`
	App          *AppInfo        `yaml:"app,omitempty"          json:"app,omitempty"`
}

Contributor names a single contributor and declares its supported envelope versions.

App, when set, opts this contributor into the dashboard's app switcher. A contributor without an App block is a "library" contributor — it may declare intents and inject nodes into other contributors' graphs via Extends, but its own routes (if any) won't appear as a switchable app in the sidebar header. The pilot and authsome both set App so they surface as first-class apps; helper contributors (e.g. a future shared "design system" contributor) can stay invisible.

type DataBinding

type DataBinding struct {
	QueryRef string                 `yaml:"-" json:"queryRef,omitempty"`
	Intent   string                 `yaml:"intent,omitempty"  json:"intent,omitempty"`
	Kind     IntentKind             `yaml:"-"                 json:"kind,omitempty"`
	Params   map[string]ParamSource `yaml:"params,omitempty"  json:"params,omitempty"`
}

DataBinding is either an inline {intent, params} pair or a named query reference. YAML supports both shapes:

data: queries.userList
data: { intent: users.list, params: {...} }

Kind is not authored in YAML; it's stamped at merge time from the referenced intent's declared kind so the React shell can pick the right hook (useContractQuery for query, useSubscription for subscription) without having to chase the manifest's intent table client-side.

func (*DataBinding) UnmarshalYAML

func (d *DataBinding) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML accepts either a scalar (treated as a named query reference) or a mapping with the inline {intent, params} form.

type Decision

type Decision struct {
	// Allow reports whether access is granted.
	Allow bool
	// Reason is a short, human-readable explanation. Surfaced in audit logs
	// and (optionally) in error responses.
	Reason string
	// Redactions lists JSONPath-like field paths that must be redacted from
	// the response payload even when Allow is true. Empty when no redactions
	// apply.
	Redactions []string
}

Decision is the Warden's verdict.

type Deprecation

type Deprecation struct {
	IntentVersion int    `json:"intentVersion"`
	RemoveAfter   string `json:"removeAfter"`
}

Deprecation surfaces a "this version will be removed" hint to the shell.

type EnvelopeSupport

type EnvelopeSupport struct {
	Supports  []string `yaml:"supports"  json:"supports"`
	Preferred string   `yaml:"preferred" json:"preferred"`
}

EnvelopeSupport declares which envelope versions this contributor can speak.

type Error

type Error struct {
	Code          ErrorCode      `json:"code"`
	Message       string         `json:"message,omitempty"`
	Details       map[string]any `json:"details,omitempty"`
	Retryable     bool           `json:"retryable,omitempty"`
	CorrelationID string         `json:"correlationID,omitempty"`
	Redactions    []string       `json:"redactions,omitempty"`
}

Error is the canonical contract error type. It serializes to the wire "error" object documented in DESIGN.md.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is matches sentinel errors by Code.

type ErrorCode

type ErrorCode string

ErrorCode is a canonical, wire-stable code for contract errors. Contributor-specific codes are namespaced like "auth.SESSION_EXPIRED".

const (
	CodeBadRequest         ErrorCode = "BAD_REQUEST"
	CodeUnauthenticated    ErrorCode = "UNAUTHENTICATED"
	CodePermissionDenied   ErrorCode = "PERMISSION_DENIED"
	CodeNotFound           ErrorCode = "NOT_FOUND"
	CodeConflict           ErrorCode = "CONFLICT"
	CodeRateLimited        ErrorCode = "RATE_LIMITED"
	CodeUnsupportedVersion ErrorCode = "UNSUPPORTED_VERSION"
	CodeUnavailable        ErrorCode = "UNAVAILABLE"
	CodeInternal           ErrorCode = "INTERNAL"
)

type ErrorResponse

type ErrorResponse struct {
	OK       bool   `json:"ok"`
	Envelope string `json:"envelope"`
	Error    *Error `json:"error"`
}

ErrorResponse is the wire envelope for failed POST responses.

type Extension

type Extension struct {
	Target ExtensionTarget `yaml:"target" json:"target"`
	Slot   string          `yaml:"slot"   json:"slot"` // dotted path: "detailDrawer.fields"
	Add    []GraphNode     `yaml:"add"    json:"add"`
}

Extension declares that this contributor wants to add nodes into another contributor's slot.

type ExtensionTarget

type ExtensionTarget struct {
	Contributor string `yaml:"contributor" json:"contributor"`
	Intent      string `yaml:"intent"      json:"intent"`
	Route       string `yaml:"route,omitempty" json:"route,omitempty"`
}

ExtensionTarget identifies the host node to extend.

type GraphBuilder

type GraphBuilder struct {
	// contains filtered or unexported fields
}

GraphBuilder produces a per-(route, principal) filtered graph by walking the merged graph from the registry and dropping nodes whose visibleWhen predicates fail. EnabledWhen is preserved as an annotation (it does not strip the node); the React shell honors it for disabled-but-visible UI states.

func NewGraphBuilder

func NewGraphBuilder(reg Registry, wardens WardenRegistry) *GraphBuilder

NewGraphBuilder returns a builder bound to the given registry and warden registry.

func (*GraphBuilder) Build

func (b *GraphBuilder) Build(ctx context.Context, contributor, route string, p Principal) (*GraphNode, error)

Build returns the filtered graph rooted at the given route for the given principal. Returns ErrNotFound if no contributor owns the route, or ErrPermissionDenied if the root node itself is filtered out by the principal's permissions.

func (*GraphBuilder) BuildWithParams

func (b *GraphBuilder) BuildWithParams(ctx context.Context, contributor, route string, p Principal) (*GraphNode, map[string]string, error)

BuildWithParams is Build plus the route-pattern params extracted from the matched route. For exact-route matches the map is empty (non-nil); for :name-style routes it carries the placeholder values. Slice (j) added this so the transport handler can surface params in ResponseMeta.

type GraphCache

type GraphCache struct {
	// contains filtered or unexported fields
}

GraphCache is a small LRU+TTL cache. Bust on contributor manifest reload.

func NewGraphCache

func NewGraphCache(maxEntries int, ttl time.Duration) *GraphCache

NewGraphCache creates a cache with the given max size and TTL per entry. TTL of 0 disables expiry.

func (*GraphCache) BustAll

func (c *GraphCache) BustAll()

BustAll clears the cache. Call after a contributor manifest reload or shell deploy.

func (*GraphCache) Get

func (c *GraphCache) Get(k GraphCacheKey) (*GraphNode, bool)

func (*GraphCache) Put

func (c *GraphCache) Put(k GraphCacheKey, v *GraphNode)

type GraphCacheKey

type GraphCacheKey struct {
	Route           string
	PermissionsHash string
	ShellVersion    string
}

GraphCacheKey is the (route, permissionsHash, shellVersion) tuple keyed by the cache.

type GraphNode

type GraphNode struct {
	Route       string                 `yaml:"route,omitempty"       json:"route,omitempty"` // top-level only
	Intent      string                 `yaml:"intent"                json:"intent"`
	Title       string                 `yaml:"title,omitempty"       json:"title,omitempty"`
	Nav         *NavConfig             `yaml:"nav,omitempty"         json:"nav,omitempty"`
	Root        bool                   `yaml:"root,omitempty"        json:"root,omitempty"`
	Data        *DataBinding           `yaml:"data,omitempty"        json:"data,omitempty"`
	Props       map[string]any         `yaml:"props,omitempty"       json:"props,omitempty"`
	Slots       map[string][]GraphNode `yaml:"slots,omitempty"       json:"slots,omitempty"`
	VisibleWhen *Predicate             `yaml:"visibleWhen,omitempty" json:"visibleWhen,omitempty"`
	EnabledWhen *Predicate             `yaml:"enabledWhen,omitempty" json:"enabledWhen,omitempty"`
	Op          string                 `yaml:"op,omitempty"          json:"op,omitempty"` // for action nodes
	Payload     map[string]ParamSource `yaml:"payload,omitempty"     json:"payload,omitempty"`
	Component   string                 `yaml:"component,omitempty"   json:"component,omitempty"` // intent: custom escape hatch
	Src         string                 `yaml:"src,omitempty"         json:"src,omitempty"`       // intent: iframe escape hatch
	Sandbox     []string               `yaml:"sandbox,omitempty"     json:"sandbox,omitempty"`
	Protocol    string                 `yaml:"protocol,omitempty"    json:"protocol,omitempty"`
}

GraphNode is a single node in the UI graph (an intent invocation with slot fills).

type Intent

type Intent struct {
	Name        string           `yaml:"name"        json:"name"`
	Kind        IntentKind       `yaml:"kind"        json:"kind"`
	Version     int              `yaml:"version"     json:"version"`
	Capability  Capability       `yaml:"capability"  json:"capability"`
	Requires    Predicate        `yaml:"requires,omitempty" json:"requires,omitempty"`
	Schema      IntentSchema     `yaml:"schema,omitempty" json:"schema,omitempty"`
	Mode        SubscriptionMode `yaml:"mode,omitempty" json:"mode,omitempty"`               // subscription only
	Invalidates []string         `yaml:"invalidates,omitempty" json:"invalidates,omitempty"` // command only
	Audit       *bool            `yaml:"audit,omitempty"       json:"audit,omitempty"`       // default true for commands
	Deprecated  *Deprecation     `yaml:"deprecated,omitempty" json:"deprecated,omitempty"`
}

Intent declares a single named operation and its security/version metadata.

type IntentKind

type IntentKind string

IntentKind is the wire-level discriminator declared on every intent. It must be consistent with the request envelope Kind at dispatch time.

const (
	IntentKindGraph        IntentKind = "graph"
	IntentKindQuery        IntentKind = "query"
	IntentKindCommand      IntentKind = "command"
	IntentKindSubscription IntentKind = "subscription"
)

type IntentKindDef

type IntentKindDef struct {
	Slots map[string]SlotDef
}

IntentKindDef declares the slots of a built-in intent kind.

type IntentSchema

type IntentSchema struct {
	Input  map[string]any `yaml:"input,omitempty"  json:"input,omitempty"`
	Output any            `yaml:"output,omitempty" json:"output,omitempty"`
}

IntentSchema is loose by design: contributors describe their input/output shapes; validation against this is opt-in (slice (b) wires it).

type Kind

type Kind string

Kind discriminates request/response semantics on the wire. A kind is enforced against the intent's declared Capability at dispatch time.

const (
	KindGraph     Kind = "graph"
	KindQuery     Kind = "query"
	KindCommand   Kind = "command"
	KindSubscribe Kind = "subscribe"
)
type NavConfig struct {
	Group    string `yaml:"group,omitempty"    json:"group,omitempty"`
	Icon     string `yaml:"icon,omitempty"     json:"icon,omitempty"`
	Priority int    `yaml:"priority,omitempty" json:"priority,omitempty"`
	Badge    string `yaml:"badge,omitempty"    json:"badge,omitempty"`
}

NavConfig is per-route nav metadata; mirrors today's contributor.NavItem fields.

type NoopAuditEmitter

type NoopAuditEmitter struct{}

NoopAuditEmitter is the disabled-audit implementation.

func (NoopAuditEmitter) Emit

type ParamSource

type ParamSource struct {
	Value any    `yaml:"value,omitempty" json:"value,omitempty"`
	From  string `yaml:"from,omitempty"  json:"from,omitempty"` // route.X | parent.X | state.X | session.X
}

ParamSource describes where a parameter value comes from. Exactly one of Value/From is set; YAML uses { from: route.tenant } or a literal.

func (*ParamSource) UnmarshalYAML

func (p *ParamSource) UnmarshalYAML(value *yaml.Node) error

UnmarshalYAML accepts either a scalar (treated as the From source) or a mapping with the explicit {value} or {from} form.

type Predicate

type Predicate struct {
	All    []string `yaml:"all,omitempty"    json:"all,omitempty"`
	Any    []string `yaml:"any,omitempty"    json:"any,omitempty"`
	Not    []string `yaml:"not,omitempty"    json:"not,omitempty"`
	Warden string   `yaml:"warden,omitempty" json:"warden,omitempty"`
}

Predicate is the boolean access expression: any of all/any/not, plus an optional named Warden delegate. An empty Predicate evaluates to allow.

func (*Predicate) Allow

func (p *Predicate) Allow(user *dashauth.UserInfo, wardenResult *Decision) bool

Allow evaluates the boolean predicate against a UserInfo. The wardenResult argument is the optional second-pass Warden decision; pass nil to skip. An empty predicate (no all/any/not) always allows.

type Principal

type Principal struct {
	User   *dashauth.UserInfo
	Claims map[string]any
}

Principal is the caller identity passed to Wardens and the predicate engine.

func PrincipalFor

func PrincipalFor(user *dashauth.UserInfo) Principal

PrincipalFor builds a Principal from a UserInfo, copying claims for safety.

type Query

type Query struct {
	Intent string                 `yaml:"intent" json:"intent"`
	Params map[string]ParamSource `yaml:"params,omitempty" json:"params,omitempty"`
	Cache  *QueryCache            `yaml:"cache,omitempty"  json:"cache,omitempty"`
}

Query is a named, reusable, cacheable data binding referenced by graph nodes.

type QueryCache

type QueryCache struct {
	StaleTime string `yaml:"staleTime,omitempty" json:"staleTime,omitempty"`
}

QueryCache declares per-query staleness for the client.

type Registry

type Registry interface {
	Register(m *ContractManifest) error
	Contributor(name string) (*ContractManifest, bool)
	Intent(contributor, intent string, version int) (Intent, bool)
	HighestVersion(contributor, intent string) (int, bool)
	All() []*ContractManifest
	MergedGraph(contributor, route string) (*GraphNode, bool)
	// MatchRoute is MergedGraph plus :name-style placeholder matching. On a
	// match the returned map carries the extracted segment values keyed by
	// placeholder name. Exact route matches return an empty (non-nil) map.
	// Slice (j) added this for deep-link detail routes; MergedGraph remains
	// for callers that don't care about params.
	MatchRoute(contributor, route string) (*GraphNode, map[string]string, bool)

	// RegisterRemote records a contributor whose handlers live in another
	// service. The manifest is registered identically to a local one so the
	// graph endpoint and capabilities listing work uniformly; the endpoint
	// is what the dispatcher's forwarding layer reads to know where to send
	// envelopes. Slice (m) added this.
	RegisterRemote(m *ContractManifest, endpoint RemoteEndpoint) error

	// IsRemote reports whether the named contributor was registered via
	// RegisterRemote.
	IsRemote(contributor string) bool

	// Remote returns the upstream endpoint for a contributor previously
	// registered via RegisterRemote. ok is false for local contributors.
	Remote(contributor string) (RemoteEndpoint, bool)

	// Unregister removes a contributor and all its intents + merged graph.
	// Used by discovery loops to clean up offline remotes; safe to call
	// for unknown names.
	Unregister(contributor string)
}

Registry holds all registered contributor manifests and provides lookup by (contributor, intent, version) plus highest-active-version queries for negotiation. It also stores per-contributor merged graphs reflecting any cross-contributor slot extensions applied at registration time.

func NewRegistry

func NewRegistry() Registry

NewRegistry returns an empty registry.

type RemoteEndpoint

type RemoteEndpoint struct {
	// BaseURL is the upstream service's root, including any path prefix
	// (e.g. https://svc.internal:8443 or /proxied/svc). The forwarding
	// client appends "/_forge/contract/dispatch" for envelope POSTs and
	// "/_forge/contract/manifest" for manifest fetches.
	BaseURL string

	// APIKey, when non-empty, is sent as Authorization: Bearer <key> on
	// every forwarded envelope so the upstream can authenticate the
	// dashboard. Inbound user headers (Authorization, Cookie) are still
	// forwarded so the upstream sees the end-user identity too — the
	// API key authenticates the dashboard itself; user identity flows in
	// parallel.
	APIKey string

	// Client overrides the http.Client used to talk to this remote.
	// nil = a default client with a 10s timeout.
	Client *http.Client
}

RemoteEndpoint describes how to reach a contract contributor that lives in another service. Slice (m) introduced this so the dispatcher's forwarding layer knows where to send envelopes for a contributor whose handlers are out-of-process.

type Request

type Request struct {
	Envelope       string          `json:"envelope"`
	Kind           Kind            `json:"kind"`
	Contributor    string          `json:"contributor"`
	Intent         string          `json:"intent"`
	IntentVersion  int             `json:"intentVersion,omitempty"`
	Payload        json.RawMessage `json:"payload,omitempty"`
	Params         map[string]any  `json:"params,omitempty"`
	Context        RequestContext  `json:"context"`
	CSRF           string          `json:"csrf,omitempty"`
	IdempotencyKey string          `json:"idempotencyKey,omitempty"`
}

Request is the wire envelope for POST /api/dashboard/{envelope}.

type RequestContext

type RequestContext struct {
	Route         string `json:"route,omitempty"`
	CorrelationID string `json:"correlationID,omitempty"`
}

RequestContext carries route + correlation metadata. Always populated by the shell.

type Response

type Response struct {
	OK       bool            `json:"ok"`
	Envelope string          `json:"envelope"`
	Kind     Kind            `json:"kind"`
	Data     json.RawMessage `json:"data,omitempty"`
	Meta     ResponseMeta    `json:"meta"`
}

Response is the wire envelope for successful POST responses.

type ResponseMeta

type ResponseMeta struct {
	IntentVersion int               `json:"intentVersion,omitempty"`
	Deprecation   *Deprecation      `json:"deprecation,omitempty"`
	CacheControl  *CacheHint        `json:"cacheControl,omitempty"`
	Invalidates   []string          `json:"invalidates,omitempty"`
	RouteParams   map[string]string `json:"routeParams,omitempty"`
}

ResponseMeta carries cross-cutting metadata (versioning, caching, invalidation).

RouteParams is populated by graph responses for routes that contain :name placeholders (e.g. /traces/:id). The map is keyed by placeholder name and holds the matched URL value. Slice (j) introduced this so the shell can resolve `route.<name>` in payload bindings.

type SlotDef

type SlotDef struct {
	Accepts     []string // intent names accepted in this slot
	Cardinality Cardinality
	Extensible  bool // if true, other contributors may extend via Extends
}

SlotDef describes one slot of a parent intent kind.

type StreamEvent

type StreamEvent struct {
	Intent  string           `json:"intent"`
	Mode    SubscriptionMode `json:"mode"`
	Payload json.RawMessage  `json:"payload"`
	Seq     uint64           `json:"seq"`
}

StreamEvent is the SSE payload for a single subscription event.

type SubscriptionMode

type SubscriptionMode string

SubscriptionMode is how the client integrates events into local state.

const (
	ModeReplace       SubscriptionMode = "replace"
	ModeAppend        SubscriptionMode = "append"
	ModeSnapshotDelta SubscriptionMode = "snapshot+delta"
)

type Warden

type Warden interface {
	Authorize(ctx context.Context, p Principal, a Action) (Decision, error)
}

Warden is the pluggable, data-aware authorization second pass. It runs after the YAML boolean Predicate succeeds and may inspect intent params (e.g. tenant ownership), claims, or external policy.

type WardenRegistry

type WardenRegistry interface {
	Register(name string, w Warden) error
	Get(name string) (Warden, bool)
}

WardenRegistry maps a Warden's declared name to its implementation. Manifest validation rejects YAML that references a name not in the registry.

func NewWardenRegistry

func NewWardenRegistry() WardenRegistry

NewWardenRegistry returns an empty in-memory registry.

Directories

Path Synopsis
Package components provides typed, fluent builders for authoring dashboard contract graphs from Go.
Package components provides typed, fluent builders for authoring dashboard contract graphs from Go.
Package dispatcher implements transport.Dispatcher and transport.SubscriptionSource against a function-table of registered handlers.
Package dispatcher implements transport.Dispatcher and transport.SubscriptionSource against a function-table of registered handlers.
Package idempotency provides command deduplication for the dashboard contract: a Store interface plus an in-memory implementation.
Package idempotency provides command deduplication for the dashboard contract: a Store interface plus an in-memory implementation.
validate.go
validate.go
Package pilot ships the migrated dashboard contributor used to validate the contract end-to-end: extensions.list, services.list, services.detail, and the metrics.summary subscription, all wired against the existing collector and contributor registry.
Package pilot ships the migrated dashboard contributor used to validate the contract end-to-end: extensions.list, services.list, services.detail, and the metrics.summary subscription, all wired against the existing collector and contributor registry.
Package remote implements the contract dispatcher's HTTP forwarding layer.
Package remote implements the contract dispatcher's HTTP forwarding layer.
Package server exposes the two HTTP endpoints a non-dashboard service needs to advertise itself as a contract contributor that other dashboards can discover + dispatch into.
Package server exposes the two HTTP endpoints a non-dashboard service needs to advertise itself as a contract contributor that other dashboards can discover + dispatch into.
capabilities.go
capabilities.go

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL