# Launchable OCI Workload Profile Contract v1

As of: April 14, 2026

## Purpose

Define the first implementable contract for launchable OCI workload images and
composition profiles.

This document turns the product direction in
`doc/product/Launchable_OCI_Workload_Images_v1.md` into a control-plane contract
that can drive:

1. catalog display,
2. generated launch forms,
3. launch-on-existing-allocation validation,
4. execution-engine adapter rendering,
5. lifecycle/status reporting,
6. future JupyterLab, vLLM, and composed AI service stacks.

## Decision Summary

Launchable OCI workloads should be modeled as a distinct app class:

```text
raw allocation -> optional managed runtime bundle -> launchable workload profile
```

The first slice should:

1. use OCI images from the platform registry as the artifact source,
2. start with launch-on-existing-allocation,
3. store a typed workload profile manifest on the app version,
4. render UI fields from JSON Schema plus UI hints,
5. produce a typed launch values object,
6. hand the values object to an execution-engine adapter,
7. keep Docker Compose, Helm, Kubernetes, dstack, and node-agent execution behind adapter boundaries.

This is the app-model lane that follows Slurm and RKE2, but it is narrower than
the full Helm/CUE/dstack meta-language problem. It defines the product-facing
profile contract first.

## Relationship To Existing App Model

The profile should reuse existing app control-plane objects where possible.

Current mapping:

1. `app_catalog`
   - app family and product card
2. `app_versions.manifest`
   - versioned workload profile manifest
3. app artifact records
   - OCI digest, promotion, trust, and provenance metadata
4. app instance
   - running workload instance and lifecycle record
5. app members or runtime state
   - adapter-reported placement, endpoints, health, and events

Do not create a separate launchable-workload ownership system for v1. The
workload is an app instance with a stricter manifest shape and a different
runtime adapter.

## Profile Manifest Shape

The app version manifest should reserve this shape:

Canonical schema file:

- `doc/architecture/schemas/launchable_oci_workload_profile.v1.schema.json`

