Skip to content
Draft
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
41 changes: 35 additions & 6 deletions clients/go/ahp/reducers.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,39 +367,68 @@ 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
v.Enablement = provenance
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
v.Enablement = provenance
case *ahptypes.SkillCustomization:
v.Enabled = &enabled
v.Enablement = provenance
case *ahptypes.PromptCustomization:
v.Enabled = &enabled
v.Enablement = provenance
case *ahptypes.RuleCustomization:
v.Enabled = &enabled
v.Enablement = provenance
case *ahptypes.HookCustomization:
v.Enabled = &enabled
v.Enablement = provenance
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 +440,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 +967,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
9 changes: 6 additions & 3 deletions clients/go/ahptypes/actions.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -992,12 +992,15 @@ type SessionCustomizationsChangedAction struct {
// `container.enabled && (child.enabled ?? true)` — so toggling a child
// only matters while its container 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"`
// The complete set of explicit decisions, replacing any existing set.
Enablement []CustomizationEnablement `json:"enablement"`
}

// Upserts a top-level customization (plugin or directory).
Expand Down
188 changes: 187 additions & 1 deletion 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 @@ -2384,6 +2393,18 @@ type PluginCustomization struct {
Uri URI `json:"uri"`
// Human-readable name.
Name string `json:"name"`
// 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.
//
// Only the agent host publishes this; clients treat it as read-only provenance.
Enablement []CustomizationEnablement `json:"enablement,omitempty"`
// Icons for UI display.
Icons []Icon `json:"icons,omitempty"`
// Optional span within {@link CustomizationBase.uri | `uri`} when this
Expand Down Expand Up @@ -2445,6 +2466,18 @@ type ClientPluginCustomization struct {
Uri URI `json:"uri"`
// Human-readable name.
Name string `json:"name"`
// 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.
//
// Only the agent host publishes this; clients treat it as read-only provenance.
Enablement []CustomizationEnablement `json:"enablement,omitempty"`
// Icons for UI display.
Icons []Icon `json:"icons,omitempty"`
// Optional span within {@link CustomizationBase.uri | `uri`} when this
Expand Down Expand Up @@ -2508,6 +2541,18 @@ type DirectoryCustomization struct {
Uri URI `json:"uri"`
// Human-readable name.
Name string `json:"name"`
// 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.
//
// Only the agent host publishes this; clients treat it as read-only provenance.
Enablement []CustomizationEnablement `json:"enablement,omitempty"`
// Icons for UI display.
Icons []Icon `json:"icons,omitempty"`
// Optional span within {@link CustomizationBase.uri | `uri`} when this
Expand Down Expand Up @@ -2562,6 +2607,18 @@ type AgentCustomization struct {
Uri URI `json:"uri"`
// Human-readable name.
Name string `json:"name"`
// 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.
//
// Only the agent host publishes this; clients treat it as read-only provenance.
Enablement []CustomizationEnablement `json:"enablement,omitempty"`
// Icons for UI display.
Icons []Icon `json:"icons,omitempty"`
// Optional span within {@link CustomizationBase.uri | `uri`} when this
Expand Down Expand Up @@ -2634,6 +2691,18 @@ type SkillCustomization struct {
Uri URI `json:"uri"`
// Human-readable name.
Name string `json:"name"`
// 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.
//
// Only the agent host publishes this; clients treat it as read-only provenance.
Enablement []CustomizationEnablement `json:"enablement,omitempty"`
// Icons for UI display.
Icons []Icon `json:"icons,omitempty"`
// Optional span within {@link CustomizationBase.uri | `uri`} when this
Expand Down Expand Up @@ -2689,6 +2758,18 @@ type PromptCustomization struct {
Uri URI `json:"uri"`
// Human-readable name.
Name string `json:"name"`
// 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.
//
// Only the agent host publishes this; clients treat it as read-only provenance.
Enablement []CustomizationEnablement `json:"enablement,omitempty"`
// Icons for UI display.
Icons []Icon `json:"icons,omitempty"`
// Optional span within {@link CustomizationBase.uri | `uri`} when this
Expand Down Expand Up @@ -2743,6 +2824,18 @@ type RuleCustomization struct {
Uri URI `json:"uri"`
// Human-readable name.
Name string `json:"name"`
// 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.
//
// Only the agent host publishes this; clients treat it as read-only provenance.
Enablement []CustomizationEnablement `json:"enablement,omitempty"`
// Icons for UI display.
Icons []Icon `json:"icons,omitempty"`
// Optional span within {@link CustomizationBase.uri | `uri`} when this
Expand Down Expand Up @@ -2796,6 +2889,18 @@ type HookCustomization struct {
Uri URI `json:"uri"`
// Human-readable name.
Name string `json:"name"`
// 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.
//
// Only the agent host publishes this; clients treat it as read-only provenance.
Enablement []CustomizationEnablement `json:"enablement,omitempty"`
// Icons for UI display.
Icons []Icon `json:"icons,omitempty"`
// Optional span within {@link CustomizationBase.uri | `uri`} when this
Expand Down Expand Up @@ -2847,6 +2952,18 @@ type McpServerCustomization struct {
Uri URI `json:"uri"`
// Human-readable name.
Name string `json:"name"`
// 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.
//
// Only the agent host publishes this; clients treat it as read-only provenance.
Enablement []CustomizationEnablement `json:"enablement,omitempty"`
// Icons for UI display.
Icons []Icon `json:"icons,omitempty"`
// Optional span within {@link CustomizationBase.uri | `uri`} when this
Expand All @@ -2861,7 +2978,8 @@ 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.
// Whether this MCP server is effectively enabled after resolving all scopes.
// {@link CustomizationBase.enablement | `enablement`} records its inputs.
Enabled bool `json:"enabled"`
// Current lifecycle state of the MCP server.
State McpServerState `json:"state"`
Expand Down Expand Up @@ -3557,6 +3675,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