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 ¶
- Variables
- func UnmarshalManifestForTest(b []byte, m *ContractManifest) error
- type Action
- type AppInfo
- type AuditEmitter
- type AuditFilter
- type AuditRecord
- type AuditStore
- type CacheHint
- type Capability
- type ContractManifest
- type Contributor
- type Decision
- type Deprecation
- type EnvelopeSupport
- type Error
- type ErrorCode
- type ErrorResponse
- type Intent
- type IntentKind
- type IntentSchema
- type Kind
- type NoopAuditEmitter
- type ParamSource
- type Predicate
- type Principal
- type Query
- type QueryCache
- type Registry
- type RemoteEndpoint
- type Request
- type RequestContext
- type Response
- type ResponseMeta
- type StreamEvent
- type SubscriptionMode
- type Warden
- type WardenRegistry
Constants ¶
This section is empty.
Variables ¶
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} 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
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 ¶
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.
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" 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.
type NoopAuditEmitter ¶
type NoopAuditEmitter struct{}
NoopAuditEmitter is the disabled-audit implementation.
func (NoopAuditEmitter) Emit ¶
func (NoopAuditEmitter) Emit(_ context.Context, _ AuditRecord)
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.
type Principal ¶
Principal is the caller identity passed to Wardens and the predicate engine.
func PrincipalFor ¶
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.
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 ¶
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.
Source Files
¶
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 |