```yaml
profile:
  kind: gpuaas.launchable_oci_workload
  schema_version: v1
  slug: jupyterlab
  display_name: JupyterLab
  description: Browser notebook workspace on an existing allocation.
  support_level: platform_curated
  launch_mode: existing_allocation

artifacts:
  primary_image:
    source: platform_registry
    artifact_name: runtime-cpu
    digest_required: true
    media_type: application/vnd.oci.image.manifest.v1+json
  nvidia_h200_image:
    source: platform_registry
    artifact_name: runtime-nvidia-h200
    digest_required: true
    media_type: application/vnd.oci.image.manifest.v1+json
  amd_rocm_image:
    source: platform_registry
    artifact_name: runtime-amd-rocm
    digest_required: true
    media_type: application/vnd.oci.image.manifest.v1+json

parameters:
  schema:
    type: object
    required: [workspace_mount, exposure_mode]
    properties:
      workspace_mount:
        type: string
        enum: [scratch, project_storage]
        default: scratch
      exposure_mode:
        type: string
        enum: [private]
        default: private
      host_port:
        type: integer
        minimum: 1024
        maximum: 65535
        default: 8888
      gpu_count:
        type: integer
        minimum: 0
        default: 1
      cpu_cores:
        type: integer
        minimum: 1
        default: 2
      memory_gib:
        type: integer
        minimum: 1
        default: 4
      python_packages:
        type: string
        default: ""
        maxLength: 4096
  ui:
    order: [workspace_mount, exposure_mode, host_port, gpu_count, cpu_cores, memory_gib, python_packages]
    groups:
      - id: workspace
        title: Workspace
        fields: [workspace_mount]
      - id: access
        title: Access
        fields: [exposure_mode, host_port]
      - id: resources
        title: Resources
        fields: [gpu_count, cpu_cores, memory_gib]
      - id: packages
        title: Packages
        fields: [python_packages]
    launch_wizard:
      steps:
        - id: target
          title: Target
          description: Choose the active allocation that will host this workload.
          components:
            - kind: platform.target_allocation
              required: true
        - id: runtime
          title: Runtime
          description: Choose the digest-pinned OCI runtime image and runtime inputs.
          components:
            - kind: platform.artifact_selector
              required: true
            - kind: field
              name: python_packages
              required: false
        - id: resources
          title: Resources
          description: Size the workload for the selected allocation.
          components:
            - kind: field
              name: gpu_count
              required: true
            - kind: field
              name: cpu_cores
              required: true
            - kind: field
              name: memory_gib
              required: true
        - id: storage
          title: Storage
          components:
            - kind: field
              name: workspace_mount
              required: true
        - id: access
          title: Access
          components:
            - kind: field
              name: exposure_mode
              required: true
            - kind: field
              name: host_port
              required: true
        - id: review
          title: Review
          components:
            - kind: platform.review
              required: true

resources:
  gpu:
    min_count: 0
    default_count: 1
    placement: allocation_local
  cpu:
    min_cores: 2
  memory:
    min_gib: 4

storage:
  mounts:
    - name: workspace
      path: /workspace
      required: true
      modes: [scratch, project_storage]

network:
  endpoints:
    - name: web
      port: 8888
      type: http
      auth_pattern: header_injected_jwt
      managed_ingress:
        enabled: true
        route_mode: host
        default_open_path: /lab
        websocket_required: true

execution:
  default_engine: node_agent
  container_env:
    HOME: /workspace
    JUPYTER_RUNTIME_DIR: /workspace/.jupyter/runtime
    JUPYTER_CONFIG_DIR: /workspace/.jupyter/config
    JUPYTER_DATA_DIR: /workspace/.jupyter/data
    JUPYTER_PREFER_ENV_PATH: "0"
    JUPYTERLAB_SETTINGS_DIR: /workspace/.jupyter/lab/user-settings
    JUPYTERLAB_WORKSPACES_DIR: /workspace/.jupyter/lab/workspaces
  supported_engines:
    - engine: node_agent
      adapter: oci_container
      status: active
    - engine: docker_compose
      adapter: compose_v2
      status: future
    - engine: helm
      adapter: helm_values
      status: future

outputs:
  endpoints:
    - name: web
      source: network.endpoints.web
  commands:
    - name: inspect
      command: gpuaas apps instances describe

validation:
  readiness:
    - name: web_http_ready
      type: http
      endpoint: web
      path: /
  smoke:
    - name: container_running
      type: adapter_status
      status: running
```

This is the target shape, not a claim that the current API validates every field
today.

## Endpoint Exposure Contract

This section is the launchable OCI v1.1 compatibility bridge. The shipping v1
stored manifests and seed data may still contain `protocol`, `exposure_modes`,
and `proxy` fields; new manifests should declare the v1.1 endpoint shape below.
The platform must accept both during the migration window and normalize them to
one route-intent model before rendering Pomerium or any future edge runtime.

Launchable OCI endpoint declarations use three product-level concepts:

1. `type` is the closed endpoint type enum: `http`, `tcp`, `ssh`,
   `kubernetes`, `mcp`, or `job_submission`.
2. `auth_pattern` is the closed two-tier authentication enum:
   `oidc_native`, `header_injected_jwt`, `per_connection_credential`,
   `mtls_user_cert`, or `per_user_instance`.
3. `managed_ingress` is the product-facing exposure primitive for endpoints
   that should be reachable through the platform edge.

`managed_ingress` means "create GPUaaS route intent for this endpoint." It does
not mean "create a Pomerium route" in the manifest. Pomerium is the first
renderer for route intent, and a future Envoy, Caddy, or Gateway API renderer
must be able to consume the same manifest without changing app developer
fields.

The v1 schema keeps `protocol`, `exposure_modes`, and `proxy` as compatibility
fields while existing JupyterLab/vLLM profiles and the path-prefix proxy are
being retired. During this window:

