Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions clients/go/ahp/reducers.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,18 +367,38 @@ func containerChildren(c *ahptypes.Customization) *[]ahptypes.ChildCustomization
return nil
}

func setContainerEnabled(c *ahptypes.Customization, enabled bool) {
func effectiveEnablement(enablement []ahptypes.CustomizationEnablement) bool {
if len(enablement) == 0 {
return true
}
switch decision := enablement[0].Value.(type) {
case *ahptypes.CustomizationEnablementGlobal:
return decision.Enabled
case *ahptypes.CustomizationEnablementWorkspace:
return decision.Enabled
case *ahptypes.CustomizationEnablementSession:
return decision.Enabled
default:
return true
}
}

func applyContainerEnablement(c *ahptypes.Customization, enablement []ahptypes.CustomizationEnablement) {
enabled := effectiveEnablement(enablement)
provenance := append([]ahptypes.CustomizationEnablement(nil), enablement...)
switch v := c.Value.(type) {
case *ahptypes.PluginCustomization:
v.Enabled = enabled
v.Enablement = provenance
case *ahptypes.DirectoryCustomization:
v.Enabled = enabled
case *ahptypes.McpServerCustomization:
v.Enabled = enabled
v.Enablement = provenance
}
}

func setChildEnabled(c *ahptypes.ChildCustomization, enabled bool) {
func applyChildEnablement(c *ahptypes.ChildCustomization, enablement []ahptypes.CustomizationEnablement) {
enabled := effectiveEnablement(enablement)
provenance := append([]ahptypes.CustomizationEnablement(nil), enablement...)
switch v := c.Value.(type) {
case *ahptypes.AgentCustomization:
v.Enabled = &enabled
Expand All @@ -391,15 +411,15 @@ func setChildEnabled(c *ahptypes.ChildCustomization, enabled bool) {
case *ahptypes.HookCustomization:
v.Enabled = &enabled
case *ahptypes.McpServerCustomization:
v.Enabled = enabled
v.Enablement = provenance
}
}

func applyToggle(list []ahptypes.Customization, id string, enabled bool) bool {
func applyToggle(list []ahptypes.Customization, id string, enablement []ahptypes.CustomizationEnablement) bool {
for i := range list {
got, ok := customizationID(list[i])
if ok && got == id {
setContainerEnabled(&list[i], enabled)
applyContainerEnablement(&list[i], enablement)
return true
}
}
Expand All @@ -411,7 +431,7 @@ func applyToggle(list []ahptypes.Customization, id string, enabled bool) bool {
for j := range *children {
got, ok := childCustomizationID((*children)[j])
if ok && got == id {
setChildEnabled(&(*children)[j], enabled)
applyChildEnablement(&(*children)[j], enablement)
return true
}
}
Expand Down Expand Up @@ -938,7 +958,7 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct
if state.Customizations == nil {
return ReduceOutcomeNoOp
}
if applyToggle(state.Customizations, a.Id, a.Enabled) {
if applyToggle(state.Customizations, a.Id, a.Enablement) {
return ReduceOutcomeApplied
}
return ReduceOutcomeNoOp
Expand Down
22 changes: 13 additions & 9 deletions clients/go/ahptypes/actions.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -982,22 +982,26 @@ type SessionCustomizationsChangedAction struct {
Customizations []Customization `json:"customizations"`
}

// A client toggled a customization on or off.
// A client updated a customization's enablement decisions.
//
// Matches `id` against every top-level customization first — a plugin or
// directory container, or a bare top-level MCP server — then against the
// children inside each container (a skill, agent, or other entry), and
// sets the matched entry's `enabled` flag. Disabling a container still
// disables all of its children — the effective state of a child is
// `container.enabled && (child.enabled ?? true)` — so toggling a child
// only matters while its container is enabled. Is a no-op when no
// children inside each container (a skill, agent, or other entry). Plugins
// and MCP servers retain the matched entry's explicit decisions; other
// entries update their `enabled` flag. Disabling a plugin still disables all
// of its children — the effective state of a plugin child is the plugin's
// derived enabled value and `(child.enabled ?? true)` — so toggling a child
// only matters while its plugin is enabled. Is a no-op when no
// customization has the given `id`.
//
// The `enablement` array completely replaces all explicit decisions. A caller
// changing one scope must include every decision it intends to preserve.
type SessionCustomizationToggledAction struct {
Type ActionType `json:"type"`
// The id of the container or child to toggle.
// The id of the container or child to update.
Id string `json:"id"`
// Whether to enable or disable the targeted customization.
Enabled bool `json:"enabled"`
// Explicit enablement decisions, replacing the previous list entirely.
Enablement []CustomizationEnablement `json:"enablement"`
}

// Upserts a top-level customization (plugin or directory).
Expand Down
154 changes: 131 additions & 23 deletions clients/go/ahptypes/state.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,15 @@ const (
CustomizationTypeMcpServer CustomizationType = "mcpServer"
)

// Scope at which customization enablement is decided.
type CustomizationEnablementKind string

const (
CustomizationEnablementKindGlobal CustomizationEnablementKind = "global"
CustomizationEnablementKindWorkspace CustomizationEnablementKind = "workspace"
CustomizationEnablementKindSession CustomizationEnablementKind = "session"
)

// Discriminant values for {@link CustomizationLoadState}.
type CustomizationLoadStatus string

Expand Down Expand Up @@ -2397,8 +2406,6 @@ type PluginCustomization struct {
// protocol; producers and consumers agree on its contents
// out-of-band.
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
// Whether this container is currently enabled.
Enabled bool `json:"enabled"`
// `clientId` of the client that contributed this container. Absent for
// server-originated entries.
ClientId *string `json:"clientId,omitempty"`
Expand All @@ -2412,6 +2419,8 @@ type PluginCustomization struct {
// nothing.
Children []ChildCustomization `json:"children,omitempty"`
Type CustomizationType `json:"type"`
// Explicit enablement decisions. See {@link McpServerCustomization.enablement}.
Enablement []CustomizationEnablement `json:"enablement,omitempty"`
// Version of the plugin, sourced from the
// [Open Plugins](https://open-plugins.com/) manifest's optional
// `version` field (semver, e.g. `"1.2.0"`). Absent when the manifest
Expand Down Expand Up @@ -2458,8 +2467,6 @@ type ClientPluginCustomization struct {
// protocol; producers and consumers agree on its contents
// out-of-band.
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
// Whether this container is currently enabled.
Enabled bool `json:"enabled"`
// `clientId` of the client that contributed this container. Absent for
// server-originated entries.
ClientId *string `json:"clientId,omitempty"`
Expand All @@ -2473,6 +2480,8 @@ type ClientPluginCustomization struct {
// nothing.
Children []ChildCustomization `json:"children,omitempty"`
Type CustomizationType `json:"type"`
// Explicit enablement decisions. See {@link McpServerCustomization.enablement}.
Enablement []CustomizationEnablement `json:"enablement,omitempty"`
// Version of the plugin, sourced from the
// [Open Plugins](https://open-plugins.com/) manifest's optional
// `version` field (semver, e.g. `"1.2.0"`). Absent when the manifest
Expand All @@ -2482,6 +2491,15 @@ type ClientPluginCustomization struct {
Version *string `json:"version,omitempty"`
// Opaque version token used by the host to detect changes.
Nonce *string `json:"nonce,omitempty"`
// Explicit enablement decisions for children this plugin contributes,
// keyed by child name (for MCP servers, the server name as it appears in
// the bundled `.mcp.json`).
//
// Bundled children are discovered by the host rather than published by the
// client, so the client cannot attach `enablement` to them directly. This
// carries the client's global decision for each one; the host applies it
// under the child's durable key.
ChildEnablement map[string][]CustomizationEnablement `json:"childEnablement,omitempty"`
}

// A directory the host watches for this session.
Expand Down Expand Up @@ -2521,8 +2539,6 @@ type DirectoryCustomization struct {
// protocol; producers and consumers agree on its contents
// out-of-band.
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
// Whether this container is currently enabled.
Enabled bool `json:"enabled"`
// `clientId` of the client that contributed this container. Absent for
// server-originated entries.
ClientId *string `json:"clientId,omitempty"`
Expand All @@ -2536,6 +2552,8 @@ type DirectoryCustomization struct {
// nothing.
Children []ChildCustomization `json:"children,omitempty"`
Type CustomizationType `json:"type"`
// Whether this container is currently enabled.
Enabled bool `json:"enabled"`
// Which child customization type this directory holds.
Contents CustomizationType `json:"contents"`
// Whether clients may write into this directory.
Expand Down Expand Up @@ -2580,9 +2598,10 @@ type AgentCustomization struct {
// turned off on its own.
//
// This flag is independent of the parent container's: the **effective**
// enabled state of a child is
// `container.enabled && (child.enabled ?? true)`, so a disabled container
// disables every child regardless of each child's own flag.
// enabled state of a plugin child is the plugin's derived enabled value and
// `(child.enabled ?? true)`, so a disabled plugin disables every child
// regardless of each child's own flag. A directory child instead uses the
// directory's `enabled` value and its own flag.
//
// A child is turned on or off by id with
// {@link SessionCustomizationToggledAction | `session/customizationToggled`}.
Expand Down Expand Up @@ -2652,9 +2671,10 @@ type SkillCustomization struct {
// turned off on its own.
//
// This flag is independent of the parent container's: the **effective**
// enabled state of a child is
// `container.enabled && (child.enabled ?? true)`, so a disabled container
// disables every child regardless of each child's own flag.
// enabled state of a plugin child is the plugin's derived enabled value and
// `(child.enabled ?? true)`, so a disabled plugin disables every child
// regardless of each child's own flag. A directory child instead uses the
// directory's `enabled` value and its own flag.
//
// A child is turned on or off by id with
// {@link SessionCustomizationToggledAction | `session/customizationToggled`}.
Expand Down Expand Up @@ -2707,9 +2727,10 @@ type PromptCustomization struct {
// turned off on its own.
//
// This flag is independent of the parent container's: the **effective**
// enabled state of a child is
// `container.enabled && (child.enabled ?? true)`, so a disabled container
// disables every child regardless of each child's own flag.
// enabled state of a plugin child is the plugin's derived enabled value and
// `(child.enabled ?? true)`, so a disabled plugin disables every child
// regardless of each child's own flag. A directory child instead uses the
// directory's `enabled` value and its own flag.
//
// A child is turned on or off by id with
// {@link SessionCustomizationToggledAction | `session/customizationToggled`}.
Expand Down Expand Up @@ -2761,9 +2782,10 @@ type RuleCustomization struct {
// turned off on its own.
//
// This flag is independent of the parent container's: the **effective**
// enabled state of a child is
// `container.enabled && (child.enabled ?? true)`, so a disabled container
// disables every child regardless of each child's own flag.
// enabled state of a plugin child is the plugin's derived enabled value and
// `(child.enabled ?? true)`, so a disabled plugin disables every child
// regardless of each child's own flag. A directory child instead uses the
// directory's `enabled` value and its own flag.
//
// A child is turned on or off by id with
// {@link SessionCustomizationToggledAction | `session/customizationToggled`}.
Expand Down Expand Up @@ -2814,9 +2836,10 @@ type HookCustomization struct {
// turned off on its own.
//
// This flag is independent of the parent container's: the **effective**
// enabled state of a child is
// `container.enabled && (child.enabled ?? true)`, so a disabled container
// disables every child regardless of each child's own flag.
// enabled state of a plugin child is the plugin's derived enabled value and
// `(child.enabled ?? true)`, so a disabled plugin disables every child
// regardless of each child's own flag. A directory child instead uses the
// directory's `enabled` value and its own flag.
//
// A child is turned on or off by id with
// {@link SessionCustomizationToggledAction | `session/customizationToggled`}.
Expand Down Expand Up @@ -2861,8 +2884,25 @@ type McpServerCustomization struct {
// out-of-band.
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
Type CustomizationType `json:"type"`
// Whether this MCP server is currently enabled.
Enabled bool `json:"enabled"`
// Explicit enablement decisions for this customization, one entry per scope
// that has one. This is a wire contract: producers MUST publish entries
// sorted by descending specificity (Session, Workspace, then Global).
// The agent host emits at most one Workspace entry, for the session's primary
// working directory. Consumers MAY treat
// `enablement[0]` as the decisive decision and
// `enablement?.[0]?.enabled ?? true` as the effective enabled value. An
// absent or empty array means no explicit decision exists, so the
// customization is enabled by default.
//
// Flows in both directions. A client publishes this alongside a customization
// to assert its global decision, which is authoritative for the Global scope;
// a client always includes its global entry, even when enabled. The host
// publishes the fully resolved set across all scopes, and consumers derive
// the effective enabled value from that set.
Enablement []CustomizationEnablement `json:"enablement,omitempty"`
// Whether the client explicitly bundled this server and owns its Global
// enablement decision.
IsClientBundled *bool `json:"isClientBundled,omitempty"`
// Current lifecycle state of the MCP server.
State McpServerState `json:"state"`
// An `mcp://`-protocol channel the client uses to side-channel traffic
Expand Down Expand Up @@ -3557,6 +3597,74 @@ type ResourceChange struct {
Type ResourceChangeType `json:"type"`
}

// ─── Customization Enablement Union ───────────────────────────────────────

// CustomizationEnablement is a single explicit customization enablement decision.
type CustomizationEnablement struct {
Value isCustomizationEnablement
}

type isCustomizationEnablement interface{ isCustomizationEnablement() }

type CustomizationEnablementGlobal struct {
Kind string `json:"kind"`
Enabled bool `json:"enabled"`
}

func (*CustomizationEnablementGlobal) isCustomizationEnablement() {}

type CustomizationEnablementWorkspace struct {
Kind string `json:"kind"`
URI URI `json:"uri"`
Enabled bool `json:"enabled"`
}

func (*CustomizationEnablementWorkspace) isCustomizationEnablement() {}

type CustomizationEnablementSession struct {
Kind string `json:"kind"`
Enabled bool `json:"enabled"`
}

func (*CustomizationEnablementSession) isCustomizationEnablement() {}

func (e *CustomizationEnablement) UnmarshalJSON(data []byte) error {
disc, _, err := readDiscriminator(data, "kind")
if err != nil {
return err
}
switch disc {
case "global":
var value CustomizationEnablementGlobal
if err := json.Unmarshal(data, &value); err != nil {
return err
}
e.Value = &value
case "workspace":
var value CustomizationEnablementWorkspace
if err := json.Unmarshal(data, &value); err != nil {
return err
}
e.Value = &value
case "session":
var value CustomizationEnablementSession
if err := json.Unmarshal(data, &value); err != nil {
return err
}
e.Value = &value
default:
return &json.UnmarshalTypeError{Value: "CustomizationEnablement"}
}
return nil
}

func (e CustomizationEnablement) MarshalJSON() ([]byte, error) {
if e.Value == nil {
return []byte("null"), nil
}
return json.Marshal(e.Value)
}

// ToolInput is raw tool input represented inline or by content reference.
type ToolInput struct {
Inline *string
Expand Down
Loading
Loading