feat(agents): activate reviewed runtime defaults on startup (#3471)

This commit is contained in:
Carlos Florêncio
2026-09-07 14:51:46 +01:00
committed by GitHub
parent 4e9556a119
commit bcdb0ce5e9
21 changed files with 1230 additions and 59 deletions
@@ -2,22 +2,23 @@
Kandev invokes these managed npm-provided ACP runtimes with the exact effective
version. The default values below are the reviewed pins in
`managed_npm_runtime_versions.json` at this commit. An operator selection
replaces the default for that installation until **Use Kandev default** clears
it.
`managed_npm_runtime_versions.json` at this commit. An operator selection takes
precedence for the current default generation. It remains effective until
**Use Kandev default** clears it or a later shipped package/default generation
resets it during startup.
| Agent | Package | Default version | ACP arguments |
| --- | --- | --- | --- |
| Claude | `@agentclientprotocol/claude-agent-acp` | `0.70.0` | none |
| Codex | `@agentclientprotocol/codex-acp` | `1.6.0` | none |
| OpenCode | `opencode-ai` | `1.18.18` | `acp --print-logs --log-level ERROR` |
| Copilot | `@github/copilot` | `1.0.75` | `--acp` |
| Gemini | `@google/gemini-cli` | `0.52.0` | `--acp` |
| Claude | `@agentclientprotocol/claude-agent-acp` | `0.75.1` | none |
| Codex | `@agentclientprotocol/codex-acp` | `1.10.0` | none |
| OpenCode | `opencode-ai` | `1.18.29` | `acp --print-logs --log-level ERROR` |
| Copilot | `@github/copilot` | `1.0.83` | `--acp` |
| Gemini | `@google/gemini-cli` | `0.58.0` | `--acp` |
Normal capability probes, sessions, container commands, and one-shot inference
use `npx --yes --prefer-offline package@effective-version` with the ACP
arguments above. For example, an unmodified Claude installation currently
launches `npx --yes --prefer-offline @agentclientprotocol/claude-agent-acp@0.70.0`.
launches `npx --yes --prefer-offline @agentclientprotocol/claude-agent-acp@0.75.1`.
OpenCode's error-only log flags
are part of its managed command so agentctl can observe terminal provider
diagnostics without reading OpenCode's private log files. The exact top-level
@@ -42,6 +43,12 @@ Already-running sessions continue with their existing process. The normal
launch path remains offline-preferred; the update path is online-preferred so
it can refresh stale npm metadata.
At startup, Kandev records the package and default version as the agent's
default generation. If either value changes, it removes the prior operator
selection before runtime consumers start. An unchanged generation preserves
the selection. The reset affects future probes and launches only; an active
agent process continues to run with its current runtime.
ACP protocol negotiation and advertised capabilities are the compatibility
boundary. Kandev does not maintain an exact package-version allowlist or
silently roll back a runtime whose initialization fails. Package selection and
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
)
@@ -13,7 +14,10 @@ var (
errSettingsMissing = errors.New("managed runtime settings store is unavailable")
)
const selectionKeyPrefix = "managed_runtime.active."
const (
selectionKeyPrefix = "managed_runtime.active."
defaultGenerationKeyPrefix = "managed_runtime.default."
)
// SettingsStore is the small persistence seam required by the install-wide
// managed-runtime selection store.
@@ -43,8 +47,31 @@ type Selection struct {
Version string `json:"version"`
}
// DefaultGeneration identifies the trusted package and exact default version
// shipped for one built-in managed agent.
type DefaultGeneration struct {
AgentID string
Package string
Version string
}
// DefaultGenerationChange describes a generation that was newly applied.
type DefaultGenerationChange struct {
Previous DefaultGeneration
Current DefaultGeneration
}
// DefaultGenerationChangeHandler observes successful generation changes.
type DefaultGenerationChangeHandler func(DefaultGenerationChange)
type appliedDefaultGeneration struct {
Package string `json:"package"`
Version string `json:"version"`
}
type Store struct {
settings SettingsStore
settings SettingsStore
onDefaultGenerationChanged DefaultGenerationChangeHandler
}
// NewStore creates the install-wide active-version store.
@@ -52,6 +79,15 @@ func NewStore(settings SettingsStore) *Store {
return &Store{settings: settings}
}
// SetDefaultGenerationChangeHandler wires the startup observer for applied
// generations. It is called only after the marker has been stored successfully.
func (s *Store) SetDefaultGenerationChangeHandler(handler DefaultGenerationChangeHandler) {
if s == nil {
return
}
s.onDefaultGenerationChanged = handler
}
// Get returns the selection only when it belongs to the package currently
// trusted for the agent. A package replacement therefore starts unselected.
func (s *Store) Get(ctx context.Context, agentID, packageName string) (Selection, bool, error) {
@@ -112,6 +148,95 @@ func (s *Store) Delete(ctx context.Context, agentID, packageName string) error {
return s.settings.Delete(ctx, selectionKey(agentID))
}
// ReconcileDefaults activates the current trusted default generation for each
// managed agent. A changed or missing marker clears the active selection before
// saving the new marker, so an interrupted reconciliation can be safely retried.
func (s *Store) ReconcileDefaults(ctx context.Context, generations []DefaultGeneration) error {
if s == nil || s.settings == nil {
return errSettingsMissing
}
ordered := append([]DefaultGeneration(nil), generations...)
sort.Slice(ordered, func(i, j int) bool {
return ordered[i].AgentID < ordered[j].AgentID
})
for _, generation := range ordered {
if err := validateDefaultGeneration(generation); err != nil {
return fmt.Errorf("validate managed runtime default for %q: %w", generation.AgentID, err)
}
markerKey := defaultGenerationKey(generation.AgentID)
rawMarker, found, err := s.settings.Get(ctx, markerKey)
if err != nil {
return fmt.Errorf("read managed runtime default for %q: %w", generation.AgentID, err)
}
previous, markerValid := decodeDefaultGeneration(rawMarker, generation.AgentID)
if found && markerValid && matchesDefaultGeneration(rawMarker, generation) {
continue
}
activeSelectionKey := selectionKey(generation.AgentID)
_, selectionFound, err := s.settings.Get(ctx, activeSelectionKey)
if err != nil {
return fmt.Errorf("read managed runtime selection for %q: %w", generation.AgentID, err)
}
if selectionFound {
if err := s.settings.Delete(ctx, activeSelectionKey); err != nil {
return fmt.Errorf("delete managed runtime selection for %q: %w", generation.AgentID, err)
}
}
rawGeneration, err := json.Marshal(appliedDefaultGeneration{
Package: generation.Package,
Version: generation.Version,
})
if err != nil {
return fmt.Errorf("encode managed runtime default for %q: %w", generation.AgentID, err)
}
if err := s.settings.Save(ctx, markerKey, rawGeneration); err != nil {
return fmt.Errorf("save managed runtime default for %q: %w", generation.AgentID, err)
}
if s.onDefaultGenerationChanged != nil {
s.onDefaultGenerationChanged(DefaultGenerationChange{
Previous: previous,
Current: generation,
})
}
}
return nil
}
func validateDefaultGeneration(generation DefaultGeneration) error {
if strings.TrimSpace(generation.AgentID) == "" || strings.TrimSpace(generation.Package) == "" {
return fmt.Errorf("agent and package are required")
}
if _, err := ParseStableVersion(generation.Version); err != nil {
return fmt.Errorf("default version: %w", err)
}
return nil
}
func matchesDefaultGeneration(raw []byte, generation DefaultGeneration) bool {
applied, ok := decodeDefaultGeneration(raw, generation.AgentID)
return ok && applied.Package == generation.Package && applied.Version == generation.Version
}
func decodeDefaultGeneration(raw []byte, agentID string) (DefaultGeneration, bool) {
var applied appliedDefaultGeneration
if err := json.Unmarshal(raw, &applied); err != nil {
return DefaultGeneration{}, false
}
return DefaultGeneration{
AgentID: agentID,
Package: applied.Package,
Version: applied.Version,
}, true
}
func selectionKey(agentID string) string {
return selectionKeyPrefix + strings.TrimSpace(agentID)
}
func defaultGenerationKey(agentID string) string {
return defaultGenerationKeyPrefix + strings.TrimSpace(agentID)
}
@@ -7,22 +7,34 @@ import (
)
type memorySettings struct {
values map[string][]byte
err error
values map[string][]byte
err error
getErrors map[string]error
saveErr error
deleteErr error
operations []string
}
func (s *memorySettings) Get(_ context.Context, key string) ([]byte, bool, error) {
s.operations = append(s.operations, "get:"+key)
if s.err != nil {
return nil, false, s.err
}
if err := s.getErrors[key]; err != nil {
return nil, false, err
}
value, ok := s.values[key]
return append([]byte(nil), value...), ok, nil
}
func (s *memorySettings) Save(_ context.Context, key string, value []byte) error {
s.operations = append(s.operations, "save:"+key)
if s.err != nil {
return s.err
}
if s.saveErr != nil {
return s.saveErr
}
if s.values == nil {
s.values = make(map[string][]byte)
}
@@ -31,9 +43,13 @@ func (s *memorySettings) Save(_ context.Context, key string, value []byte) error
}
func (s *memorySettings) Delete(_ context.Context, key string) error {
s.operations = append(s.operations, "delete:"+key)
if s.err != nil {
return s.err
}
if s.deleteErr != nil {
return s.deleteErr
}
delete(s.values, key)
return nil
}
@@ -103,3 +119,207 @@ func TestStoreDeleteRemovesSelection(t *testing.T) {
t.Fatalf("selection after delete = found %v, err %v; want absent", found, err)
}
}
// @covers AC-AGENTS-RUNTIME-UPDATES-002.1, AC-AGENTS-RUNTIME-UPDATES-002.2,
// AC-AGENTS-RUNTIME-UPDATES-002.7
func TestStoreReconcileDefaultsPreservesMatchingGeneration(t *testing.T) {
settings := &memorySettings{values: map[string][]byte{
selectionKey("agent-a"): []byte(`{"package":"pkg-a","version":"0.9.0"}`),
defaultGenerationKey("agent-a"): []byte(`{"package":"pkg-a","version":"1.0.0"}`),
}}
store := NewStore(settings)
err := store.ReconcileDefaults(context.Background(), []DefaultGeneration{{
AgentID: "agent-a",
Package: "pkg-a",
Version: "1.0.0",
}})
if err != nil {
t.Fatalf("ReconcileDefaults: %v", err)
}
if got := settings.operations; len(got) != 1 || got[0] != "get:"+defaultGenerationKey("agent-a") {
t.Fatalf("operations = %#v, want matching marker read only", got)
}
if _, found, err := store.Get(context.Background(), "agent-a", "pkg-a"); err != nil || !found {
t.Fatalf("selection after matching reconciliation = found %v, err %v", found, err)
}
}
// @covers AC-AGENTS-RUNTIME-UPDATES-002.1 and AC-AGENTS-RUNTIME-UPDATES-002.7
func TestStoreReconcileDefaultsResetsChangedAndLegacyGenerations(t *testing.T) {
tests := []struct {
name string
marker []byte
wantSequence []string
}{
{
name: "changed marker",
marker: []byte(`{"package":"pkg-a","version":"0.9.0"}`),
wantSequence: []string{"get:" + defaultGenerationKey("agent-a"), "get:" + selectionKey("agent-a"), "delete:" + selectionKey("agent-a"), "save:" + defaultGenerationKey("agent-a")},
},
{
name: "legacy selection without marker",
wantSequence: []string{"get:" + defaultGenerationKey("agent-a"), "get:" + selectionKey("agent-a"), "delete:" + selectionKey("agent-a"), "save:" + defaultGenerationKey("agent-a")},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
values := map[string][]byte{
selectionKey("agent-a"): []byte(`{"package":"pkg-a","version":"0.9.0"}`),
}
if tt.marker != nil {
values[defaultGenerationKey("agent-a")] = tt.marker
}
settings := &memorySettings{values: values}
store := NewStore(settings)
err := store.ReconcileDefaults(context.Background(), []DefaultGeneration{{
AgentID: "agent-a",
Package: "pkg-a",
Version: "1.0.0",
}})
if err != nil {
t.Fatalf("ReconcileDefaults: %v", err)
}
if len(settings.operations) != len(tt.wantSequence) {
t.Fatalf("operations = %#v, want %#v", settings.operations, tt.wantSequence)
}
for i, want := range tt.wantSequence {
if settings.operations[i] != want {
t.Fatalf("operation %d = %q, want %q; all operations %#v", i, settings.operations[i], want, settings.operations)
}
}
if _, found, err := store.Get(context.Background(), "agent-a", "pkg-a"); err != nil || found {
t.Fatalf("selection after reset = found %v, err %v; want absent", found, err)
}
if got := string(settings.values[defaultGenerationKey("agent-a")]); got != `{"package":"pkg-a","version":"1.0.0"}` {
t.Fatalf("stored marker = %q, want current generation", got)
}
})
}
}
// @covers AC-AGENTS-RUNTIME-UPDATES-002.1
func TestStoreReconcileDefaultsTreatsMalformedMarkerAsStale(t *testing.T) {
settings := &memorySettings{values: map[string][]byte{
selectionKey("agent-a"): []byte(`{"package":"pkg-a","version":"0.9.0"}`),
defaultGenerationKey("agent-a"): []byte("not-json"),
}}
store := NewStore(settings)
if err := store.ReconcileDefaults(context.Background(), []DefaultGeneration{{
AgentID: "agent-a",
Package: "pkg-a",
Version: "1.0.0",
}}); err != nil {
t.Fatalf("ReconcileDefaults: %v", err)
}
if _, found, err := store.Get(context.Background(), "agent-a", "pkg-a"); err != nil || found {
t.Fatalf("selection after malformed marker = found %v, err %v; want absent", found, err)
}
}
// @covers AC-AGENTS-RUNTIME-UPDATES-002.6
func TestStoreReconcileDefaultsStopsBeforeMarkerWriteWhenSelectionDeleteFails(t *testing.T) {
wantErr := errors.New("selection delete failed")
settings := &memorySettings{
values: map[string][]byte{
selectionKey("agent-a"): []byte(`{"package":"pkg-a","version":"0.9.0"}`),
},
deleteErr: wantErr,
}
store := NewStore(settings)
generation := []DefaultGeneration{{AgentID: "agent-a", Package: "pkg-a", Version: "1.0.0"}}
if err := store.ReconcileDefaults(context.Background(), generation); !errors.Is(err, wantErr) {
t.Fatalf("delete error = %v, want %v", err, wantErr)
}
if _, found := settings.values[defaultGenerationKey("agent-a")]; found {
t.Fatal("marker was written after selection deletion failed")
}
settings.deleteErr = nil
if err := store.ReconcileDefaults(context.Background(), generation); err != nil {
t.Fatalf("retry ReconcileDefaults: %v", err)
}
}
// @covers AC-AGENTS-RUNTIME-UPDATES-002.6
func TestStoreReconcileDefaultsPropagatesReadAndMarkerWriteErrors(t *testing.T) {
generation := []DefaultGeneration{{AgentID: "agent-a", Package: "pkg-a", Version: "1.0.0"}}
readErr := errors.New("settings read failed")
settings := &memorySettings{getErrors: map[string]error{
defaultGenerationKey("agent-a"): readErr,
}}
store := NewStore(settings)
if err := store.ReconcileDefaults(context.Background(), generation); !errors.Is(err, readErr) {
t.Fatalf("marker read error = %v, want %v", err, readErr)
}
selectionReadErr := errors.New("selection read failed")
settings = &memorySettings{
values: map[string][]byte{
defaultGenerationKey("agent-a"): []byte(`{"package":"pkg-a","version":"0.9.0"}`),
},
getErrors: map[string]error{
selectionKey("agent-a"): selectionReadErr,
},
}
store = NewStore(settings)
if err := store.ReconcileDefaults(context.Background(), generation); !errors.Is(err, selectionReadErr) {
t.Fatalf("selection read error = %v, want %v", err, selectionReadErr)
}
markerWriteErr := errors.New("marker write failed")
settings = &memorySettings{
values: map[string][]byte{
selectionKey("agent-a"): []byte(`{"package":"pkg-a","version":"0.9.0"}`),
},
saveErr: markerWriteErr,
}
store = NewStore(settings)
if err := store.ReconcileDefaults(context.Background(), generation); !errors.Is(err, markerWriteErr) {
t.Fatalf("marker write error = %v, want %v", err, markerWriteErr)
}
if _, found := settings.values[selectionKey("agent-a")]; found {
t.Fatal("selection remains after marker write failure")
}
settings.saveErr = nil
if err := store.ReconcileDefaults(context.Background(), generation); err != nil {
t.Fatalf("retry after marker write failure: %v", err)
}
if got := string(settings.values[defaultGenerationKey("agent-a")]); got != `{"package":"pkg-a","version":"1.0.0"}` {
t.Fatalf("marker after retry = %q, want current generation", got)
}
}
// @covers AC-AGENTS-RUNTIME-UPDATES-002.1
func TestStoreReconcileDefaultsProcessesAgentsInStableOrder(t *testing.T) {
settings := &memorySettings{}
store := NewStore(settings)
generations := []DefaultGeneration{
{AgentID: "agent-b", Package: "pkg-b", Version: "1.0.0"},
{AgentID: "agent-a", Package: "pkg-a", Version: "1.0.0"},
}
if err := store.ReconcileDefaults(context.Background(), generations); err != nil {
t.Fatalf("ReconcileDefaults: %v", err)
}
if got, want := settings.operations, []string{
"get:" + defaultGenerationKey("agent-a"),
"get:" + selectionKey("agent-a"),
"save:" + defaultGenerationKey("agent-a"),
"get:" + defaultGenerationKey("agent-b"),
"get:" + selectionKey("agent-b"),
"save:" + defaultGenerationKey("agent-b"),
}; len(got) != len(want) {
t.Fatalf("operations = %#v, want %#v", got, want)
} else {
for i := range want {
if got[i] != want[i] {
t.Fatalf("operation %d = %q, want %q; all operations %#v", i, got[i], want[i], got)
}
}
}
}
@@ -110,6 +110,26 @@ func TestBuildFreshAgentCommandUsesExactVersionForStandaloneRestart(t *testing.T
}
}
// @covers AC-AGENTS-RUNTIME-UPDATES-002.4 and AC-AGENTS-RUNTIME-UPDATES-002.5
func TestBuildFreshAgentCommandUsesReviewedDefaultAfterSelectionReset(t *testing.T) {
manager := &Manager{commandBuilder: NewCommandBuilder(), logger: newTestLogger()}
manager.SetManagedRuntimeSelectionStore(managedRuntimeSelectionStore{found: false})
agent := agents.NewOpenCodeACP()
commands, err := manager.buildFreshAgentCommand(
context.Background(),
&AgentExecution{RuntimeName: agentruntime.RuntimeStandalone},
agent,
)
if err != nil {
t.Fatalf("buildFreshAgentCommand: %v", err)
}
want := agent.ManagedNPMRuntime().DefaultVersionOrPinned()
if !strings.Contains(commands.initial, "opencode-ai@"+want) {
t.Fatalf("restart command = %q, want reviewed default %q", commands.initial, want)
}
}
func TestBuildFreshAgentCommandFailsWhenStandaloneSelectionCannotBeRead(t *testing.T) {
manager := &Manager{commandBuilder: NewCommandBuilder(), logger: newTestLogger()}
wantErr := errors.New("selection unavailable")
@@ -0,0 +1,64 @@
package backendapp
import (
"context"
"fmt"
"go.uber.org/zap"
"github.com/kandev/kandev/internal/agent/agents"
"github.com/kandev/kandev/internal/agent/managedruntime"
"github.com/kandev/kandev/internal/agent/registry"
"github.com/kandev/kandev/internal/common/logger"
)
func managedRuntimeDefaultGenerations(agentRegistry *registry.Registry) []managedruntime.DefaultGeneration {
if agentRegistry == nil {
return nil
}
generations := make([]managedruntime.DefaultGeneration, 0)
for _, agent := range agentRegistry.List() {
managed, ok := agent.(agents.ManagedNPMRuntimeAgent)
if !ok {
continue
}
spec := managed.ManagedNPMRuntime()
generations = append(generations, managedruntime.DefaultGeneration{
AgentID: agent.ID(),
Package: spec.Package,
Version: spec.DefaultVersionOrPinned(),
})
}
return generations
}
func reconcileManagedRuntimeDefaults(
ctx context.Context,
store *managedruntime.Store,
agentRegistry *registry.Registry,
log *logger.Logger,
) error {
if store == nil {
return fmt.Errorf("managed runtime selection store is unavailable")
}
store.SetDefaultGenerationChangeHandler(func(change managedruntime.DefaultGenerationChange) {
if log == nil {
return
}
log.Info("managed runtime default generation changed",
zap.String("agent_id", change.Current.AgentID),
zap.String("previous_package", change.Previous.Package),
zap.String("previous_version", change.Previous.Version),
zap.String("package", change.Current.Package),
zap.String("version", change.Current.Version),
)
})
if err := store.ReconcileDefaults(ctx, managedRuntimeDefaultGenerations(agentRegistry)); err != nil {
if log != nil {
log.Error("managed runtime default reconciliation failed", zap.Error(err))
}
return err
}
return nil
}
@@ -0,0 +1,149 @@
package backendapp
import (
"context"
"errors"
"go/ast"
"strings"
"testing"
"github.com/kandev/kandev/internal/agent/agents"
"github.com/kandev/kandev/internal/agent/managedruntime"
"github.com/kandev/kandev/internal/agent/registry"
"github.com/kandev/kandev/internal/common/config"
"github.com/kandev/kandev/internal/events/bus"
)
type managedRuntimeDefaultSettings struct {
getErr error
}
func (s managedRuntimeDefaultSettings) Get(context.Context, string) ([]byte, bool, error) {
return nil, false, s.getErr
}
func (s managedRuntimeDefaultSettings) Save(context.Context, string, []byte) error {
return nil
}
func (s managedRuntimeDefaultSettings) Delete(context.Context, string) error {
return nil
}
// @covers AC-AGENTS-RUNTIME-UPDATES-002.1 and AC-AGENTS-RUNTIME-UPDATES-002.2
func TestManagedRuntimeDefaultGenerationsUseRegisteredManagedAgents(t *testing.T) {
log := newTestLogger()
reg := registry.NewRegistry(log)
for _, agent := range []agents.Agent{
agents.NewOpenCodeACP(),
agents.NewClaudeACP(),
agents.NewDynamicAgent(),
agents.NewMockAgent(),
} {
if err := reg.Register(agent); err != nil {
t.Fatalf("register %s: %v", agent.ID(), err)
}
}
got := managedRuntimeDefaultGenerations(reg)
if len(got) != 2 {
t.Fatalf("managed generations = %#v, want two built-in managed agents", got)
}
if got[0].AgentID != "claude-acp" || got[1].AgentID != "opencode-acp" {
t.Fatalf("managed generation order = %#v, want claude-acp then opencode-acp", got)
}
for _, generation := range got {
if generation.Package == "" || generation.Version == "" {
t.Fatalf("incomplete managed generation = %#v", generation)
}
}
}
// @covers AC-AGENTS-RUNTIME-UPDATES-002.6
func TestReconcileManagedRuntimeDefaultsReturnsErrorBeforeServicesAreReady(t *testing.T) {
wantErr := errors.New("settings read failed")
reg := registry.NewRegistry(newTestLogger())
if err := reg.Register(agents.NewOpenCodeACP()); err != nil {
t.Fatalf("register managed agent: %v", err)
}
store := managedruntime.NewStore(managedRuntimeDefaultSettings{getErr: wantErr})
if err := reconcileManagedRuntimeDefaults(context.Background(), store, reg, newTestLogger()); !errors.Is(err, wantErr) {
t.Fatalf("reconciliation error = %v, want %v", err, wantErr)
}
}
// @covers AC-AGENTS-RUNTIME-UPDATES-002.6
func TestProvideServicesStopsWhenManagedRuntimeReconciliationFails(t *testing.T) {
cfg := &config.Config{
HomeDir: t.TempDir(),
Database: config.DatabaseConfig{Driver: "sqlite"},
}
log := newTestLogger()
pool, repos, cleanups, err := provideRepositories(context.Background(), cfg, log, "test-managed-runtime-defaults")
if err != nil {
t.Fatalf("provideRepositories: %v", err)
}
t.Cleanup(func() {
for i := len(cleanups) - 1; i >= 0; i-- {
if cleanups[i] != nil {
_ = cleanups[i]()
}
}
})
agentRegistry, registryCleanup, err := registry.Provide(log)
if err != nil {
t.Fatalf("registry.Provide: %v", err)
}
t.Cleanup(func() {
if registryCleanup != nil {
_ = registryCleanup()
}
})
if err := pool.Reader().Close(); err != nil {
t.Fatalf("close settings reader: %v", err)
}
_, _, err = provideServices(cfg, log, repos, pool, bus.NewMemoryEventBus(log), agentRegistry, "test-managed-runtime-defaults")
if err == nil || !strings.Contains(err.Error(), "reconcile managed runtime defaults") {
t.Fatalf("provideServices error = %v, want reconciliation failure before readiness", err)
}
}
func TestProvideServicesReconcilesManagedRuntimeDefaultsBeforeDiscovery(t *testing.T) {
provideFn := findFuncDecl(t, "services.go", "provideServices")
callOrder := []string{}
ast.Inspect(provideFn, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
switch fn := call.Fun.(type) {
case *ast.Ident:
callOrder = append(callOrder, fn.Name)
case *ast.SelectorExpr:
callOrder = append(callOrder, fn.Sel.Name)
}
return true
})
find := func(name string) int {
for i, call := range callOrder {
if call == name {
return i
}
}
return -1
}
reconcileIndex := find("reconcileManagedRuntimeDefaults")
discoveryIndex := find("LoadRegistry")
if reconcileIndex < 0 {
t.Fatal("provideServices does not reconcile managed runtime defaults")
}
if discoveryIndex < 0 {
t.Fatal("provideServices does not load the discovery registry")
}
if reconcileIndex > discoveryIndex {
t.Fatalf("managed runtime reconciliation call index %d occurs after discovery index %d", reconcileIndex, discoveryIndex)
}
}
+9 -5
View File
@@ -76,6 +76,15 @@ func provideServices(cfg *config.Config, log *logger.Logger, repos *Repositories
// Load custom TUI agents from DB into registry before discovery
loadCustomTUIAgents(context.Background(), repos, agentRegistry, log)
managedRuntimeSettings, err := systemsettings.NewStore(dbPool)
if err != nil {
return nil, nil, fmt.Errorf("initialize managed runtime settings: %w", err)
}
managedRuntimeSelections := managedruntime.NewStore(managedRuntimeSettings)
if err := reconcileManagedRuntimeDefaults(context.Background(), managedRuntimeSelections, agentRegistry, log); err != nil {
return nil, nil, fmt.Errorf("reconcile managed runtime defaults: %w", err)
}
discoveryRegistry, err := discovery.LoadRegistry(context.Background(), agentRegistry, log)
if err != nil {
return nil, nil, err
@@ -84,11 +93,6 @@ func provideServices(cfg *config.Config, log *logger.Logger, repos *Repositories
agentSettingsController := agentsettingscontroller.NewController(repos.AgentSettings, discoveryRegistry, agentRegistry, repos.Task, log)
agentSettingsController.SetDynamicAgentRoutingEnabled(cfg.Features.DynamicAgentRouting)
agentSettingsController.SetSecretStore(userSecretStore)
managedRuntimeSettings, err := systemsettings.NewStore(dbPool)
if err != nil {
return nil, nil, fmt.Errorf("initialize managed runtime settings: %w", err)
}
managedRuntimeSelections := managedruntime.NewStore(managedRuntimeSettings)
agentSettingsController.SetManagedRuntimeSelectionStore(managedRuntimeSelections)
userSvc := userservice.NewService(repos.User, eventBus, log)
@@ -1,6 +1,6 @@
# 0034: Agent Client Protocol Codex ACP Bridge
**Status:** accepted (amended 2026-07-26)
**Status:** accepted (amended 2026-07-26 and 2026-09-07)
**Date:** 2026-07-10
**Area:** backend, protocol
@@ -12,7 +12,7 @@ Kandev's `codex-acp` agent previously launched `@zed-industries/codex-acp`. That
Kandev's `codex-acp` agent launches the exact effective version of
`@agentclientprotocol/codex-acp` for ACP chat and one-shot inference sessions.
The reviewed Kandev default is currently `1.6.0`; an install-wide operator
The reviewed Kandev default is currently `1.10.0`; an install-wide operator
selection can override it. Normal launches prefer npm's execution cache. The
install script still installs `@openai/codex` for `codex login`; that native
authentication helper is separate from the managed ACP runtime.
@@ -23,6 +23,11 @@ replaces the advertised model and configuration catalogue for future
sessions. The shared runtime-management boundary is recorded in
[ADR-2026-07-26](2026-07-26-user-managed-agent-runtime-updates.md) and the
[managed runtime update spec](../specs/agents/requirements/runtime-updates.md).
At startup, a changed Codex package or reviewed default clears the prior
operator selection before new runtime consumers start. An unchanged default
generation preserves the selection, and an active Codex process is not
replaced during backend recovery. The shared upgrade rule is recorded in
[ADR-2026-09-07](2026-09-07-activate-managed-runtime-defaults.md).
## Consequences
@@ -1,11 +1,16 @@
# ADR-2026-08-12-validated-managed-runtime-version-selection: Validate and Persist Managed Runtime Version Selection
**Status:** accepted (amended 2026-08-21 and 2026-08-24)
**Status:** accepted (amended 2026-08-21, 2026-08-24, and by 2026-09-07-activate-managed-runtime-defaults)
**Date:** 2026-08-12
**Area:** backend, frontend, protocol, workflow
**Supersedes:**
[ADR-2026-07-26-user-managed-agent-runtime-updates](2026-07-26-user-managed-agent-runtime-updates.md)
**Amendment:**
[ADR-2026-09-07](2026-09-07-activate-managed-runtime-defaults.md) limits a
selection to one shipped default generation. A changed package or default
activates the new Kandev default before runtime consumers start.
## Context
Kandev originally invoked managed npm ACP runtimes by an unversioned package
@@ -51,7 +56,8 @@ Command resolution uses the persisted operator selection when its package
identity still matches; otherwise it uses the shipped default. Kandev does not
copy the default into the database. An installation with no explicit selection
therefore follows newer defaults when Kandev upgrades, while an operator
selection remains stable until the operator changes or clears it.
selection remains stable until the operator changes or clears it, unless a
changed applied default generation resets it before runtime consumers start.
The resulting effective version applies to every Kandev-built ACP command for
that managed package, including capability probes, utility prompts, local
@@ -0,0 +1,50 @@
# ADR-2026-09-07-activate-managed-runtime-defaults: Activate Shipped Managed Runtime Defaults
**Status:** accepted
**Date:** 2026-09-07
**Area:** backend, frontend, protocol
**Amends:**
[ADR-2026-08-12-validated-managed-runtime-version-selection](2026-08-12-validated-managed-runtime-version-selection.md)
## Context
Kandev ships reviewed default versions for its managed npm agent runtimes. Operators can also select an exact stable version for each runtime.
The prior decision kept an operator selection until manual removal. Therefore, an old selection could hide a newer default after a Kandev upgrade.
The release default represents the runtime that the Kandev release reviewed. The release must use that default before an operator makes another version choice.
## Decision
Each built-in managed agent has a default generation. The generation contains the trusted package and the exact Kandev default version.
Kandev records the applied generation in its install-wide settings. Startup compares this marker with the current embedded catalog before runtime consumers start.
If the generation changed, Kandev deletes the prior operator selection. Then it records the new generation, and the shipped default becomes effective.
If the generation did not change, Kandev keeps the operator selection. An unrelated Kandev release does not reset a selected runtime.
The first release with this rule treats an unmarked legacy selection as stale. This rule makes that release activate all current reviewed defaults once.
After startup, the operator can select any validated stable version. This selection can be older or newer than the default.
Kandev deletes the selection before it writes the generation marker. If either operation fails, startup stops before readiness and retries on the next start.
The reset changes future probes and launches only. Kandev does not replace an agent process that remains active during backend recovery.
## Consequences
A Kandev release activates each changed reviewed agent default. An operator must reselect a custom version after that release starts.
Same-generation restarts preserve the new selection. A later change to the package or default starts another generation and resets the selection again.
The settings store gains one small marker per managed agent. The startup operation is local and does not query npm or start an ACP probe.
The operation is idempotent but not one database transaction. Its delete-before-save order cannot restore an older selection after an interrupted start.
## Alternatives Considered
- Preserve every operator selection across upgrades. Rejected because an older selection can hide the runtime that the new Kandev release reviewed.
- Reset selections after every Kandev version change. Rejected because an unrelated release must not erase a deliberate agent-version choice.
- Reset only when the selected version is older. Rejected because SemVer order does not prove compatibility with the new Kandev release.
- Replace active agent processes during upgrade. Rejected because runtime updates affect future launches and must not interrupt active work.
+2 -1
View File
@@ -85,7 +85,7 @@ Read individual ADRs for full context. Create new ones via `/record decision` or
| 2026-07-24-opt-in-authentication | [Opt-in Authentication and Per-User Workspace Scoping](2026-07-24-opt-in-authentication.md) | accepted | backend, frontend, protocol, security | 2026-07-24 |
| 2026-07-24-semantic-notification-events | [Semantic notification events come from domain occurrences](2026-07-24-semantic-notification-events.md) | accepted | backend, frontend, desktop | 2026-07-24 |
| 2026-07-26-user-managed-agent-runtime-updates | [User-Managed Agent Runtime Updates](2026-07-26-user-managed-agent-runtime-updates.md) | superseded by 2026-08-12-validated-managed-runtime-version-selection | backend, frontend, protocol | 2026-07-26 |
| 2026-08-12-validated-managed-runtime-version-selection | [Validate and Persist Managed Runtime Version Selection](2026-08-12-validated-managed-runtime-version-selection.md) | accepted (amended 2026-08-21) | backend, frontend, protocol, workflow | 2026-08-12 |
| 2026-08-12-validated-managed-runtime-version-selection | [Validate and Persist Managed Runtime Version Selection](2026-08-12-validated-managed-runtime-version-selection.md) | accepted (amended 2026-08-21, 2026-08-24, and by 2026-09-07) | backend, frontend, protocol, workflow | 2026-08-12 |
| 2026-07-26-single-session-model-switching | [Single-Session Model Switching](2026-07-26-single-session-model-switching.md) | accepted (amended 2026-07-27) | workflow, infra | 2026-07-26 |
| 2026-07-27-task-git-credential-policy | [Separate GitHub Automation From Task Git Credential Policy](2026-07-27-task-git-credential-policy.md) | accepted | backend, frontend, security | 2026-07-27 |
| 2026-07-27-spa-failure-containment-and-deployment-recovery | [SPA Failure Containment and Deployment Recovery](2026-07-27-spa-failure-containment-and-deployment-recovery.md) | accepted | frontend | 2026-07-27 |
@@ -252,3 +252,4 @@ Read individual ADRs for full context. Create new ones via `/record decision` or
| 2026-09-05-bounded-progressive-storage-analysis | [Use bounded progressive storage analysis](2026-09-05-bounded-progressive-storage-analysis.md) | accepted | backend, frontend, protocol | 2026-09-05 |
| 2026-09-05-script-capable-html-preview-isolation | [Capability-Free Runtime for Script-Capable HTML Preview](2026-09-05-script-capable-html-preview-isolation.md) | proposed | frontend, desktop, security | 2026-09-05 |
| 2026-09-05-trusted-browser-html-preview | [Treat HTML Preview as Trusted Workspace Code](2026-09-05-trusted-browser-html-preview.md) | accepted | frontend, backend, agentctl, security | 2026-09-05 |
| 2026-09-07-activate-managed-runtime-defaults | [Activate Shipped Managed Runtime Defaults](2026-09-07-activate-managed-runtime-defaults.md) | accepted | backend, frontend, protocol | 2026-09-07 |
@@ -0,0 +1,103 @@
---
created: 2026-09-07
status: implemented
requirements:
- REQ-AGENTS-RUNTIME-UPDATES-001
- REQ-AGENTS-RUNTIME-UPDATES-002
system_design:
- ../../specs/agents/system-design/runtime-default-activation.md
- ../../specs/agents/system-design/runtime-updates-01.md
- ../../specs/agents/system-design/runtime-updates-02.md
legacy_specs: []
---
# Implementation Plan: Managed Runtime Default Activation
## Overview
Kandev will activate a changed reviewed agent default during startup. The implementation first adds an idempotent selection-store reconciliation and then wires it before runtime consumers.
The existing Settings version action remains the downgrade path. Documentation and focused end-to-end evidence complete the change after the backend behavior is stable.
## Scope
### In scope
- Record the applied package and default version for each built-in managed agent.
- Remove an older operator selection when that agent's shipped generation changes.
- Preserve selections across restarts when the shipped generation is unchanged.
- Stop startup before readiness when reconciliation cannot finish.
- Keep active agent processes unchanged during backend recovery.
- Document default activation and the later downgrade path.
### Out of scope
- Replacing active agent processes during a Kandev upgrade.
- Querying npm or ACP-probing a reviewed default during startup.
- Changing native agents, passthrough commands, or authentication helpers.
- Resetting a selection when an unrelated Kandev release keeps the same agent default.
- Adding a new Settings control or API field.
## Technical approach
### Default-generation store
Add `managedruntime.DefaultGeneration` and `Store.ReconcileDefaults`. Each generation contains the built-in agent ID, trusted package, and exact default version.
Store the applied generation at `managed_runtime.default.<agent-id>`. Keep the existing active selection at `managed_runtime.active.<agent-id>`.
When the marker matches, preserve the selection. When the marker is absent or different, delete the selection before writing the current marker.
Use a stable agent order for deterministic behavior and logs. Return each read, delete, or save error to the startup caller.
### Startup integration
Build the generation list from registered agents that implement `agents.ManagedNPMRuntimeAgent`. Do not accept generation values from requests or stored selections.
Run reconciliation in `backendapp.provideServices` before agent discovery and runtime selection wiring. Return an initialization error before the backend reports readiness.
Keep current effective-version consumers unchanged. They will read no active selection after a generation reset and will use the embedded default.
### Documentation
Update the managed-agent how-to guide with the upgrade rule. Explain that the user can select an older stable version after startup.
Update internal bridge documentation and current Codex version claims. Make the displayed defaults match the embedded version catalog.
## Tests
- `AC-AGENTS-RUNTIME-UPDATES-002.1`, `.2`, `.4`, and `.7`: store tests cover changed, unchanged, and missing generation markers.
- `AC-AGENTS-RUNTIME-UPDATES-002.5`: lifecycle coverage proves that future resume commands use the reconciled effective version.
- `AC-AGENTS-RUNTIME-UPDATES-002.6`: startup composition tests cover every reconciliation error and readiness failure.
- `AC-AGENTS-RUNTIME-UPDATES-001.5` and `.6`: existing selection tests remain green with generation-aware precedence.
## E2E tests
The implementation does not change the Settings UI. The existing rollback scenario proves `AC-AGENTS-RUNTIME-UPDATES-002.3` after the backend activates a default.
Run the focused Playwright scenario in `apps/web/e2e/tests/settings/agent-runtime-update.spec.ts`. The test selects an older stable version and approves **Roll back runtime**.
## Work orders
- [x] [Task 01: Reconcile managed runtime generations](task-01-reconcile-default-generations.md)
- [x] [Task 02: Activate defaults during startup](task-02-activate-defaults-at-startup.md)
- [x] [Task 03: Document and prove upgrade behavior](task-03-document-and-prove-upgrade-behavior.md)
## Verification results
- `GOTMPDIR=... go test ./internal/agent/managedruntime -race -count=1` passed.
- The Task 02 focused backend command passed for backendapp, registry,
hostutility, lifecycle, and Settings controller packages.
- `python3 scripts/lint-spec-files.py --all` passed.
- Public documentation tests passed: 61 tests; 46 published pages validated.
- The focused rollback E2E passed: 1 test in
`tests/settings/agent-runtime-update.spec.ts`.
- `git diff --check` passed for the implementation, specifications, plans,
decisions, public docs, and managed-runtime documentation.
## Risks
- The first release with this behavior removes all legacy managed-runtime selections because they have no generation marker.
- A user-selected version can be newer than the new Kandev default. The reset still uses the reviewed Kandev default by design.
- A partial multi-agent reset can occur before startup stops. The safe operation order makes the next startup complete the remaining resets.
- Backend recovery can reconnect to an existing process with its old runtime. Only its next process launch uses the new default.
@@ -0,0 +1,88 @@
---
id: "01-reconcile-default-generations"
title: "Reconcile managed runtime generations"
status: done
wave: 1
depends_on: []
plan: "plan.md"
requirements:
- REQ-AGENTS-RUNTIME-UPDATES-001
- REQ-AGENTS-RUNTIME-UPDATES-002
acceptance_criteria:
- AC-AGENTS-RUNTIME-UPDATES-001.5
- AC-AGENTS-RUNTIME-UPDATES-001.6
- AC-AGENTS-RUNTIME-UPDATES-002.1
- AC-AGENTS-RUNTIME-UPDATES-002.2
- AC-AGENTS-RUNTIME-UPDATES-002.4
- AC-AGENTS-RUNTIME-UPDATES-002.7
system_design:
- ../../specs/agents/system-design/runtime-default-activation.md
---
# Task 01: Reconcile Managed Runtime Generations
## Summary
Add the default-generation model and the idempotent store operation. The operation removes an older selection before it records the current generation.
## In scope
- Add trusted package and default-version generation records.
- Reconcile missing, matching, and changed markers.
- Treat legacy unmarked selections as stale.
- Keep the current active-selection key and JSON shape.
- Return all storage errors.
## Out of scope
- Backend startup wiring.
- Runtime command construction.
- User-interface or API changes.
## Acceptance
- A matching generation preserves the active selection without a write.
- A missing or changed generation deletes the selection before marker storage.
- A failed operation returns an error and remains safe to retry.
## Verification
```bash
go test ./internal/agent/managedruntime -count=1
```
Run the command from `apps/backend`.
## Files likely touched
- `apps/backend/internal/agent/managedruntime/selection.go`
- `apps/backend/internal/agent/managedruntime/selection_test.go`
## Dependencies
None.
## Risks
- The delete and marker write are separate operations. The order must keep retries safe after interruption.
- A malformed marker must not preserve an old selection.
## Parallelism
`sequential`
## Inputs
- `REQ-AGENTS-RUNTIME-UPDATES-001`
- `REQ-AGENTS-RUNTIME-UPDATES-002`
- `docs/specs/agents/system-design/runtime-default-activation.md`
- Existing `managedruntime.Store` tests and `system/settings.Store` behavior.
## Results
Implemented `DefaultGeneration` and idempotent `Store.ReconcileDefaults`. Matching
markers preserve selections; missing, changed, or malformed markers clear the
selection before saving the current marker. Storage errors stop reconciliation
and leave it safe to retry.
Verification: `go test ./internal/agent/managedruntime -count=1` passed.
@@ -0,0 +1,89 @@
---
id: "02-activate-defaults-at-startup"
title: "Activate defaults during startup"
status: done
wave: 2
depends_on:
- "01-reconcile-default-generations"
plan: "plan.md"
requirements:
- REQ-AGENTS-RUNTIME-UPDATES-002
acceptance_criteria:
- AC-AGENTS-RUNTIME-UPDATES-002.1
- AC-AGENTS-RUNTIME-UPDATES-002.2
- AC-AGENTS-RUNTIME-UPDATES-002.4
- AC-AGENTS-RUNTIME-UPDATES-002.5
- AC-AGENTS-RUNTIME-UPDATES-002.6
system_design:
- ../../specs/agents/system-design/runtime-default-activation.md
---
# Task 02: Activate Defaults During Startup
## Summary
Wire default-generation reconciliation into service startup. The backend must finish this operation before discovery, probes, launches, or readiness.
## In scope
- Build trusted generations from registered managed agents.
- Run reconciliation before runtime consumers start.
- Stop startup when reconciliation returns an error.
- Add structured logs for changed generations and errors.
- Prove that later resume commands use the reconciled effective version.
## Out of scope
- npm metadata requests or candidate ACP probes during startup.
- Replacement of active agent processes.
- Frontend changes.
## Acceptance
- Startup removes stale selections before any effective-version consumer starts.
- An unchanged generation preserves a selection through another startup.
- A reconciliation error prevents readiness and leaves the operation retryable.
## Verification
```bash
go test ./internal/backendapp ./internal/agent/registry ./internal/agent/hostutility ./internal/agent/runtime/lifecycle ./internal/agent/settings/controller -run 'ManagedRuntime|RuntimeVersion|AgentUpdate|BuildAgentCommand' -count=1
```
Run the command from `apps/backend`.
## Files likely touched
- `apps/backend/internal/backendapp/services.go`
- `apps/backend/internal/backendapp/managed_runtime_defaults_test.go`
- `apps/backend/internal/agent/runtime/lifecycle/managed_runtime_command_test.go`
- Focused registry, host-utility, or Settings tests affected by startup order.
## Dependencies
Task 01.
## Risks
- Reconciliation must run before a boot probe reads the old selection.
- Mock and custom agents must remain outside the built-in managed generation list.
## Parallelism
`sequential`
## Inputs
- Task 01 store contract.
- `docs/specs/agents/system-design/runtime-default-activation.md`
- Existing `backendapp.provideServices` and effective-version command builders.
## Results
Startup now reconciles managed runtime generations after trusted agent
registration and before discovery. Reconciliation errors are logged and
returned from `provideServices`, preventing readiness; future launches fall
back to the reviewed default when a selection was cleared.
Verification: the work-order command passed, including backendapp, registry,
hostutility, lifecycle, and Settings controller tests.
@@ -0,0 +1,90 @@
---
id: "03-document-and-prove-upgrade-behavior"
title: "Document and prove upgrade behavior"
status: done
wave: 3
depends_on:
- "02-activate-defaults-at-startup"
plan: "plan.md"
requirements:
- REQ-AGENTS-RUNTIME-UPDATES-002
acceptance_criteria:
- AC-AGENTS-RUNTIME-UPDATES-002.3
- AC-AGENTS-RUNTIME-UPDATES-002.4
- AC-AGENTS-RUNTIME-UPDATES-002.5
system_design:
- ../../specs/agents/system-design/runtime-default-activation.md
---
# Task 03: Document and Prove Upgrade Behavior
## Summary
Update the operator documentation for default activation and later version selection. Run the existing Settings rollback flow as end-to-end evidence.
## In scope
- Explain when Kandev replaces an operator selection.
- Explain which processes keep their current runtime.
- Explain how to select an older stable version after startup.
- Synchronize internal default-version documentation with the embedded catalog.
- Run the focused desktop Settings rollback scenario.
## Out of scope
- New UI copy, controls, screenshots, or mobile layout changes.
- A new browser fixture for compile-time default catalogs.
- General agent-runtime documentation rewrites.
## Acceptance
- Public documentation states the default-generation upgrade rule and downgrade path.
- Internal version tables match the embedded managed-runtime catalog.
- The existing browser rollback flow passes against the production build.
## Verification
```bash
node --test scripts/validate-public-docs.test.mjs
node scripts/validate-public-docs.mjs
pnpm e2e:run tests/settings/agent-runtime-update.spec.ts -- --grep "selects an older stable version for rollback"
git diff --check -- docs/public docs/specs docs/decisions docs/plans apps/backend/internal/agent/agents
```
Run the first two commands from the repository root. Run the Playwright command from `apps/web`.
## Files likely touched
- `docs/public/agents-and-profiles.md`
- `apps/backend/internal/agent/agents/ACP_BRIDGE_VERSIONS.md`
- `docs/decisions/0034-agentclientprotocol-codex-acp.md`
## Dependencies
Task 02.
## Risks
- Documentation can imply that every Kandev release resets selections. The rule applies only when that agent's package or default changes.
- The browser test proves the existing downgrade action. Backend tests must prove upgrade-time default activation.
## Parallelism
`sequential`
## Inputs
- Completed backend behavior from Tasks 01 and 02.
- `docs/public/agents-and-profiles.md` as the operator how-to guide.
- Existing desktop rollback scenario in `agent-runtime-update.spec.ts`.
## Results
Updated the public managed-agent guide with the default-generation upgrade
rule, active-process behavior, and post-startup rollback path. Synchronized
the internal ACP bridge version table and Codex decision with the embedded
catalogue, and recorded the startup rule in the durable decision/spec files.
Verification: specification lint passed; public documentation tests passed
(61 tests) and the validator passed (46 published pages); the focused rollback
E2E passed (1 test); and the scoped `git diff --check` passed.
+17 -3
View File
@@ -59,9 +59,23 @@ Each managed runtime has a reviewed Kandev default. If you have not selected a
version, Kandev uses that exact default for probes, sessions, standalone
inference, containers, and SSH commands. A successful version update stores
your exact selection for this Kandev installation. The selection takes
precedence over the default until you choose **Use Kandev default**. Kandev
does not store the default as a user selection, so later Kandev releases can
move unmodified installations to their reviewed defaults.
precedence for the current default generation. **Use Kandev default** clears
it, and a later shipped package or reviewed default resets it during startup.
Kandev does not store the default as a user selection.
When a Kandev upgrade changes the managed package or its reviewed default,
Kandev removes the older selection during startup before the service becomes
ready. New probes and launches then use the reviewed default for that release.
On the first startup with this generation tracking, Kandev treats an existing
selection without a generation marker as legacy and resets it once.
When the package and default stay the same, Kandev preserves your selection
across restarts and unrelated upgrades. A process that is already running is
not replaced, so the new default applies when Kandev starts a future process.
After startup, open the update control to select any validated stable version,
including an older version. This lets you roll back the new default when a
provider or environment requires it. The selection remains active until you
change it or a later Kandev release changes that agent's package or default.
When the cached npm check finds a newer stable release, the update control has
a blue dot and its accessible label includes the effective and latest
+1
View File
@@ -79,6 +79,7 @@ surface shared by task and Office consumers.
- [No Silent Model Fallback System Design Part 2](system-design/no-silent-model-fallback-02.md)
- [Managed Agent Runtime Versions and Updates System Design Part 1](system-design/runtime-updates-01.md)
- [Managed Agent Runtime Versions and Updates System Design Part 2](system-design/runtime-updates-02.md)
- [Managed Runtime Default Activation](system-design/runtime-default-activation.md)
- [Managed npm runtime recovery](system-design/managed-npm-runtime-recovery.md)
- [MCP Bridge Reliability](system-design/mcp-bridge-reliability.md)
- [Agent MCP Timeout Budgets](system-design/mcp-timeout-budgets.md)
@@ -2,7 +2,7 @@
status: active
system: agents
created: 2026-07-26
updated: 2026-09-04
updated: 2026-09-07
owners:
- Kandev
---
@@ -24,13 +24,30 @@ Operators need newly released agent models without waiting for a Kandev release.
- **AC-AGENTS-RUNTIME-UPDATES-001.2:** The update dialog lists stable versions published for the trusted package. The list contains the newest 50 stable versions plus the active and last observed versions when either falls outside that window. The upstream `latest` stable version is selected initially.
- **AC-AGENTS-RUNTIME-UPDATES-001.3:** The backend classifies the selected action as `update`, `rollback`, `repair`, or `up_to_date`. The UI uses this structural state for copy and approval; it never compares translated labels or version strings itself.
- **AC-AGENTS-RUNTIME-UPDATES-001.4:** Kandev stages the exact trusted `package@version`, ACP-probes that candidate, and activates it only after a successful probe. Candidate failure preserves the prior active version and capability catalogue.
- **AC-AGENTS-RUNTIME-UPDATES-001.5:** Every managed npm runtime has an exact Kandev default version. A successful activation persists an operator-selected exact version for this Kandev install. The effective version is the selected version when present and the Kandev default otherwise.
- **AC-AGENTS-RUNTIME-UPDATES-001.6:** Kandev does not persist the default as a selection. An installation without an operator selection follows default-pin changes delivered by later Kandev releases.
- **AC-AGENTS-RUNTIME-UPDATES-001.5:** Every managed npm runtime has an exact Kandev default version. A successful activation persists an exact operator selection for the current default generation. The effective version is that selection when present and the Kandev default otherwise.
- **AC-AGENTS-RUNTIME-UPDATES-001.6:** Kandev does not persist the default as an operator selection. A change to the shipped package or default starts a new default generation.
- **AC-AGENTS-RUNTIME-UPDATES-001.7:** Every Kandev-built ACP command for the managed package uses the effective exact version, including probes, utility calls, standalone sessions, containers, and SSH executors. Active sessions continue unchanged.
- **AC-AGENTS-RUNTIME-UPDATES-001.8:** Settings lets the operator clear the selected version and return to the Kandev default after that default passes the normal candidate validation.
- **AC-AGENTS-RUNTIME-UPDATES-001.9:** When the weekly or manually started pin-maintenance run finds a changed stable default, it validates the catalogue and opens or refreshes one grouped review pull request without activating a runtime or merging the pull request. When no default changes, it creates no branch or pull request.
- **AC-AGENTS-RUNTIME-UPDATES-001.10:** The pin-maintenance run operates with the repository's built-in Actions authorization and does not require a separately provisioned GitHub App or personal access token. Because built-in-token branch and pull-request events do not recursively start validation workflows, the run explicitly dispatches the six required validation workflows against the exact updater branch commit after creating or refreshing the pull request. The repository or organization setting **Allow GitHub Actions to create and approve pull requests** must be enabled, and verification checks that setting.
### REQ-AGENTS-RUNTIME-UPDATES-002: Activate Reviewed Defaults After an Upgrade
**Intent:** A Kandev release must activate its reviewed managed-agent versions. An older operator selection must not hide a new shipped default.
**User story:** As an operator, I want a Kandev upgrade to activate its reviewed agent versions, so that the release works with its tested runtimes.
#### Acceptance criteria
- **AC-AGENTS-RUNTIME-UPDATES-002.1:** When startup detects a changed managed package or Kandev default, Kandev shall remove the prior selection for that agent before it becomes ready.
- **AC-AGENTS-RUNTIME-UPDATES-002.2:** When the shipped package and default remain unchanged, Kandev shall preserve a current-generation operator selection across restarts and unrelated Kandev upgrades. An unmarked legacy selection is reset during the first reconciliation.
- **AC-AGENTS-RUNTIME-UPDATES-002.3:** After Kandev activates a new default, Settings shall let the operator select any validated stable version, including an older version.
- **AC-AGENTS-RUNTIME-UPDATES-002.4:** A selection made after default activation shall remain effective until the operator changes it or a later shipped default changes.
- **AC-AGENTS-RUNTIME-UPDATES-002.5:** Default activation shall affect future probes and launches only. Kandev shall not replace an agent process that remains active during backend recovery.
- **AC-AGENTS-RUNTIME-UPDATES-002.6:** If Kandev cannot complete default activation, startup shall stop before readiness and retry the activation during the next start.
- **AC-AGENTS-RUNTIME-UPDATES-002.7:** On the first release with this behavior, Kandev shall treat an unmarked legacy selection as part of an earlier default generation.
## System design
The migrated technical source is split into [part 1](../system-design/runtime-updates-01.md), [part 2](../system-design/runtime-updates-02.md).
Upgrade-time default activation is defined in [runtime default activation](../system-design/runtime-default-activation.md).
@@ -0,0 +1,119 @@
---
status: current
system: agents
requirements:
- REQ-AGENTS-RUNTIME-UPDATES-002
created: 2026-09-07
owners:
- Kandev
---
# Managed Runtime Default Activation System Design
## Purpose and boundaries
The agent system owns the effective version of each managed npm runtime. This design defines how a shipped default replaces an older operator selection.
The design covers built-in managed ACP runtimes only. Native agents, passthrough commands, authentication helpers, and active agent processes remain outside this boundary.
## Requirement mapping
| Requirement | Design section |
| --- | --- |
| `REQ-AGENTS-RUNTIME-UPDATES-002` | [Startup reconciliation](#startup-reconciliation), [Persistence](#persistence), [Failure behavior](#failure-behavior) |
## Default generation
A default generation contains these trusted values for one built-in agent:
- The agent ID.
- The managed npm package.
- The exact Kandev default version.
The package and version come from the embedded managed-runtime catalog. A changed package or version starts a new generation for that agent.
An unrelated Kandev release does not start a new generation. Thus, the release preserves a user selection when its reviewed agent default remains unchanged.
## Components and responsibilities
- `agents.ManagedNPMRuntimeAgent` supplies each trusted package and default version.
- `managedruntime.Store` owns active selections and applied default-generation markers.
- `system/settings.Store` persists both record types in the install-wide `settings` table.
- `backendapp.provideServices` completes reconciliation before discovery, probes, command construction, or readiness.
- Existing lifecycle, registry, host-utility, and Settings consumers continue to read the effective selection through `managedruntime.SelectionReader`.
## Persistence
The existing active-selection key remains unchanged:
```text
managed_runtime.active.<agent-id>
```
Each managed agent also has an applied-generation key:
```text
managed_runtime.default.<agent-id>
```
The marker value contains the trusted package and exact default version. The marker does not contain an operator-selected version.
No database schema change is required. Both SQLite and PostgreSQL use the existing install-wide `settings` table.
## Startup reconciliation
`managedruntime.Store.ReconcileDefaults` receives the current built-in default generations. It processes agents in a stable order before runtime consumers start.
For each agent, reconciliation uses this flow:
1. Read the applied-generation marker.
2. Preserve the active selection when the marker matches the current package and default.
3. Delete the active selection when the marker is absent or differs from the current generation.
4. Save the current generation marker after the selection deletion succeeds.
The delete-before-save order is idempotent. If the process stops between these writes, the next startup repeats the safe reset.
A fresh installation writes generation markers and deletes no selections. The first release with this behavior treats each legacy selection without a marker as stale.
## Effective-version behavior
After reconciliation, existing resolution rules remain unchanged. A current-generation operator selection wins over the default. Otherwise, the shipped Kandev default is effective.
Settings can activate any validated stable version after startup. This action creates a new operator selection under the current generation, including a downgrade.
A later default-generation change removes that selection during startup. An unchanged generation preserves the selection across backend and browser restarts.
The reset applies to future probes and commands. Kandev does not replace an agent process that remains active during backend recovery.
## Failure behavior
An unreadable marker, failed selection deletion, or failed marker write stops service initialization. Kandev does not become ready with an unresolved older selection.
The marker advances only after selection deletion succeeds. Therefore, the next startup can retry every incomplete reconciliation.
Reconciliation does not query npm or probe a candidate. The reviewed default already passed repository validation before the Kandev release included it.
## Security
Only built-in agent metadata can create a default generation. Requests, profiles, and stored selection values cannot supply the package or default version.
Reconciliation removes settings keys only for known built-in managed agents. It does not scan or remove unrelated install-wide settings.
## Observability
Kandev logs one structured information event for each changed generation. The event includes the agent ID and the old and new trusted generation values.
Kandev logs the reconciliation error before startup stops. No new metric is required because the operation occurs once before readiness.
## Test strategy
Store tests cover fresh markers, matching generations, changed packages, changed defaults, legacy rows, replay, and each write error.
Backend composition tests prove that reconciliation finishes before runtime selection consumers start. Lifecycle tests prove that later resume commands use the new default.
The existing Settings end-to-end test proves that the operator can select an older validated version after the default becomes effective.
## Related decisions
- [Activate shipped managed-runtime defaults after an upgrade](../../../decisions/2026-09-07-activate-managed-runtime-defaults.md)
- [Validate and persist managed runtime version selection](../../../decisions/2026-08-12-validated-managed-runtime-version-selection.md)
@@ -4,7 +4,7 @@ system: agents
requirements:
- REQ-AGENTS-RUNTIME-UPDATES-001
created: 2026-07-26
updated: 2026-09-04
updated: 2026-09-07
owners:
- Kandev
---
@@ -53,12 +53,10 @@ Managed npm runtime recovery now has an authoritative requirement and design:
and activates it only after a successful probe. Candidate failure preserves
the prior active version and capability catalogue.
- Every managed npm runtime has an exact Kandev default version. A successful
activation persists an operator-selected exact version for this Kandev
install. The effective version is the selected version when present and the
Kandev default otherwise.
- Kandev does not persist the default as a selection. An installation without
an operator selection follows default-pin changes delivered by later Kandev
releases.
activation persists an exact selection for the current default generation.
The selection is effective until the operator or a later generation replaces it.
- Kandev does not persist the default as an operator selection. Startup removes
an earlier selection when the trusted package or default version changes.
- Every Kandev-built ACP command for the managed package uses the effective
exact version, including probes, utility calls, standalone sessions,
containers, and SSH executors. Active sessions continue unchanged.
@@ -115,6 +113,8 @@ remain outside this version action when they use another installer or package.
Decision:
[ADR-2026-08-12-validated-managed-runtime-version-selection](../../../decisions/2026-08-12-validated-managed-runtime-version-selection.md).
Upgrade-time precedence is defined by the
[runtime default activation design](runtime-default-activation.md).
## Version and operation semantics
@@ -123,9 +123,8 @@ Kandev distinguishes six version values:
- `current_version` is the version reported by the last successful host ACP
probe. It can be absent after a failed probe.
- `default_version` is the exact reviewed version shipped with Kandev.
- `active_version` is the exact operator-selected version persisted for future
managed-runtime commands. It is absent when the operator follows the Kandev
default.
- `active_version` is the exact operator-selected version for the current
default generation. It is absent when the operator follows the Kandev default.
- `effective_version` is `active_version` when present and `default_version`
otherwise. It is never empty for a supported managed runtime.
- `latest_version` is npm's stable `latest` version from the most recent update
@@ -302,7 +301,7 @@ produces a repair job.
## Command routing
- Every managed-agent command resolves the effective version immediately before
building its command and emits the trusted `package@effective_version` spec.
building its command. It emits the trusted `package@effective_version` spec.
- Boot probes, manual capability refreshes, model-configuration resolution,
sessionless utility prompts, standalone sessions, containers, and SSH
executors use the same effective-version resolver.
@@ -354,10 +353,12 @@ authoritative for launch-time stale metadata recovery.
## Persistence guarantees
- The trusted package identity and operator-selected version are stored
install-wide per built-in agent in the system settings store and survive backend and browser
restarts. A record whose package no longer matches the agent's built-in
metadata is treated as having no active selection; the replacement package's
reviewed default becomes effective.
install-wide per built-in agent. Current-generation selections survive
restarts while the shipped package and default version remain unchanged. A
legacy selection without a generation marker is reset during the first
startup that applies this behavior.
- Startup records one applied default generation per managed agent. A changed
package or default removes the earlier selection before runtime consumers start.
- The Kandev default is compiled into the managed runtime catalogue and is not
copied into the settings database. Clearing a selection deletes its settings
record only after the default candidate passes validation.
@@ -419,14 +420,13 @@ authoritative for launch-time stale metadata recovery.
**WHEN** an operator selects an older published stable version and approves
**Roll back runtime**, **THEN** Kandev prepares and probes that exact version,
persists it only after success, and restores its model list without restart.
- **GIVEN** a healthy exact active version, **WHEN** Kandev restarts, **THEN**
boot probes and later managed commands use the same exact version.
- **GIVEN** a healthy exact active version and an unchanged default generation,
**WHEN** Kandev restarts, **THEN** boot probes and later commands use that version.
- **GIVEN** an agent has no operator selection, **WHEN** Kandev builds any of
its managed npm ACP commands, **THEN** the command uses the exact reviewed
Kandev default and never an unversioned package spec.
- **GIVEN** an agent has a validated operator selection, **WHEN** Kandev builds
a local, container, or SSH managed npm ACP command, **THEN** the command uses
that exact selection instead of the Kandev default.
- **GIVEN** an agent has a current-generation operator selection, **WHEN**
Kandev builds a managed ACP command, **THEN** the command uses that selection.
- **GIVEN** an operator selection exists, **WHEN** the operator chooses **Use
Kandev default**, **THEN** Kandev validates the exact default, deletes the
selection only after success, and future commands follow shipped defaults.
@@ -519,8 +519,8 @@ problem solved by explicit dispatch.
## Out of scope
- Automatic runtime installation, automatic operator-selection changes, and
automatic rollback after launch failure.
- Automatic runtime installation, selection changes outside default-generation
activation, and automatic rollback after launch failure.
- Global npm cache cleanup, registry replacement, dependency substitution, or
automatic selection of another package version.
- Prerelease, tag, arbitrary package-spec, registry, or shell-command input.
@@ -4,7 +4,7 @@ system: agents
requirements:
- REQ-AGENTS-RUNTIME-UPDATES-001
created: 2026-07-26
updated: 2026-08-22
updated: 2026-09-07
owners:
- Kandev
---
@@ -28,14 +28,13 @@ This design preserves the technical source detail for `REQ-AGENTS-RUNTIME-UPDATE
**WHEN** an operator selects an older published stable version and approves
**Roll back runtime**, **THEN** Kandev prepares and probes that exact version,
persists it only after success, and restores its model list without restart.
- **GIVEN** a healthy exact active version, **WHEN** Kandev restarts, **THEN**
boot probes and later managed commands use the same exact version.
- **GIVEN** a healthy exact active version and an unchanged default generation,
**WHEN** Kandev restarts, **THEN** boot probes and later commands use that version.
- **GIVEN** an agent has no operator selection, **WHEN** Kandev builds any of
its managed npm ACP commands, **THEN** the command uses the exact reviewed
Kandev default and never an unversioned package spec.
- **GIVEN** an agent has a validated operator selection, **WHEN** Kandev builds
a local, container, or SSH managed npm ACP command, **THEN** the command uses
that exact selection instead of the Kandev default.
- **GIVEN** an agent has a current-generation operator selection, **WHEN**
Kandev builds a managed ACP command, **THEN** the command uses that selection.
- **GIVEN** an operator selection exists, **WHEN** the operator chooses **Use
Kandev default**, **THEN** Kandev validates the exact default, deletes the
selection only after success, and future commands follow shipped defaults.
@@ -101,8 +100,8 @@ This design preserves the technical source detail for `REQ-AGENTS-RUNTIME-UPDATE
## Out of scope
- Automatic runtime installation, automatic operator-selection changes, and
automatic rollback after launch failure.
- Automatic runtime installation, selection changes outside default-generation
activation, and automatic rollback after launch failure.
- Global npm cache cleanup, registry replacement, dependency substitution, or
automatic selection of another package version.
- Prerelease, tag, arbitrary package-spec, registry, or shell-command input.