- `protocol: http` or `protocol: tcp` maps to `type: http` or `type: tcp` when
  `type` is absent in a legacy stored manifest.
- `exposure_modes: [platform_proxy]` maps to
  `managed_ingress.enabled: true` with `route_mode: platform_path_compat`.
- `proxy.default_open_path` and `proxy.websocket_required` map to the same
  fields under `managed_ingress`.
- `proxy.auth_bridge` remains renderer/runtime material and should be replaced
  by `auth_pattern` plus workload-access material before production
  Pomerium behavior expands.

New manifest authors should use `type`, `auth_pattern`, and `managed_ingress`.
Do not add Pomerium annotations, policy syntax, route CRD names, or app-specific
endpoint enum values to the manifest.

`managed_ingress.client_auth_mode` is the route's client-facing auth mode:

- `browser_oidc` for interactive browser tools such as notebooks and dashboards,
- `api_bearer` for SDK/backend API clients such as OpenAI-compatible endpoints
  that send GPUaaS bearer tokens in the `Authorization` header.

`managed_ingress.route_family` is a closed policy and observability enum:

- `browser_app` for interactive app endpoints such as notebooks,
- `api_app` for app-owned API endpoints such as OpenAI-compatible routes,
- `platform_admin` for GPUaaS-owned platform tools,
- `terminal_ws` for terminal/session WebSocket routes.

App manifests normally set `browser_app` or `api_app`. If omitted,
app-runtime derives `api_app` from `client_auth_mode: api_bearer` and
`browser_app` otherwise. `platform_admin` and `terminal_ws` are reserved for
platform-owned route intent and should not be used by app authors.

Environment-specific public host binding is platform configuration, not app
manifest configuration. App-runtime may attach `managed_ingress.public_host` to
the stored route intent from an operator-managed host map such as
`APP_RUNTIME_MANAGED_INGRESS_PUBLIC_HOST_MAP`, keyed by
`<app_slug>.<endpoint_name>`. For example, an environment can bind
`jupyterlab.web` to `jupyter.apps.<env>.aicloud.core42.dev` and
`vllm-openai.openai` to `openai.apps.<env>.aicloud.core42.dev` without putting
those environment hostnames in the launchable profile.

For launchable profiles, `parameters.ui.launch_wizard` is the deploy UX source
of truth. The platform owns the wizard shell, validation gates, and reserved
platform components; the app manifest owns step names, descriptions, order, and
which declared fields appear in each step.

Reserved platform wizard component kinds:

- `platform.target_allocation`: single allocation picker for
  `profile.launch_mode: existing_allocation`; output is
  `placement_intent.target_allocation_id`.
- `platform.artifact_selector`: verified published/promoted OCI artifact picker
  filtered by selected allocation and requested GPU count.
- `platform.access_credential`: project SSH/bootstrap credential picker when a
  profile requires host bootstrap access.
- `platform.operator_service_account`: project service-account picker when a
  profile requires app-owned automation identity.
- `platform.review`: final read-only review of target, artifact digest,
  resource overrides, and config payload.
- `field`: renders a field from `parameters.schema.properties`; `name` must
  reference a declared parameter.

For Docker Compose-backed profiles, the manifest may also carry a
platform-owned compose renderer and logical service topology. This is metadata
and adapter selection, not user-authored Compose YAML:

```yaml
execution:
  default_engine: docker_compose
  compose:
    renderer: vllm_openai
    topology:
      description: Single-node OpenAI-compatible inference endpoint.
      services:
        - name: vllm
          role: openai_server
          endpoints: [openai]
          mounts: [workspace]
```

The control plane remains responsible for rendering the actual Compose file
from the curated renderer, selected digest-pinned artifact, resource overrides,
endpoint policy, and storage mounts.

## Launch Request Values

The UI should not submit backend YAML.

It should submit:

