{
  "revision": "6f341b4e6d356dd28d1a90ec65e220b98a9bbb96",
  "resources": [
    {
      "source": "providers/infrastructure/install/crds/infrastructure.faros.sh_instances.yaml",
      "schema": {
        "apiVersion": "apiextensions.k8s.io/v1",
        "kind": "CustomResourceDefinition",
        "metadata": {
          "annotations": {
            "controller-gen.kubebuilder.io/version": "v0.16.5"
          },
          "name": "instances.infrastructure.faros.sh"
        },
        "spec": {
          "group": "infrastructure.faros.sh",
          "names": {
            "categories": [
              "faros"
            ],
            "kind": "Instance",
            "listKind": "InstanceList",
            "plural": "instances",
            "shortNames": [
              "inst"
            ],
            "singular": "instance"
          },
          "scope": "Cluster",
          "versions": [
            {
              "additionalPrinterColumns": [
                {
                  "jsonPath": ".spec.template",
                  "name": "Template",
                  "type": "string"
                },
                {
                  "jsonPath": ".status.phase",
                  "name": "Phase",
                  "type": "string"
                },
                {
                  "jsonPath": ".status.conditions[?(@.type==\"Ready\")].status",
                  "name": "Ready",
                  "type": "string"
                },
                {
                  "jsonPath": ".status.url",
                  "name": "URL",
                  "priority": 1,
                  "type": "string"
                },
                {
                  "jsonPath": ".metadata.creationTimestamp",
                  "name": "Age",
                  "type": "date"
                }
              ],
              "name": "v1alpha1",
              "schema": {
                "openAPIV3Schema": {
                  "description": "Instance is one provisioned unit of a catalog Template \u2014 a deployed\napplication, a search backend, a database. It is the ONE tenant-facing\ninstance kind of the infrastructure provider: which product an Instance\nis comes from spec.template (data), not from its GroupVersionKind, so\nadding a Template to the catalog never changes the API surface, the\nAPIExport's resource list, or any consumer's permission claims.\n\nspec.values carries the template-shaped input, validated by the instance\ncontroller against Template.spec.schema (structural schema, defaults, and\nCEL rules) rather than by the apiserver \u2014 an invalid Instance is admitted\nand reports Ready=False/InvalidValues instead of being rejected at\nadmission. The controller materializes the instance as a per-template kro\nCR on the runtime cluster and mirrors that CR's status back here, so\nstatus carries the platform baseline (phase/message/conditions) plus\nwhatever the template's backend projects (url, runtimeNamespace,\ncomponents, outputs, \u2026).",
                  "properties": {
                    "apiVersion": {
                      "description": "APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
                      "type": "string"
                    },
                    "kind": {
                      "description": "Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
                      "type": "string"
                    },
                    "metadata": {
                      "type": "object"
                    },
                    "spec": {
                      "description": "InstanceSpec is the desired state.",
                      "properties": {
                        "template": {
                          "description": "Template names the catalog Template this instance is provisioned from\n(Template.metadata.name in the provider workspace, discoverable through\nthe read-only templates catalog). Immutable: changing the product of a\nlive instance would strand the old backend state, so it is a delete +\nrecreate.",
                          "maxLength": 253,
                          "minLength": 1,
                          "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$",
                          "type": "string",
                          "x-kubernetes-validations": [
                            {
                              "message": "spec.template is immutable",
                              "rule": "self == oldSelf"
                            }
                          ]
                        },
                        "values": {
                          "description": "Values is the template-shaped input \u2014 exactly the object\nTemplate.spec.schema describes, the same payload that used to be the\nwhole spec of the retired per-template kinds. The platform-reserved\nfields (farosMode, farosActions*, plus controller-stamped fields like\nexpose.fqdn, farosCluster, credentialsSecretName) live in here too, so\n\"spec\" in template schemas, RGD ${schema.spec.*} expressions, and view\ndefinitions all keep meaning this object.\n\nThe apiserver preserves it verbatim; the instance controller validates\nit against the Template's schema and reports violations on the Ready\ncondition.",
                          "type": "object",
                          "x-kubernetes-preserve-unknown-fields": true
                        }
                      },
                      "required": [
                        "template"
                      ],
                      "type": "object"
                    },
                    "status": {
                      "description": "InstanceStatus is the observed state: a platform-guaranteed baseline plus\nwhatever the template's backend projects. The struct only types the\nbaseline \u2014 backend-projected fields (url, runtimeNamespace, components,\noutputs, controlSecretRef, \u2026) are preserved as unknown fields, exactly as\nthe retired per-template CRDs did, so a template's status contract is\nstill authored in its RGD statusMapping and not here.",
                      "properties": {
                        "conditions": {
                          "description": "Conditions carries both provider-owned conditions (Valid,\nOIDCConfigured) and conditions mirrored from the runtime kro instance\n(Ready, ResourcesReady, \u2026). The shape is deliberately looser than\nmetav1.Condition because mirrored backend conditions may omit reason.",
                          "items": {
                            "description": "InstanceCondition is the loose condition shape shared by the platform\nbaseline and mirrored backend conditions (matches the status schema the\nretired per-template CRDs carried).",
                            "properties": {
                              "lastTransitionTime": {
                                "type": "string"
                              },
                              "message": {
                                "type": "string"
                              },
                              "observedGeneration": {
                                "format": "int64",
                                "type": "integer"
                              },
                              "reason": {
                                "type": "string"
                              },
                              "status": {
                                "type": "string"
                              },
                              "type": {
                                "type": "string"
                              }
                            },
                            "required": [
                              "status",
                              "type"
                            ],
                            "type": "object"
                          },
                          "type": "array"
                        },
                        "farosNetworkPhase": {
                          "description": "NetworkPhase is the controller-owned runtime network phase. It is\nmirrored from the runtime Instance only after the runtime reports Ready;\ncallers must not use spec.values as an execution-readiness signal.",
                          "type": "string"
                        },
                        "message": {
                          "description": "Message carries human-readable detail for the current phase.",
                          "type": "string"
                        },
                        "observedGeneration": {
                          "description": "ObservedGeneration mirrors metadata.generation last reconciled by the\ninstance controller.",
                          "format": "int64",
                          "type": "integer"
                        },
                        "phase": {
                          "description": "Phase is the coarse lifecycle summary (Pending / Ready / Failed),\nderived from conditions for consumers that want one word.",
                          "type": "string"
                        },
                        "template": {
                          "description": "Template echoes spec.template as resolved at last reconcile.",
                          "type": "string"
                        },
                        "templateVersion": {
                          "description": "TemplateVersion is the Template.spec.version the instance was last\nreconciled against.",
                          "type": "string"
                        }
                      },
                      "type": "object",
                      "x-kubernetes-preserve-unknown-fields": true
                    }
                  },
                  "required": [
                    "spec"
                  ],
                  "type": "object"
                }
              },
              "served": true,
              "storage": true,
              "subresources": {
                "status": {}
              }
            }
          ]
        }
      }
    },
    {
      "source": "providers/infrastructure/install/crds/infrastructure.faros.sh_templates.yaml",
      "schema": {
        "apiVersion": "apiextensions.k8s.io/v1",
        "kind": "CustomResourceDefinition",
        "metadata": {
          "annotations": {
            "controller-gen.kubebuilder.io/version": "v0.16.5"
          },
          "name": "templates.infrastructure.faros.sh"
        },
        "spec": {
          "group": "infrastructure.faros.sh",
          "names": {
            "categories": [
              "faros"
            ],
            "kind": "Template",
            "listKind": "TemplateList",
            "plural": "templates",
            "shortNames": [
              "tmpl"
            ],
            "singular": "template"
          },
          "scope": "Cluster",
          "versions": [
            {
              "additionalPrinterColumns": [
                {
                  "jsonPath": ".spec.displayName",
                  "name": "Display",
                  "type": "string"
                },
                {
                  "jsonPath": ".spec.backend",
                  "name": "Backend",
                  "type": "string"
                },
                {
                  "jsonPath": ".spec.instanceCRD.kind",
                  "name": "Kind",
                  "type": "string"
                },
                {
                  "jsonPath": ".status.conditions[?(@.type==\"Ready\")].status",
                  "name": "Ready",
                  "type": "string"
                },
                {
                  "jsonPath": ".metadata.creationTimestamp",
                  "name": "Age",
                  "type": "date"
                }
              ],
              "name": "v1alpha1",
              "schema": {
                "openAPIV3Schema": {
                  "description": "Template is the platform-owned catalog entry for one provisionable\nthing \u2014 a Redis cache, a Postgres database, a packaged application.\nOperators apply Templates to the provider workspace\n(root:faros:providers:infrastructure). The Template controller\nreacts by:\n\n 1. Materializing the per-template CRD declared in spec.instanceCRD\n    (e.g. redis.infrastructure.faros.sh) into the cluster's\n    CRD set, with OpenAPI validation derived from spec.schema.\n 2. Adding that CRD to APIExport.spec.schemas so tenants who\n    APIBind to the infrastructure provider can see and create\n    instances.\n 3. Calling Backend.SetupTemplate on the backend named in\n    spec.backend. The backend does whatever backend-specific\n    bookkeeping it needs (the kro backend authors an RGD; future\n    terraform / cloud backends stage modules / validate credentials).\n\nTenants discover Templates read-only via a CachedResource (PR B);\ninstances are CRs of the per-template CRD (PR C). The Template CR\nitself is never tenant-facing as authorable input \u2014 it's the\nplatform's source of truth.",
                  "properties": {
                    "apiVersion": {
                      "description": "APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
                      "type": "string"
                    },
                    "kind": {
                      "description": "Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
                      "type": "string"
                    },
                    "metadata": {
                      "type": "object"
                    },
                    "spec": {
                      "description": "TemplateSpec is the desired state.",
                      "properties": {
                        "agent": {
                          "description": "Agent is operational guidance for AI agents that discover this template\nvia MCP \u2014 what it provisions, when to choose it, prerequisites, and where\nits outputs (URL, DB connection Secret, \u2026) land. It complements the\nhuman-facing displayName/description (which target the portal UI) and is\nnot rendered in the form.",
                          "properties": {
                            "outputs": {
                              "description": "Outputs describe where the provisioned instance's results land so an agent\ncan discover and wire them \u2014 e.g. \"status.url: public app URL\",\n\"Secret <name>-db-credentials key 'uri': postgres:// connection string\".\nOne output per entry.",
                              "items": {
                                "type": "string"
                              },
                              "type": "array"
                            },
                            "prerequisites": {
                              "description": "Prerequisites the caller must satisfy BEFORE provisioning \u2014 e.g. a\ncloud-credentials Secret in the tenant's default namespace carrying\nspecific keys. One human-readable requirement per entry.",
                              "items": {
                                "type": "string"
                              },
                              "type": "array"
                            },
                            "usage": {
                              "description": "Usage is markdown guidance for an agent: what this template provisions,\nwhen to choose it, how the result is exposed (URLs/ingress/auth), and how\nto operate it after provisioning. The primary, free-form field; the\nstructured fields below call out the most actionable specifics.",
                              "maxLength": 8192,
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "backend": {
                          "description": "Backend names the registered backend implementation that\nreconciles instances of this template. The Template controller\nvalidates the backend is registered at admission time\n(PR A scope: validation lives in the controller; future PR\nmoves it to a webhook). Today only \"kro\" and \"stub\" are\nexpected; the seam supports terraform, cloud, etc.",
                          "maxLength": 64,
                          "pattern": "^[a-z][a-z0-9-]*$",
                          "type": "string"
                        },
                        "backendConfig": {
                          "description": "BackendConfig is opaque to the platform; only the named\nbackend interprets it. For \"kro\" it's a resource graph\n(equivalent to an RGD's resources + statusMapping); for a\nhypothetical \"terraform\" backend it would be a module ref and\nvariable mapping. Stored as raw JSON to keep the API surface\nstable as backends evolve.",
                          "type": "object",
                          "x-kubernetes-preserve-unknown-fields": true
                        },
                        "category": {
                          "description": "Category groups templates in the catalog (e.g. \"Databases\",\n\"Workloads\", \"Storage\"). Empty puts the template under an\n\"Other\" bucket.",
                          "maxLength": 64,
                          "type": "string"
                        },
                        "dataPlane": {
                          "description": "DataPlane optionally declares the live data-plane verbs this template's\ninstances expose \u2014 log streaming, a service proxy, sync/restart control \u2014\nand how each resolves to a runtime Service/Secret/port from the instance's\nstatus. The infrastructure provider serves these as subresources on the\ninstance (e.g. sandboxrunners/<name>/log) so consumers reach a workload's\ndata plane without holding a credential to the runtime cluster themselves.\nEmpty means the template's instances expose no data plane.\n\nSee docs/app-studio-runtime-decoupling.md for the end-to-end design.",
                          "properties": {
                            "components": {
                              "additionalProperties": {
                                "description": "TemplateDataPlaneComponent is one component's verb set. Endpoint\nresolution is identical to instance-level endpoints \u2014 servicePath is an\nabsolute status dot-path (per-component Services land under\nstatus.components.<name>.* by backend convention, but any status path\ninside the runtime namespace is valid).",
                                "properties": {
                                  "endpoints": {
                                    "additionalProperties": {
                                      "description": "TemplateDataPlaneEndpoint describes one data-plane verb: either a value served\nstraight from the instance status (FromStatus), or a reverse proxy to a\nService in the instance's runtime namespace.",
                                      "properties": {
                                        "fromStatus": {
                                          "description": "FromStatus serves this verb from the instance CR status with no runtime\nhop (e.g. a \"status\" verb that just returns status). When true, the proxy\nfields below are ignored.",
                                          "type": "boolean"
                                        },
                                        "methods": {
                                          "description": "Methods is the allowed HTTP method allowlist for this verb. Empty allows\nGET only. Ignored when FromStatus.",
                                          "items": {
                                            "type": "string"
                                          },
                                          "type": "array"
                                        },
                                        "port": {
                                          "description": "Port is the Service port name to target (e.g. \"control\", \"preview\").\nRequired unless FromStatus.",
                                          "maxLength": 63,
                                          "type": "string"
                                        },
                                        "servicePath": {
                                          "description": "ServicePath is the status dot-path to a {name, namespace} object naming the\nService to proxy to (e.g. \"status.controlServiceRef\"). When the ref omits a\nnamespace it defaults to RuntimeNamespacePath; a namespace that differs\nfrom RuntimeNamespacePath is rejected. Required unless FromStatus.",
                                          "maxLength": 256,
                                          "type": "string"
                                        },
                                        "stream": {
                                          "description": "Stream marks a long-lived response (e.g. log follow) so the provider\ndisables response buffering and request timeouts. Ignored when FromStatus.",
                                          "type": "boolean"
                                        },
                                        "upgrade": {
                                          "description": "Upgrade allows HTTP connection upgrades (WebSocket / SPDY exec /\nport-forward) through this verb's proxy. Ignored when FromStatus.",
                                          "type": "boolean"
                                        },
                                        "upstreamPath": {
                                          "description": "UpstreamPath is prepended to the caller-supplied path when composing the\nservice-proxy URL (e.g. \"/logs\"). Defaults to \"/\". Ignored when FromStatus.",
                                          "maxLength": 256,
                                          "type": "string"
                                        }
                                      },
                                      "type": "object"
                                    },
                                    "description": "Endpoints maps a verb name to how it resolves for this component.",
                                    "minProperties": 1,
                                    "type": "object"
                                  },
                                  "exec": {
                                    "description": "Exec declares the bounded, non-interactive command capability for this\ncomponent. Exec is deliberately separate from Endpoints: endpoint\nUpgrade is an HTTP proxy feature and must never implicitly grant command\nexecution or Kubernetes SPDY exec access.",
                                    "properties": {
                                      "maxOutputBytes": {
                                        "description": "MaxOutputBytes is the combined stdout/stderr response ceiling. Zero uses\nthe provider default. Values above the provider maximum are rejected.",
                                        "format": "int32",
                                        "maximum": 262144,
                                        "minimum": 0,
                                        "type": "integer"
                                      },
                                      "maxTimeoutSeconds": {
                                        "description": "MaxTimeoutSeconds is the maximum wall-clock duration for one command.\nZero uses the provider default. Values above the provider maximum are\nrejected when the Template contract is resolved.",
                                        "format": "int32",
                                        "maximum": 120,
                                        "minimum": 0,
                                        "type": "integer"
                                      }
                                    },
                                    "type": "object"
                                  }
                                },
                                "required": [
                                  "endpoints"
                                ],
                                "type": "object"
                              },
                              "description": "Components maps a component name to that component's own verb set,\nserved as \u2026/<resource>/<name>/components/<component>/<verb>. Used by\nmulti-tier templates so a caller can sync the backend and restart the\nfrontend independently. Component names should match the template's\nspec.development components where both are declared. Every endpoint\nresolves and is namespace-confined exactly like an instance-level one.",
                              "type": "object"
                            },
                            "endpoints": {
                              "additionalProperties": {
                                "description": "TemplateDataPlaneEndpoint describes one data-plane verb: either a value served\nstraight from the instance status (FromStatus), or a reverse proxy to a\nService in the instance's runtime namespace.",
                                "properties": {
                                  "fromStatus": {
                                    "description": "FromStatus serves this verb from the instance CR status with no runtime\nhop (e.g. a \"status\" verb that just returns status). When true, the proxy\nfields below are ignored.",
                                    "type": "boolean"
                                  },
                                  "methods": {
                                    "description": "Methods is the allowed HTTP method allowlist for this verb. Empty allows\nGET only. Ignored when FromStatus.",
                                    "items": {
                                      "type": "string"
                                    },
                                    "type": "array"
                                  },
                                  "port": {
                                    "description": "Port is the Service port name to target (e.g. \"control\", \"preview\").\nRequired unless FromStatus.",
                                    "maxLength": 63,
                                    "type": "string"
                                  },
                                  "servicePath": {
                                    "description": "ServicePath is the status dot-path to a {name, namespace} object naming the\nService to proxy to (e.g. \"status.controlServiceRef\"). When the ref omits a\nnamespace it defaults to RuntimeNamespacePath; a namespace that differs\nfrom RuntimeNamespacePath is rejected. Required unless FromStatus.",
                                    "maxLength": 256,
                                    "type": "string"
                                  },
                                  "stream": {
                                    "description": "Stream marks a long-lived response (e.g. log follow) so the provider\ndisables response buffering and request timeouts. Ignored when FromStatus.",
                                    "type": "boolean"
                                  },
                                  "upgrade": {
                                    "description": "Upgrade allows HTTP connection upgrades (WebSocket / SPDY exec /\nport-forward) through this verb's proxy. Ignored when FromStatus.",
                                    "type": "boolean"
                                  },
                                  "upstreamPath": {
                                    "description": "UpstreamPath is prepended to the caller-supplied path when composing the\nservice-proxy URL (e.g. \"/logs\"). Defaults to \"/\". Ignored when FromStatus.",
                                    "maxLength": 256,
                                    "type": "string"
                                  }
                                },
                                "type": "object"
                              },
                              "description": "Endpoints maps an instance-level verb name \u2014 the subresource the\nprovider serves, e.g. \"log\", \"proxy\", \"sync\", \"restart\", \"status\" \u2014 to\nhow it resolves. At least one of Endpoints and Components must be\nnon-empty when DataPlane is set.",
                              "type": "object"
                            },
                            "runtimeNamespacePath": {
                              "description": "RuntimeNamespacePath is the status dot-path to the namespace the backend\nowns for this instance (e.g. \"status.runtimeNamespace\"). Every Service and\nSecret a data-plane verb resolves to MUST live in this namespace; the\nresolver rejects refs that point elsewhere. Required when any endpoint\nproxies to the runtime cluster (i.e. anything but a FromStatus endpoint).",
                              "maxLength": 256,
                              "type": "string"
                            },
                            "tokenSecretPath": {
                              "description": "TokenSecretPath is an optional status dot-path to a {name, namespace}\nobject naming the Secret whose \"token\" key the provider injects as the\nX-Sandbox-Control-Token header on upstream requests (the per-instance\ncontrol token). Empty means no token header is added. The named Secret is\nconfined to RuntimeNamespacePath like every other ref.",
                              "maxLength": 256,
                              "type": "string"
                            }
                          },
                          "type": "object"
                        },
                        "description": {
                          "description": "Description is one to three sentences shown beneath the\ndisplay name in catalog cards.",
                          "maxLength": 2048,
                          "type": "string"
                        },
                        "development": {
                          "description": "Development optionally declares how instances of this template run in\ndevelopment mode: which graph components can be hot-swapped to\nplatform-managed dev images with a hot-reload agent, where each\ncomponent's source lives in the project workspace, and how each reloads.\nA template with a Development block can have instances provisioned with\nfarosMode: development (the platform-reserved instance spec field the\nTemplate controller injects); templates without one are\nproduction-only.\n\nSee docs/app-studio-template-sandboxes.md for the end-to-end design.",
                          "properties": {
                            "build": {
                              "description": "Build optionally declares the repository-owned GitHub Actions workflow\nthat builds this template's production images. App Studio observes and\ndispatches this workflow; it never authors or rewrites it. Absence means\nthe template declares no CI workflow.",
                              "properties": {
                                "workflowPath": {
                                  "description": "WorkflowPath is a repository-relative GitHub Actions workflow path. It\nmust live directly under .github/workflows and end in .yml or .yaml.",
                                  "maxLength": 256,
                                  "pattern": "^\\.github/workflows/[^/]+\\.ya?ml$",
                                  "type": "string"
                                }
                              },
                              "required": [
                                "workflowPath"
                              ],
                              "type": "object"
                            },
                            "components": {
                              "additionalProperties": {
                                "description": "TemplateDevelopmentComponent describes one hot-swappable component of the\ngraph in development mode.",
                                "properties": {
                                  "devImage": {
                                    "description": "DevImage is the platform-managed toolchain image the component's\nworkload runs in development mode, in place of the user-supplied\nproduction image. MUST be a ${faros.devImage.<toolchain>} token \u2014 the\nbackend resolves it from provider configuration; tenants never choose\ndev images.",
                                    "maxLength": 128,
                                    "pattern": "^\\$\\{faros\\.devImage\\.[a-z][a-z0-9-]*\\}$",
                                    "type": "string"
                                  },
                                  "imageInput": {
                                    "description": "ImageInput names the production schema input this component's built\nimage feeds when the project is launched (e.g. \"frontendImage\" for the\nfrontend component, \"image\" for a single-component template). It is the\nlink between a development component and the production image field that\nruns it: App Studio builds one OCI image per component (build context =\nWorkspacePath) and, on launch, sets each named input to that component's\nbuilt digest before provisioning the instance with farosMode:\nproduction. Empty means the component produces no launchable image (e.g.\na worker developed in-cluster but not yet promotable). Must match a\ntop-level property of the template's production schema.",
                                    "maxLength": 253,
                                    "pattern": "^[a-zA-Z][a-zA-Z0-9]*$",
                                    "type": "string"
                                  },
                                  "port": {
                                    "description": "Port is the named container port (from the production workload) the\ndev process serves on. The overlay keeps the production Service and\nroute wiring pointed at it. Empty means the component serves no\ntraffic (e.g. a worker).",
                                    "maxLength": 63,
                                    "type": "string"
                                  },
                                  "reload": {
                                    "description": "Reload declares the component's reload procedure, executed by the dev\nagent on file sync. Empty means strategy \"process\" with no rules.",
                                    "properties": {
                                      "rules": {
                                        "description": "Rules name path patterns that require a command BEFORE the process\n(re)starts \u2014 dependency installs, code generation. Evaluated in order;\nevery matching rule's command runs.",
                                        "items": {
                                          "description": "TemplateDevelopmentReloadRule pairs changed-path patterns with the command\nthe dev agent must run before restarting the process.",
                                          "properties": {
                                            "command": {
                                              "description": "Command runs in the component's workingDir before the process restart.",
                                              "maxLength": 4096,
                                              "type": "string"
                                            },
                                            "paths": {
                                              "description": "Paths are glob patterns, relative to the component's workingDir, that\ntrigger this rule (e.g. \"package.json\", \"requirements*.txt\").",
                                              "items": {
                                                "type": "string"
                                              },
                                              "minItems": 1,
                                              "type": "array"
                                            }
                                          },
                                          "required": [
                                            "command",
                                            "paths"
                                          ],
                                          "type": "object"
                                        },
                                        "type": "array"
                                      },
                                      "strategy": {
                                        "description": "Strategy is the baseline action after a sync: \"process\" restarts the\nsupervised dev process (default; a no-op for servers that hot-reload\nthemselves \u2014 the agent only restarts when a rule fires or the process\ndied), \"container\" restarts the whole container (the escape hatch for\ntoolchains that cannot reload in place).",
                                        "enum": [
                                          "process",
                                          "container"
                                        ],
                                        "type": "string"
                                      }
                                    },
                                    "type": "object"
                                  },
                                  "startCommand": {
                                    "description": "StartCommand launches the component's dev process (hot reload is the\nprocess's own job \u2014 vite, uvicorn --reload, air). The dev agent wraps\nand supervises it.",
                                    "maxLength": 4096,
                                    "type": "string"
                                  },
                                  "workingDir": {
                                    "description": "WorkingDir is where the component's workspace PVC is mounted and its\ndev process runs. Defaults to /workspace.",
                                    "maxLength": 256,
                                    "type": "string"
                                  },
                                  "workspacePath": {
                                    "description": "WorkspacePath is the project workspace / repository subdirectory whose\nfiles belong to this component. Builders route file sync by these\nprefixes, and the scaffold follows this layout.\n\nONE NAME RULE: it MUST equal the component's own key (the map key is\nthe component name), so a component is never addressed by two\ndifferent words. Divergence caused repeated bugs \u2014 sync routing and\nlog/restart calls that named the directory instead of the component \u2014\nso the map-level CEL rule on Components rejects it. The single\nexception is \".\" for a template whose one component owns the whole\nworkspace root. Omitting it defaults to the component name.",
                                    "maxLength": 256,
                                    "type": "string"
                                  }
                                },
                                "required": [
                                  "devImage",
                                  "startCommand"
                                ],
                                "type": "object"
                              },
                              "description": "Components maps a component name to its development behavior. Each key\nMUST name a workload resource the template's graph emits (by the\nbackend's component\u2192resource naming convention, e.g. \"frontend\" names\nthe graph resource with id \"frontend\"). Components not listed here run\nexactly as declared in production mode \u2014 a dev sandbox keeps its real\ndatabase. Keys must match ^[a-z][a-z0-9-]*$.\n\nONE NAME RULE (see TemplateDevelopmentComponent.WorkspacePath): a\ncomponent's directory must be its own name, so agents, sync routing,\nand data-plane verbs all address it by one word.",
                              "minProperties": 1,
                              "type": "object",
                              "x-kubernetes-validations": [
                                {
                                  "message": "workspacePath must equal the component name (or \".\" for a single root component)",
                                  "rule": "self.all(k, !has(self[k].workspacePath) || self[k].workspacePath == k || self[k].workspacePath == '.')"
                                }
                              ]
                            },
                            "idleTimeoutSeconds": {
                              "description": "IdleTimeoutSeconds is the maximum period without an authorized data-plane\nrequest before a development instance is deleted. Zero disables the\nlimit. Activity is recorded on the runtime CR by the provider's runtime\ncredential, never by the workload pod or caller.",
                              "format": "int64",
                              "maximum": 604800,
                              "minimum": 0,
                              "type": "integer"
                            },
                            "maxLifetimeSeconds": {
                              "description": "MaxLifetimeSeconds is the hard wall-clock lifetime for a development\ninstance. Zero disables the limit; platform sandbox templates set a\nfinite value so abandoned runs are deleted by the Instance controller\nand their runtime resources pass through normal finalizer cleanup.",
                              "format": "int64",
                              "maximum": 604800,
                              "minimum": 0,
                              "type": "integer"
                            },
                            "providerActions": {
                              "description": "ProviderActions controls whether the development pod receives the\nshort-lived setup token used by the optional Provider Actions bridge.\nIt defaults to true for backwards compatibility with existing\ndevelopment templates. A coding-only sandbox should set it to false so\nnone of its containers receive a projected ServiceAccount token.",
                              "type": "boolean"
                            },
                            "scaffold": {
                              "description": "Scaffold optionally names starter code for a fresh project built on\nthis template. Its layout MUST match the components' workspacePaths and\nit SHOULD ship CI workflows that build each component's production\nimage, with the owned workflow declared by Build (see\ndocs/app-studio-template-sandboxes.md \u00a74.1a). Consumed by App Studio at\nproject bootstrap; opaque to the infrastructure provider.",
                              "properties": {
                                "ref": {
                                  "description": "Ref pins a branch or tag. Empty means the repository default branch.",
                                  "maxLength": 128,
                                  "type": "string"
                                },
                                "repository": {
                                  "description": "Repository is the git URL of the scaffold.",
                                  "maxLength": 2048,
                                  "type": "string"
                                }
                              },
                              "required": [
                                "repository"
                              ],
                              "type": "object"
                            }
                          },
                          "required": [
                            "components"
                          ],
                          "type": "object"
                        },
                        "displayName": {
                          "description": "DisplayName is the human-readable name surfaced in the portal\ncatalog. Empty falls back to metadata.name.",
                          "maxLength": 128,
                          "type": "string"
                        },
                        "exposure": {
                          "default": "internal",
                          "description": "Exposure declares whether instances of this template are reachable from\noutside the platform. It is a statement ABOUT the resource graph, not a\nswitch that changes it \u2014 the graph still has to carry (or not carry) the\nHTTPRoute. Declaring it lets every caller stop guessing: the portal and\nthe MCP tools can say \"this has no URL\" instead of surfacing an empty\nstatus field, and an agent stops polling status.url forever for an\ninstance that will never have one.\n\nThe API server defaults it to \"internal\", which is the safe reading: a\ntemplate that never said it publishes anything is assumed not to. Because\nthe default is stamped at admission, readers see a concrete value and\nnever need to interpret an empty field.",
                          "enum": [
                            "internal",
                            "optional",
                            "public"
                          ],
                          "type": "string"
                        },
                        "iconURL": {
                          "description": "IconURL is an optional asset URL the portal shows on catalog\ncards. Falls back to a generic icon when empty.",
                          "maxLength": 2048,
                          "type": "string"
                        },
                        "instanceCRD": {
                          "description": "InstanceCRD declares the per-template CRD the platform\npublishes for tenants to author instances against. Must be in\ngroup infrastructure.faros.sh; the resource (lowercase\nplural) and kind (CamelCase singular) are operator-chosen but\nmust be unique across all Templates.",
                          "properties": {
                            "group": {
                              "description": "Group MUST be infrastructure.faros.sh. Pinned here so\nevery per-template CRD lives under the same namespace and the\nportal can render them uniformly.",
                              "pattern": "^infrastructure\\.faros\\.sh$",
                              "type": "string"
                            },
                            "kind": {
                              "description": "Kind is the CamelCase singular tenants use in apiVersion + kind.",
                              "maxLength": 64,
                              "pattern": "^[A-Z][A-Za-z0-9]*$",
                              "type": "string"
                            },
                            "resource": {
                              "description": "Resource is the lowercase plural the apiserver routes on\n(kubectl get <resource>). Must be unique across all Templates\nin the provider workspace.",
                              "maxLength": 64,
                              "pattern": "^[a-z][a-z0-9]*$",
                              "type": "string"
                            },
                            "version": {
                              "description": "Version of the per-template CRD's served + storage schema.\nTemplates can ship multiple Versions (a future Template can\nextend a previous one's set); the controller updates the CRD's\nspec.versions list rather than overwriting on conflict.",
                              "pattern": "^v[0-9]+((alpha|beta)[0-9]+)?$",
                              "type": "string"
                            }
                          },
                          "required": [
                            "group",
                            "kind",
                            "resource",
                            "version"
                          ],
                          "type": "object"
                        },
                        "sampleValues": {
                          "description": "SampleValues is an optional example input payload the portal pre-fills\nthe provision form with, so a user can provision a working instance in\none click and tweak from there. Keyed by the schema's top-level property\nnames (nested objects allowed). Opaque to the controller; surfaced to the\nportal as spec.sampleValues. Stored as raw JSON.",
                          "type": "object",
                          "x-kubernetes-preserve-unknown-fields": true
                        },
                        "schema": {
                          "description": "Schema is the JSON Schema applied to the per-template CRD's\nspec field. Stored as raw JSON because importing\napiextensions/v1.JSONSchemaProps directly trips controller-gen\non the upstream type's recursive shape; the Template controller\nparses this back into JSONSchemaProps when it builds the CRD's\nspec.versions[].schema.openAPIV3Schema.properties.spec.\n\nExpected content is the standard subset of OpenAPI v3 (type,\nproperties, required, enum, default, description, minimum,\nmaximum, pattern). The controller rejects Templates whose\nSchema fails to parse.",
                          "type": "object",
                          "x-kubernetes-preserve-unknown-fields": true
                        },
                        "version": {
                          "description": "Version pins the Template definition's revision. Required by\nthe per-template CRD's served version selection and by\ninstance-create-time consistency checks.",
                          "maxLength": 64,
                          "pattern": "^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$",
                          "type": "string"
                        },
                        "view": {
                          "description": "View is optional presentation metadata that tells the portal how to render\nthis template's instances \u2014 extra columns in the instance-list table and\ngrouped, typed fields on the instance detail page \u2014 instead of the default\nraw-JSON dump. Authored by the template owner so each template controls its\nown UX. Field values are dot-paths or ${\u2026}-interpolated strings resolved\nagainst the instance's spec/status/meta (see the portal's view resolver).\nStored as raw JSON (preserve-unknown-fields) and surfaced to the portal as\nspec.view; opaque to the controller. Shape:\n\n\tcolumns:                         # extra instance-list columns\n\t  - header: Endpoint\n\t    value: \"https://${spec.expose.fqdn}\"\n\t    type: link                   # text | link | badge | code\n\tdetail:                          # detail-page field groups\n\t  - title: Access\n\t    fields:\n\t      - label: URL\n\t        value: \"https://${status.url}\"\n\t        type: link\n\t      - label: Region\n\t        path: spec.region",
                          "type": "object",
                          "x-kubernetes-preserve-unknown-fields": true
                        }
                      },
                      "required": [
                        "backend",
                        "instanceCRD",
                        "schema",
                        "version"
                      ],
                      "type": "object"
                    },
                    "status": {
                      "description": "TemplateStatus is the observed state.",
                      "properties": {
                        "backend": {
                          "description": "Backend reflects what the backend reported from its\nSetupTemplate call. Empty until first reconcile.",
                          "properties": {
                            "message": {
                              "description": "Message carries human-readable detail when Ready is false.",
                              "maxLength": 2048,
                              "type": "string"
                            },
                            "name": {
                              "description": "Name echoes spec.backend so consumers don't have to cross-\nreference. Helpful if a Template's backend changes mid-life.",
                              "type": "string"
                            },
                            "ready": {
                              "description": "Ready is the backend's headline status; matches BackendTemplateStatus.Ready\nfrom the Go interface.",
                              "type": "boolean"
                            }
                          },
                          "type": "object"
                        },
                        "conditions": {
                          "description": "Conditions follows the standard Kubernetes conditions pattern.\nThe aggregate Ready condition is True iff schema validation and\nthe backend both succeed.",
                          "items": {
                            "description": "Condition contains details for one aspect of the current state of this API Resource.",
                            "properties": {
                              "lastTransitionTime": {
                                "description": "lastTransitionTime is the last time the condition transitioned from one status to another.\nThis should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.",
                                "format": "date-time",
                                "type": "string"
                              },
                              "message": {
                                "description": "message is a human readable message indicating details about the transition.\nThis may be an empty string.",
                                "maxLength": 32768,
                                "type": "string"
                              },
                              "observedGeneration": {
                                "description": "observedGeneration represents the .metadata.generation that the condition was set based upon.\nFor instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date\nwith respect to the current state of the instance.",
                                "format": "int64",
                                "minimum": 0,
                                "type": "integer"
                              },
                              "reason": {
                                "description": "reason contains a programmatic identifier indicating the reason for the condition's last transition.\nProducers of specific condition types may define expected values and meanings for this field,\nand whether the values are considered a guaranteed API.\nThe value should be a CamelCase string.\nThis field may not be empty.",
                                "maxLength": 1024,
                                "minLength": 1,
                                "pattern": "^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$",
                                "type": "string"
                              },
                              "status": {
                                "description": "status of the condition, one of True, False, Unknown.",
                                "enum": [
                                  "True",
                                  "False",
                                  "Unknown"
                                ],
                                "type": "string"
                              },
                              "type": {
                                "description": "type of condition in CamelCase or in foo.example.com/CamelCase.",
                                "maxLength": 316,
                                "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$",
                                "type": "string"
                              }
                            },
                            "required": [
                              "lastTransitionTime",
                              "message",
                              "reason",
                              "status",
                              "type"
                            ],
                            "type": "object"
                          },
                          "type": "array",
                          "x-kubernetes-list-map-keys": [
                            "type"
                          ],
                          "x-kubernetes-list-type": "map"
                        },
                        "observedGeneration": {
                          "description": "ObservedGeneration mirrors metadata.generation last reconciled.\nDrives the standard \"is the status fresh?\" check.",
                          "format": "int64",
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    }
                  },
                  "required": [
                    "spec"
                  ],
                  "type": "object"
                }
              },
              "served": true,
              "storage": true,
              "subresources": {
                "status": {}
              }
            }
          ]
        }
      }
    }
  ]
}
