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
8 changes: 4 additions & 4 deletions providers/app-studio/api/assistant_runtime_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import (
"github.com/cloudwego/eino/compose"
)

// Runtime data-plane assistant tools. These wire the existing SandboxRunner
// data-plane verbs (log, restart, env) to the project assistant so it can
// Runtime data-plane assistant tools. These wire the development data-plane
// verbs (log, restart, env) to the project assistant so it can
// diagnose and drive the live development sandbox instead of only guessing at
// its state. They mirror the runtime status/preview graph tools: read-only
// tools (logs) run unwrapped, runtime-mutating tools (restart, env) are wrapped
Expand Down Expand Up @@ -65,8 +65,8 @@ type projectAssistantRuntimeEnvToolInput struct {
Restart *bool `json:"restart,omitempty" jsonschema_description:"Whether to restart the dev process so the new environment takes effect. Defaults to true."`
}

// projectSandboxEnvRequest is the data-plane payload for the SandboxRunner env
// verb; the infrastructure provider forwards it to the in-pod runner.
// projectSandboxEnvRequest is the data-plane payload for the env verb; the
// infrastructure provider forwards it to the in-pod dev agent.
type projectSandboxEnvRequest struct {
Env map[string]string `json:"env"`
Restart bool `json:"restart"`
Expand Down
12 changes: 6 additions & 6 deletions providers/app-studio/api/assistant_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ type projectAssistantRuntimeWorkflowInput struct {
SessionSnapshot *projectEinoAssistantSessionSnapshot
AppDeployment projectAssistantAppDeploymentRequest
// RuntimeResolved is set by the status/preview tool input builders once
// they have queried the live development SandboxRunner runtime.
// RuntimeHasBinding is false when the project has no sandbox runner
// they have queried the live development runtime.
// RuntimeHasBinding is false when the project has no development
// binding yet — i.e. genuinely nothing is deployed. RuntimePreview carries
// the readiness state plus the signed preview URL when ready.
RuntimeResolved bool
Expand Down Expand Up @@ -162,7 +162,7 @@ type projectAssistantWorkflowRunContext struct {
RunState *projectEinoAssistantRunState
// Identity and Client carry the caller's tenant identity and project
// client so runtime/preview tools can query the live development
// SandboxRunner runtime instead of returning a placeholder status.
// runtime instead of returning a placeholder status.
Identity identity
Client *asclient.Client
}
Expand Down Expand Up @@ -582,7 +582,7 @@ func projectAssistantRuntimeWorkflowInputFromStatusTool(runCtx projectAssistantW
if runCtx.RunState != nil {
input.SessionSnapshot = runCtx.RunState.SessionSnapshot()
}
// Resolve the live development SandboxRunner runtime so the status and
// Resolve the live development runtime so the status and
// preview tools report the real deployment state instead of a static
// not_configured placeholder. A nil client (e.g. background runs without
// a project client) leaves the input unresolved and the format functions
Expand Down Expand Up @@ -771,8 +771,8 @@ func formatProjectAssistantPreviewURLResult(ctx context.Context, input projectAs
if err := ctx.Err(); err != nil {
return nil, err
}
// Prefer the live development SandboxRunner preview when it has been
// resolved for this run.
// Prefer the live development preview when it has been resolved for
// this run.
if input.RuntimeResolved && input.RuntimeHasBinding {
preview := input.RuntimePreview
if preview.Ready && strings.TrimSpace(preview.PreviewURL) != "" {
Expand Down
3 changes: 1 addition & 2 deletions providers/app-studio/api/development_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ type projectDevelopmentSyncTargetInfo struct {
ResourceName string

// Resource / Kind / APIVersion are the instance coordinates the data
// plane and tenant client address (sandboxrunners, or the Project
// template's instanceCRD).
// plane and tenant client address (the Project template's instanceCRD).
Resource string `json:"Resource,omitempty"`
Kind string `json:"Kind,omitempty"`
APIVersion string `json:"APIVersion,omitempty"`
Expand Down
6 changes: 3 additions & 3 deletions providers/app-studio/apis/ai/v1alpha1/types_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ type ProjectSpec struct {
// Project's development environment (docs/app-studio-template-sandboxes.md).
// When set, the development binding is generated from the Template's
// instanceCRD with kedgeMode: development, and file sync routes per the
// Template's declared development components. Empty keeps the legacy
// SandboxRunner development binding (transitional; retired with the
// sandbox-runner template).
// Template's declared development components. Empty means the project has
// no development environment yet — one must be selected before any
// development runtime surface (sync, preview, logs) works.
// +optional
Template *ProjectTemplateSpec `json:"template,omitempty"`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,9 @@ spec:
Project's development environment (docs/app-studio-template-sandboxes.md).
When set, the development binding is generated from the Template's
instanceCRD with kedgeMode: development, and file sync routes per the
Template's declared development components. Empty keeps the legacy
SandboxRunner development binding (transitional; retired with the
sandbox-runner template).
Template's declared development components. Empty means the project has
no development environment yet — one must be selected before any
development runtime surface (sync, preview, logs) works.
properties:
name:
description: Name is the Template's catalog name (e.g. "application").
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ spec:
resources:
- group: ai.kedge.faros.sh
name: projects
schema: v260704-936c379.projects.ai.kedge.faros.sh
schema: v260711-a1a2bb6.projects.ai.kedge.faros.sh
storage:
crd: {}
status: {}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: apis.kcp.io/v1alpha1
kind: APIResourceSchema
metadata:
name: v260704-936c379.projects.ai.kedge.faros.sh
name: v260711-a1a2bb6.projects.ai.kedge.faros.sh
spec:
group: ai.kedge.faros.sh
names:
Expand Down Expand Up @@ -206,9 +206,9 @@ spec:
Project's development environment (docs/app-studio-template-sandboxes.md).
When set, the development binding is generated from the Template's
instanceCRD with kedgeMode: development, and file sync routes per the
Template's declared development components. Empty keeps the legacy
SandboxRunner development binding (transitional; retired with the
sandbox-runner template).
Template's declared development components. Empty means the project has
no development environment yet — one must be selected before any
development runtime surface (sync, preview, logs) works.
properties:
name:
description: Name is the Template's catalog name (e.g. "application").
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: apis.kcp.io/v1alpha1
kind: APIResourceSchema
metadata:
name: v260704-936c379.projects.ai.kedge.faros.sh
name: v260711-a1a2bb6.projects.ai.kedge.faros.sh
spec:
group: ai.kedge.faros.sh
names:
Expand Down Expand Up @@ -206,9 +206,9 @@ spec:
Project's development environment (docs/app-studio-template-sandboxes.md).
When set, the development binding is generated from the Template's
instanceCRD with kedgeMode: development, and file sync routes per the
Template's declared development components. Empty keeps the legacy
SandboxRunner development binding (transitional; retired with the
sandbox-runner template).
Template's declared development components. Empty means the project has
no development environment yet — one must be selected before any
development runtime surface (sync, preview, logs) works.
properties:
name:
description: Name is the Template's catalog name (e.g. "application").
Expand Down
15 changes: 8 additions & 7 deletions providers/app-studio/deploy/chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ assistant:
# deployments whose users should not see implementation detail.
toolDisclosure: ""

# App Studio no longer holds a kubeconfig to the runtime cluster. The sandbox
# data plane (sync, logs, restart, preview readiness) is served by the
# infrastructure provider as subresources on the SandboxRunner instance, reached
# through the hub as the calling user. See docs/app-studio-runtime-decoupling.md.
# App Studio no longer holds a kubeconfig to the runtime cluster. The
# development data plane (sync, logs, restart, preview readiness) is served by
# the infrastructure provider as subresources on the project's template
# instance, reached through the hub as the calling user.
# See docs/app-studio-runtime-decoupling.md.

# The SandboxRunner runner image is NOT configured here — the sandbox-runner
# template declares it as a schema field with a sane default (the web-app
# convention). App Studio passes only the project reference. See
# Development runtime images are NOT configured here — the infrastructure
# provider owns them (its chart's development.images / KEDGE_DEV_IMAGE_*).
# App Studio passes only the project reference. See
# providers/infrastructure/docs/template-conventions.md.

# App Studio chat-message persistence. Prefer databaseURLSecretRef in
Expand Down
11 changes: 6 additions & 5 deletions providers/infrastructure/backend/kro/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ type Backend struct {
// See substituteTokens in rgd.go. The tokens are the exposure-layer Gateway
// parent (${kedge.gatewayName}/${kedge.gatewayNamespace}) — the ONE Gateway
// every template's HTTPRoutes attach to (cfgate cloudflare-tunnel in prod,
// envoy locally) — plus ${kedge.sandboxPreviewBaseDomain}; per-instance
// envoy locally) — plus ${kedge.appPublicPort} and the dev-overlay images
// (${kedge.devImage.<toolchain>}, ${kedge.devAgentImage}); per-instance
// inputs like container images are schema fields with defaults, not tokens
// (see providers/infrastructure/docs/template-conventions.md).
tokens map[string]string
Expand All @@ -87,16 +88,16 @@ const (
// template edit):
//
// - KEDGE_GATEWAY_NAME / KEDGE_GATEWAY_NAMESPACE — the exposure-layer Gateway
// parent every template's HTTPRoutes (apps AND sandbox previews) attach to
// (defaults "cloudflare-tunnel" / "cfgate-system").
// parent every template's HTTPRoutes attach to (defaults
// "cloudflare-tunnel" / "cfgate-system").
// - KEDGE_APP_PUBLIC_PORT — bare port number appended (as ":<port>") to
// synthesized exposure URLs via ${kedge.appPublicPort}. Unset in
// production (443 implied); local kind sets 10443 (the envoy
// port-forward).
//
// Per-instance inputs (container images, etc.) are NOT env tokens — templates
// declare them as schema fields with sane defaults (e.g. simple-webapp's
// spec.image), the same convention every other template follows. See
// declare them as schema fields (e.g. simple-webapp's spec.image, database's
// spec.version), the same convention every other template follows. See
// providers/infrastructure/docs/template-conventions.md.
func New(runtime dynamic.Interface) *Backend {
gatewayName := os.Getenv("KEDGE_GATEWAY_NAME")
Expand Down
13 changes: 6 additions & 7 deletions providers/infrastructure/backend/kro/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ const (
)

// e2eMinimalSpecs supplies a valid instance spec for templates that ship no
// sampleValues (those that do — application, database — use them directly).
// sampleValues. Currently empty — every seed template ships sampleValues —
// but kept as the escape hatch for a future template that can't.
var e2eMinimalSpecs = map[string]map[string]any{}

// e2ePlatformStamped are spec fields a platform component normally writes onto an
Expand Down Expand Up @@ -118,8 +119,7 @@ func TestE2ESeedTemplates(t *testing.T) {
dyn, mapper := e2eClients(t)
// A per-run nonce makes every instance (and therefore every child object)
// uniquely named, so re-running against a reused cluster can't collide with a
// previous run's not-yet-garbage-collected objects. 16 hex digits because
// sandbox-runner's name must match ^kedge-sandbox-[a-f0-9]{16}$.
// previous run's not-yet-garbage-collected objects.
runID := fmt.Sprintf("%016x", time.Now().UnixNano())
dir := filepath.Join("..", "..", "install", "templates")
entries, err := os.ReadDir(dir)
Expand Down Expand Up @@ -280,10 +280,9 @@ func e2eInstance(t *testing.T, tmpl *infrav1alpha1.Template, runID string) *unst
}

// e2eInstanceName returns a per-run-unique instance name so child objects from
// one run never collide with a prior run's on a reused cluster. sandbox-runner's
// name is constrained to ^kedge-sandbox-[a-f0-9]{16}$, so it takes the 16-hex
// runID verbatim; every other template suffixes its base name with the LOW
// half of it. The low half, not the high: the runID is a hex UnixNano, whose
// one run never collide with a prior run's on a reused cluster. Each template
// suffixes its base name with the LOW half of the hex-UnixNano runID.
// The low half, not the high: the runID's
// top 32 bits change every ~4s — two instances created seconds apart (the
// dev-mode e2e's production/development pair) would collide on runID[:8], and
// the second create would silently adopt the first's terminating instance.
Expand Down
13 changes: 13 additions & 0 deletions providers/infrastructure/controller/template/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
return ctrl.Result{Requeue: true}, nil
}

// Retired platform templates are deleted on sight (see retired.go).
// AFTER the finalizer add so the deletion runs the full finalize chain,
// BEFORE the backend lookup so a retired template whose backend is no
// longer registered still gets swept instead of parking on a
// BackendNotFound condition.
Comment on lines +121 to +125
if reason, retired := retiredTemplates[tmpl.Name]; retired {
logger.Info("deleting retired platform template", "reason", reason)
if err := r.Client.Delete(ctx, &tmpl); err != nil && !apierrors.IsNotFound(err) {
return ctrl.Result{}, fmt.Errorf("delete retired template: %w", err)
}
return ctrl.Result{}, nil // the deletion event re-enters via finalize
}

// Look up backend FIRST so a typo on spec.backend never causes a
// CRD to be created without a corresponding handler.
b, ok := r.Backends.Get(tmpl.Spec.Backend)
Expand Down
59 changes: 59 additions & 0 deletions providers/infrastructure/controller/template/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,65 @@ func TestReconcileBackendNotFound(t *testing.T) {
}
}

// TestReconcileRetiredTemplateIsSwept pins the retirement mechanism
// (retired.go): a Template whose name is on the retired list — e.g. left
// behind in a workspace seeded before the template was removed from the
// catalog — is deleted by the reconciler itself and dismantled through the
// normal finalize chain (backend teardown + CRD/APIExport cleanup), without
// any operator action.
func TestReconcileRetiredTemplateIsSwept(t *testing.T) {
if _, ok := retiredTemplates["sandbox-runner"]; !ok {
t.Fatal("sandbox-runner is no longer on the retired list; pick another retired name for this test")
}
tmpl := newTestTemplate(t, "sandbox-runner")
tmpl.Spec.InstanceCRD.Kind = "SandboxRunner"
tmpl.Spec.InstanceCRD.Resource = "sandboxrunners"
r, dyn, stb := newTestReconciler(t, tmpl)

reconcileUntilSettled(t, r, "sandbox-runner")

// The Template must be gone — deleted by the controller, finalizer
// removed by the finalize chain.
var post infrav1alpha1.Template
if err := r.Client.Get(context.Background(), types.NamespacedName{Name: "sandbox-runner"}, &post); err == nil {
t.Fatalf("retired template still present after reconcile (deletionTimestamp=%v, finalizers=%v)",
post.DeletionTimestamp, post.Finalizers)
}
Comment on lines +268 to +272

// The finalize chain ran: the backend saw a teardown, and no
// per-template CRD or APIExport entry survives. Retirement fires before
// the CRD is ever authored on a fresh workspace, so absence — not
// deletion — is the invariant.
if len(stb.SeenTeardowns) < 1 {
t.Fatalf("expected the finalize chain to call TeardownTemplate; got %v", stb.SeenTeardowns)
}
if _, err := dyn.Resource(crdGVR).Get(context.Background(), perTemplateCRDName(tmpl), metav1.GetOptions{}); err == nil {
t.Fatal("per-template CRD exists for a retired template")
}
Comment on lines +281 to +283
export, err := dyn.Resource(apiExportGVR).Get(context.Background(), APIExportName, metav1.GetOptions{})
if err != nil {
t.Fatalf("get APIExport: %v", err)
}
resources, err := getAPIExportResources(export)
if err != nil {
t.Fatalf("decode resources: %v", err)
}
if len(resources) != 0 {
t.Fatalf("APIExport still carries entries for a retired template: %v", resources)
}

// Re-applying the retired template must sweep it again — retirement is
// enforced by the watch loop, not a one-shot migration.
again := newTestTemplate(t, "sandbox-runner")
if err := r.Client.Create(context.Background(), again); err != nil {
t.Fatalf("re-create retired template: %v", err)
}
reconcileUntilSettled(t, r, "sandbox-runner")
if err := r.Client.Get(context.Background(), types.NamespacedName{Name: "sandbox-runner"}, &post); err == nil {
t.Fatal("re-applied retired template survived reconciliation")
}
Comment on lines +303 to +305
}

func TestReconcileDelete(t *testing.T) {
tmpl := newTestTemplate(t, "delgone")
r, dyn, stb := newTestReconciler(t, tmpl)
Expand Down
42 changes: 42 additions & 0 deletions providers/infrastructure/controller/template/retired.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
Copyright 2026 The Faros Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0
*/

package template

// retiredTemplates names platform templates that have been RETIRED: removed
// from the seed catalog and no longer supported by the platform. Seeding only
// upserts the CURRENT catalog, so a live workspace initialized before a
// retirement keeps serving the old Template forever — visible in the portal,
// selectable by agents, backed by code paths that no longer exist. The
// reconciler deletes any Template on this list on sight; the normal finalize
// chain (backend teardown → APIExport entry removal → per-template CRD
// deletion) then dismantles what it authored, exactly as if an operator had
// deleted it by hand. Because this runs in the watch loop, a re-applied
// retired template is removed again — retirement is enforced, not one-shot.
Comment on lines +18 to +22
//
// The value is the reason, logged when the deletion happens. Names on this
// list are reserved: an operator's hand-authored Template with a retired name
// will be deleted too, so never reuse a retired name for something new.
//
// Operators who fully manage their own catalog (INFRASTRUCTURE_SKIP_SEED_TEMPLATES)
// still get retirement — the list names platform templates only, which such
// catalogs should not carry.
var retiredTemplates = map[string]string{
// The dedicated sandbox workload was replaced by template-native
// development mode: any workload template with a spec.development block
// (docs/app-studio-template-sandboxes.md). App Studio refuses projects
// without a template since #394, so instances of this can no longer work.
"sandbox-runner": "replaced by template-native development mode (spec.development on workload templates)",

// Preview routing is declared per-template (each dev-capable template's
// own HTTPRoute serves the preview) since the Gateway API exposure work;
// the standalone preview-route template has no consumer.
"sandbox-preview-httproute": "preview routing is declared by each template's own HTTPRoute",
}
2 changes: 1 addition & 1 deletion providers/infrastructure/dataplane/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import (
)

// ContractGetter returns the data-plane contract for an instance resource
// (the lowercase plural, e.g. "sandboxrunners"). Returns a nil contract with no
// (the lowercase plural, e.g. "simplewebapps"). Returns a nil contract with no
// error when the template declares no data plane, and an error when no template
// owns the resource.
type ContractGetter interface {
Expand Down
Loading
Loading