```json
{
  "app_slug": "jupyterlab",
  "app_version": "2026.04-preview",
  "display_name": "My JupyterLab",
  "placement_intent": {
    "target_allocation_id": "uuid"
  },
  "config": {
    "workspace_mount": "project_storage",
    "exposure_mode": "private",
    "host_port": 8888,
    "python_packages": "numpy==2.4.4 pandas>=2.2"
  },
  "resource_overrides": {
    "gpu_count": 0,
    "cpu_cores": 2,
    "memory_gib": 4
  }
}
```

The app instance placement key for this workload class is
`target_allocation_id`. Do not overload Slurm `controller_allocation_id` or RKE2
`server_allocation_id` for allocation-local OCI workloads.

Server-side launch validation should check:

1. the allocation is active,
2. the allocation belongs to the project,
3. the app version is entitled and published,
4. the selected OCI artifact is promoted and not retired,
5. the digest is immutable and trusted according to current policy,
6. workspace/storage choices are allowed by the profile,
7. requested exposure mode is supported by the environment,
8. requested GPU/CPU/memory shape fits the target allocation,
9. the execution adapter is enabled for the environment.

## Execution Engine Adapter Boundary

Every execution engine adapter receives the same high-level input:

1. profile manifest,
2. validated launch values,
3. project and app instance identity,
4. allocation placement,
5. resolved artifact digests and pull credentials,
6. resolved storage mount decisions,
7. resolved endpoint exposure decision.

Every adapter must return the same high-level output:

1. lifecycle state,
2. adapter-specific status summary,
3. endpoints,
4. logs or log references,
5. metrics references where available,
6. events and failure reasons,
7. cleanup/decommission result.

Adapter examples:

1. `node_agent`
   - launch one OCI container or local process on an allocation through typed node-agent tasks
2. `docker_compose`
   - render Compose YAML for allocation-local multi-container execution
3. `kubernetes`
   - render Kubernetes resources directly
4. `helm`
   - render curated Helm values and apply a chart bundle
5. `dstack`
   - render `.dstack.yml` or submit through dstack API

The adapter owns YAML/run configuration. The product contract owns values,
constraints, entitlements, billing attribution, and lifecycle mapping.

## Config-Only Authoring Maturity Gate

The App SDK goal is that a new curated OCI or Compose app can be introduced by
declaring configuration, publishing trusted artifacts, and adding environment
route policy. It should not require app-name-specific launch, routing, billing,
or frontend branches.

The code-server and OpenClaw slices are the first post-Jupyter/vLLM tests of
this rule. They intentionally exercise CPU-first browser apps, managed-ingress
host routing, architecture-specific artifacts, and app-specific runtime
defaults without creating app-specific product routes.

Before a new launchable OCI or Compose app is considered SDK-ready, it must pass
this gate:

1. the app version manifest declares all user inputs through
   `parameters.schema` and `parameters.ui.launch_wizard`;
2. endpoint exposure is declared through `network.endpoints[*].type`,
   `auth_pattern`, and `managed_ingress`, not renderer-specific annotations;
3. artifact selection is resolved by target allocation/family/SKU architecture
   and trusted artifact metadata, not by app slug conditionals;
4. resource requirements are expressed as resource-class-neutral fields
   (`cpu`, `memory`, `gpu`, storage mounts, endpoint roles) rather than
   app-specific launch logic;
5. runtime defaults such as container port, command, working directory,
   readiness probe, and environment variables are manifest or adapter inputs;
6. the generic app-instance create path and the V3 launch path use the same
   resolver and validation rules;
7. missing artifact, missing port, unsupported architecture, unsupported
   resource class, and failed readiness are rejected or surfaced before the app
   is advertised as runnable;
8. the frontend renders the wizard from the manifest contract. Temporary app
   extensions are allowed only as a tracked migration exception and must not
   become the durable app authoring model;
9. Docker Compose-backed profiles declare logical service topology and a
   platform-owned renderer. App developers do not submit raw Compose YAML
   through the browser;
