contract

package
v1.11.1 Latest Latest
Warning

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

Go to latest
Published: Sep 15, 2026 License: Apache-2.0 Imports: 10 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, and the per-contributor version negotiation protocol.

DESIGN.md in this directory is the original slice (a) spec, kept for history. Read it as a record of how this package came to be, not as current design: it describes the server-driven UI graph and slot composition that W3 removed.

envelope.go

manifest.go

registry.go

Index

Constants

This section is empty.

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.

Functions

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 (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 names a non-root app for URL namespacing. When set, apps.list projects Home to /@<slug><home> on the wire (see projectAppHome). Defaults to Contributor.Name when unset. Has no effect on a root app — root URLs are always bare regardless of slug.

That projection has no consumer. apps.list is not called from any TypeScript in forge-dashboard, and definePlugin's own routes are declared bare (the spec's example declares /authsome/users, not /@authsome/users). So do NOT assume your plugin's React routes must live under /@<slug>/*. A later wave has to settle it one way or the other: either definePlugin adopts the /@<slug> prefix and this projection becomes real, or the projection is dropped.

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.

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

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"`
}

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 without appearing 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 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 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 (
	IntentKindQuery        IntentKind = "query"
	IntentKindCommand      IntentKind = "command"
	IntentKindSubscription IntentKind = "subscription"
)

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 (
	KindQuery     Kind = "query"
	KindCommand   Kind = "command"
	KindSubscribe Kind = "subscribe"
)

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 binding of an intent plus its parameters. It is parsed from the manifest and validated (loader.Validate checks the intent it names is declared by the same contributor), and it is carried in the manifest wire shape — but nothing consumes it at runtime today. Plugins never see the Go manifest; they call intents through their scoped client. Cache is likewise parsed and read by nobody. Retained deliberately so the manifest schema stays stable; do not build on it until something actually reads it.

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

	// RegisterRemote records a contributor whose handlers live in another
	// service. The manifest is registered identically to a local one so the
	// capabilities listing works 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.
	// 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.

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"`
}

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

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 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