10. smoke evidence includes at least one successful launch and one intentional
    preflight failure caught before runtime, with correlation IDs in the
    resulting evidence.

If an app needs behavior outside this list, update the manifest contract first.
Do not patch a special case into app-runtime, proxy routing, or V3 launch UX.

Generic OCI apps may declare `execution.command_args` when the upstream image
default command is not the runnable service entrypoint for managed ingress. The
field is a validated argv array and remains app-neutral: app-runtime passes it
through to the node-agent OCI adapter without branching on the app slug. Use it
for curated images such as OpenClaw where the image default exposes a command
group and the gateway service must be started explicitly.

## First Slice Recommendation

The first implementable slice should be:

1. one curated `jupyterlab` profile and one single-node `vllm-openai` profile,
2. launch on an existing allocation,
3. scratch workspace first, project storage once allocation storage is ready,
4. private or platform-proxied HTTP endpoint depending on the endpoint work,
5. node-agent OCI container adapter first if container execution is available on the node,
6. otherwise Docker Compose adapter as the first allocation-local backend,
7. status/events surfaced through the existing app instance shell.

`vllm` should be the second curated profile after the profile/adapter contract is
proven with JupyterLab because it introduces model path, GPU placement, tensor
parallelism, and endpoint health concerns earlier.

## Current Implementation Status

As of the April 14, 2026 local and platform-control implementation:

1. the profile schema exists at
   `doc/architecture/schemas/launchable_oci_workload_profile.v1.schema.json`,
2. `jupyterlab` is seeded as an active, entitled, launchable OCI catalog app,
3. the catalog UI can open the launch form only when a verified published or
   promoted matching OCI artifact is available,
4. the catalog UI can fetch app version manifests and preview the workload
   profile, execution engine summary, endpoints, resources, and raw manifest,
5. the general app-instance create path still rejects launch requests without
   the required target allocation, profile config, and digest-pinned artifact,
6. the node-agent task catalog recognizes `workload.oci_runtime_status`,
   `workload.oci_launch`, `workload.oci_control`, `workload.oci_remove`,
   `workload.compose_launch`, `workload.compose_control`, and
   `workload.compose_remove`,
7. `workload.oci_runtime_status` performs a non-mutating approved-runtime probe
   for `docker`, `podman`, and `nerdctl`,
8. node-agent launch/control/remove handlers are implemented as a bounded first
   adapter slice: digest-pinned images only, deterministic platform container
   names, workload-scoped host mounts under the allocation user's home, no raw
   pull credentials in task logs, approved local runtimes only, and bounded
   launch readiness based on container state/health,
9. the app-runtime worker defaults to fail-closed for
   `gpuaas.launchable_oci_workload`; when explicitly enabled with
   `APP_RUNTIME_LAUNCHABLE_OCI_NODE_TASKS_ENABLED=true`, it renders the selected
   artifact/profile/config into a queued `workload.oci_launch` node task and
   marks the app instance `deploying`,
10. while that flag is enabled, app-runtime also polls terminal
   `workload.oci_*` and `workload.compose_*` node
   task results and reconciles app/member state for launch, stop, start,
   restart, and decommission,
11. the app-instance create path reserves `placement_intent.target_allocation_id`
   for allocation-local launchable OCI placement and performs minimal
   manifest-aware value validation before accepting a launch request,
12. profiles with `artifacts.primary_image.digest_required: true` require a
   verified published or promoted app artifact to be selected before the
   instance is accepted,
13. the JupyterLab deploy form exposes GPU count, CPU cores, memory GiB, and a
   private host-local port as launch inputs; the node-agent adapter maps the
   host port to a `127.0.0.1:<host_port>:8888/tcp` Docker publish binding and
   maps CPU and memory overrides to bounded Docker runtime limits,
14. the JupyterLab deploy form accepts an optional Python package list; the
   node-agent adapter validates package specifiers and runs `python -m pip
   install --no-cache-dir ...` inside the container before readiness completes. Pip
   version specifiers are allowed as single tokens, for example
   `numpy==2.4.4`, `pandas>=2.2`, or `torch~=2.9`.
15. curated first-slice JupyterLab runtime images now exist for:
   - `runtime-cpu`,
   - `runtime-nvidia-h200`,
   - `runtime-amd-rocm`,
16. platform-control validation has published and launched the
   `runtime-nvidia-h200` image on an H200 allocation with `gpu_count: 1`; the
   resulting Docker device request is bounded to `Count: 1`, and `nvidia-smi`
   inside the container reports one visible NVIDIA H200,
17. node-agent bootstrap owns the host container-runtime prerequisite for this
   slice: fresh bootstrap/rebootstrap installs Docker when no approved runtime
   is present and auto-configures `nvidia-container-toolkit` when `nvidia-smi`
   is present,
18. the platform-control JupyterLab validation instance reached stable
   `running` state through the public app-instance API after the updated
   control plane and node-agent bootstrap were deployed,
19. `vllm-openai` is seeded as the first active Docker Compose-backed curated
   launchable profile; app-runtime selects the manifest-declared
   `execution.compose.renderer`, emits a manifest-owned topology block in the
   node task, and node-agent returns that topology in runtime output so the UI
   can show the service/endpoint/mount shape without exposing arbitrary Compose
   YAML,
20. platform-control validation launched the `vllm-openai` NVIDIA H200 profile
   with Mistral Small 3.2 on one H200 and validated both `/v1/models` and
   `/v1/chat/completions` through a private SSH tunnel,
21. node-agent lifecycle self-update now exists for agent binary delivery, and
   fresh bootstrap/rebootstrap owns Docker, Docker Compose, NVIDIA Container
   Toolkit, registry trust, and H200 site-bootstrap prerequisites for the
   current slices.

Keep `APP_RUNTIME_LAUNCHABLE_OCI_NODE_TASKS_ENABLED` enabled only in environments
where node-agent has the OCI task handlers and target nodes have an approved OCI
runtime. Local-kind and platform-control currently enable this path for the
validation slice; production promotion still needs explicit runtime/preflight
sign-off.

Adapter constraint observed and addressed during April 2026 validation:

1. some existing worker allocations did not expose `docker`, `podman`, or
   `nerdctl` in the default PATH,
2. Docker was not an active service on those nodes,
3. node-agent bootstrap now owns the first-slice container-runtime prerequisite:
   fresh bootstrap/rebootstrap checks for `docker`, `podman`, or `nerdctl` and
   installs the configured package (`docker.io` by default) when missing,
4. on NVIDIA nodes, node-agent bootstrap auto-detects `nvidia-smi`, installs
   `nvidia-container-toolkit`, runs `nvidia-ctk runtime configure --runtime=docker`,
   and restarts Docker,
5. node-agent bootstrap configures Docker registry host trust and registry login
   from a node-bound bootstrap credential endpoint when registry pull
   credentials are configured,
6. `node_agent` is the first adapter slice; deploy still depends on artifact
   verification and target-node runtime readiness.

This is intentionally a node bootstrap behavior, not an app deploy behavior.
Existing nodes may still require rebootstrap or an infra-owned runtime install
when host prerequisites drift outside the current lifecycle scope. The remaining
platform gap is full fleet reconciliation and read-model telemetry, not the
basic node-agent binary update path.

## Storage Boundary

The profile may define storage requirements, but it must not invent its own
storage product.

For v1:

1. scratch mount is allowed,
2. project persistent storage is a declared dependency on the allocation storage model,
3. arbitrary host path entry is not allowed in the user-facing launch form,
4. Kubernetes PVC generation is blocked until the RKE2 storage/CSI decision is made.

## Endpoint Boundary

Launchable OCI workloads commonly need browser or HTTP access.

For v1:

1. every endpoint must be declared in the profile,
2. direct public exposure must be disabled until infra defines the exposure boundary,
3. platform-proxied exposure should integrate with the workload UI gateway model,
4. endpoint credentials or tokens must be generated by the platform or app adapter, never pasted into raw YAML.

## Security Boundary

The first slice must preserve these rules:

1. deploy by digest, not mutable tag,
2. use platform registry artifacts unless explicitly approved otherwise,
3. use wrapped or short-lived pull credentials,
4. do not expose raw registry credentials to the browser,
5. do not allow arbitrary image names in the user-facing launcher,
6. do not let a profile request host mounts or privileged containers unless the profile is platform-curated and explicitly approved.

## UI Implications

The launch UI should be generated from:

1. profile metadata,
2. JSON Schema parameter schema,
3. UI hints,
4. environment capability checks,
5. entitlement and placement checks.

The UI should show:

1. image/profile name and version,
2. support level,
3. target allocation,
4. workspace/storage mode,
5. endpoint exposure mode,
6. resource summary,
7. cost/resource warnings for optional add-ons,
8. generated endpoints after launch.

The UI should not show:

1. arbitrary Docker Compose YAML,
2. arbitrary Kubernetes YAML,
3. raw registry credentials,
4. raw Helm chart values beyond an advanced/debug view.

## Open Questions

1. Should node-agent lifecycle upgrades automatically deliver host prerequisites
   such as Docker and NVIDIA Container Toolkit to existing nodes, or should this
   remain partially bootstrap/remediation-owned until infra packages own full
   fleet prerequisite reconciliation?
2. Should profile manifests live only inside `app_versions.manifest`, or also as OCI artifacts with annotations?
3. How should profile versions map to image versions when a profile uses multiple images?
4. What is the first approved endpoint exposure mode beyond private host-local
   access in platform-control?
5. Which storage mode is available before project persistent storage ships?
6. Should `jupyterlab` be allocation-scoped only, or also visible as a project workload under `/workloads`?
7. Should user package installs remain ephemeral per container launch, or should
   the next version add derived per-project images and persistent package
   caches?
8. What is the release process for publishing/promoting the curated CPU,
   NVIDIA/CUDA, and AMD/ROCm runtime image family?
9. How should package-install UX represent versions, constraints, failure
   reporting, and eventual environment reuse?

## Next Implementation Steps

Completed local baseline:

1. Add a schema file for the workload profile manifest.
2. Add seed/catalog metadata for an internal `jupyterlab` launchable profile.
3. Expose a read-only manifest/profile preview through the existing app catalog version API and catalog UI.
4. Add the bounded node-agent OCI task handlers for runtime status, launch,
   control, and remove.
5. Add the feature-flagged app-runtime path that queues `workload.oci_launch`
   and reconciles terminal node task results back into app state.
6. Add app-runtime lifecycle mapping for `workload.oci_control` and
   `workload.oci_remove`, including stop/start/restart/decommission reconcile.

Remaining implementation:

1. Use `workload.oci_runtime_status` in a local smoke flow to decide whether the
   target node has an approved reachable runtime before enqueueing launch.
2. Expand the automated local-kind and platform-control smoke validation:
   - `scripts/ops/app_runtime_first_slice_smoke.sh` now provides the first
     observe/deploy/access/decommission harness,
   - CI wiring and stable environment-specific placement/artifact inputs remain
     to be added.
3. Package-install v1 design is captured in
   `doc/product/Jupyter_Package_Install_v1.md`; node-agent now reports a
   bounded pip install excerpt in task output and the workload Events tab
   surfaces it. Remaining follow-up is the derived image workflow design and
   release/channel policy for reusable project environments.
4. Endpoint-access guidance in the workload Access tab covers private SSH tunnel
   access now and states that platform-proxy routing is not wired yet.
5. Convert the Jupyter/vLLM scripts and manifests into a documented
   app-developer release flow covering image variants, schema validation,
   artifact promotion, smoke tests, and decommission cleanup.
