fix(runtime): kimi as first-class BYO-compute runtime (SOP)
Some checks failed
CI / Detect changes (pull_request) Successful in 1m41s
E2E API Smoke Test / detect-changes (pull_request) Successful in 1m43s
Harness Replays / detect-changes (pull_request) Successful in 25s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 56s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 25s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 53s
gate-check-v3 / gate-check (pull_request) Successful in 24s
qa-review / approved (pull_request) Failing after 20s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 45s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: 7
security-review / approved (pull_request) Failing after 16s
sop-checklist-gate / gate (pull_request) Successful in 17s
sop-tier-check / tier-check (pull_request) Successful in 17s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 56s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 15s
Harness Replays / Harness Replays (pull_request) Successful in 7s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Failing after 4m30s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m26s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 2m57s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 3m58s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 4m35s
Block internal-flavored paths / Block forbidden paths (pull_request) Failing after 10m47s
CI / Python Lint & Test (pull_request) Failing after 7m17s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m0s
CI / Platform (Go) (pull_request) Failing after 9m10s
CI / Canvas (Next.js) (pull_request) Failing after 9m50s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Failing after 13m36s
Some checks failed
CI / Detect changes (pull_request) Successful in 1m41s
E2E API Smoke Test / detect-changes (pull_request) Successful in 1m43s
Harness Replays / detect-changes (pull_request) Successful in 25s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 56s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 25s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 53s
gate-check-v3 / gate-check (pull_request) Successful in 24s
qa-review / approved (pull_request) Failing after 20s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 45s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: 7
security-review / approved (pull_request) Failing after 16s
sop-checklist-gate / gate (pull_request) Successful in 17s
sop-tier-check / tier-check (pull_request) Successful in 17s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 56s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 15s
Harness Replays / Harness Replays (pull_request) Successful in 7s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Failing after 4m30s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m26s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 2m57s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 3m58s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 4m35s
Block internal-flavored paths / Block forbidden paths (pull_request) Failing after 10m47s
CI / Python Lint & Test (pull_request) Failing after 7m17s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m0s
CI / Platform (Go) (pull_request) Failing after 9m10s
CI / Canvas (Next.js) (pull_request) Failing after 9m50s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Failing after 13m36s
Follows the same pattern as 'external' — no template repo, injected into the runtime allowlist as a meta-runtime. Changes: Backend: - workspace.go: use isExternalLikeRuntime() instead of hardcoded 'external' check so runtime=kimi/kimi-cli workspaces take the BYO-compute path - Preserve the caller's runtime label (kimi/kimi-cli/external) in DB so the canvas shows the correct runtime name Frontend: - Add canvas/src/lib/externalRuntimes.ts utility (mirrors backend isExternalLikeRuntime) — single source of truth for BYO-compute detection - Update all hardcoded 'runtime === external' checks to use the utility: FilesTab, TerminalTab, ConfigTab, WorkspaceNode, mobile/components - Add 'kimi' and 'kimi-cli' to RUNTIME_NAMES display map - CreateWorkspaceDialog: external-runtime selector dropdown so operators can pick Generic External / Kimi CLI / Kimi CLI (alt) Tests: - Go tests pass (registry, restart, plugin install, workspace create)
This commit is contained in:
parent
ed41164a3e
commit
7383620be3
@ -80,6 +80,7 @@ export function CreateWorkspaceButton() {
|
||||
// isExternal is true the template / model / hermes-provider fields are
|
||||
// hidden (they're meaningless for BYO-compute agents).
|
||||
const [isExternal, setIsExternal] = useState(false);
|
||||
const [externalRuntime, setExternalRuntime] = useState("external");
|
||||
const [externalConnection, setExternalConnection] =
|
||||
useState<ExternalConnectionInfo | null>(null);
|
||||
|
||||
@ -223,6 +224,7 @@ export function CreateWorkspaceButton() {
|
||||
setBudgetLimit("");
|
||||
setError(null);
|
||||
setHermesProvider("anthropic");
|
||||
setExternalRuntime("external");
|
||||
setHermesApiKey("");
|
||||
setHermesModel("");
|
||||
api
|
||||
@ -282,7 +284,7 @@ export function CreateWorkspaceButton() {
|
||||
// Runtime=external flips the backend into awaiting-agent mode:
|
||||
// no container provisioning, token minted, connection payload
|
||||
// returned in the response for the modal below.
|
||||
...(isExternal ? { runtime: "external" } : {}),
|
||||
...(isExternal ? { runtime: externalRuntime } : {}),
|
||||
...(!isExternal && isHermes && provider
|
||||
? {
|
||||
secrets: { [provider.envVar]: hermesApiKey.trim() },
|
||||
@ -382,6 +384,23 @@ export function CreateWorkspaceButton() {
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{isExternal && (
|
||||
<div>
|
||||
<label className="text-[11px] text-ink-mid block mb-1">
|
||||
External Runtime
|
||||
</label>
|
||||
<select
|
||||
value={externalRuntime}
|
||||
onChange={(e) => setExternalRuntime(e.target.value)}
|
||||
className="w-full bg-surface-card/60 border border-line/50 rounded-lg px-3 py-2 text-sm text-ink focus:outline-none focus:border-accent/60 focus:ring-1 focus:ring-accent/20 transition-colors"
|
||||
>
|
||||
<option value="external">Generic External</option>
|
||||
<option value="kimi">Kimi CLI</option>
|
||||
<option value="kimi-cli">Kimi CLI (alt)</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isExternal && (
|
||||
<InputField
|
||||
label="Template"
|
||||
|
||||
@ -9,6 +9,7 @@ import { Tooltip } from "@/components/Tooltip";
|
||||
import { STATUS_CONFIG, TIER_CONFIG } from "@/lib/design-tokens";
|
||||
import { useOrgDeployState } from "@/components/canvas/useOrgDeployState";
|
||||
import { OrgCancelButton } from "@/components/canvas/OrgCancelButton";
|
||||
import { isExternalLikeRuntime } from "@/lib/externalRuntimes";
|
||||
|
||||
/** Descendant count for the "N sub" badge — children are first-class nodes
|
||||
* rendered as full cards inside this one via React Flow's native parentId,
|
||||
@ -248,7 +249,7 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
|
||||
if (!runtime) return null;
|
||||
return (
|
||||
<div className="mb-1 flex items-center gap-1">
|
||||
{runtime === "external" ? (
|
||||
{isExternalLikeRuntime(runtime) ? (
|
||||
<span
|
||||
className="text-[7px] font-mono px-1.5 py-0.5 rounded-md text-white bg-violet-600 border border-violet-700"
|
||||
title="Phase 30 remote agent — runs outside this platform's Docker network. Lifecycle managed via heartbeat-based polling, not Docker exec."
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
usePalette,
|
||||
} from "./palette";
|
||||
import { Icons, StatusDot, TierChip } from "./primitives";
|
||||
import { isExternalLikeRuntime } from "@/lib/externalRuntimes";
|
||||
|
||||
// Derived view-model the mobile screens consume. Built once per render
|
||||
// from the store's Node<WorkspaceNodeData>.
|
||||
@ -37,7 +38,7 @@ export interface MobileAgent {
|
||||
export function toMobileAgent(node: Node<WorkspaceNodeData>): MobileAgent {
|
||||
const cap = summarizeWorkspaceCapabilities(node.data);
|
||||
const runtime = cap.runtime ?? "unknown";
|
||||
const remote = runtime === "external";
|
||||
const remote = isExternalLikeRuntime(runtime);
|
||||
return {
|
||||
id: node.id,
|
||||
name: node.data.name || node.id,
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
findProviderForModel,
|
||||
type SelectorValue,
|
||||
} from "../ProviderModelSelector";
|
||||
import { isExternalLikeRuntime } from "@/lib/externalRuntimes";
|
||||
|
||||
interface Props {
|
||||
workspaceId: string;
|
||||
@ -175,7 +176,7 @@ function deriveProvidersFromModels(models: ModelSpec[]): string[] {
|
||||
// exactly the point of the platform adaptor. The deep `~/.hermes/
|
||||
// config.yaml` on the container is a separate runtime-internal file,
|
||||
// not this one.
|
||||
const RUNTIMES_WITH_OWN_CONFIG = new Set<string>(["external"]);
|
||||
const RUNTIMES_WITH_OWN_CONFIG = new Set<string>(["external", "kimi", "kimi-cli"]);
|
||||
|
||||
const FALLBACK_RUNTIME_OPTIONS: RuntimeOption[] = [
|
||||
{ value: "", label: "LangGraph (default)", models: [], providers: [] },
|
||||
@ -1003,7 +1004,7 @@ export function ConfigTab({ workspaceId }: Props) {
|
||||
: "This runtime manages its own config outside the platform template."}
|
||||
</div>
|
||||
)}
|
||||
{!error && config.runtime === "external" && (
|
||||
{!error && isExternalLikeRuntime(config.runtime) && (
|
||||
<ExternalConnectionSection workspaceId={workspaceId} />
|
||||
)}
|
||||
{success && (
|
||||
|
||||
@ -9,6 +9,7 @@ import { FileEditor } from "./FilesTab/FileEditor";
|
||||
import { NotAvailablePanel } from "./FilesTab/NotAvailablePanel";
|
||||
import { useFilesApi } from "./FilesTab/useFilesApi";
|
||||
import { buildTree } from "./FilesTab/tree";
|
||||
import { isExternalLikeRuntime } from "@/lib/externalRuntimes";
|
||||
|
||||
// Re-exports preserved for external imports (e.g. tests importing from `../tabs/FilesTab`)
|
||||
export { buildTree } from "./FilesTab/tree";
|
||||
@ -32,7 +33,8 @@ interface Props {
|
||||
* has no platform-owned filesystem. Otherwise the user loses access to
|
||||
* a real surface (e.g. claude-code SaaS workspaces have files served
|
||||
* by ListFiles via EIC; they belong on the rendering path, not here). */
|
||||
const RUNTIMES_WITHOUT_FILES = new Set(["external"]);
|
||||
|
||||
const RUNTIMES_WITHOUT_FILES = new Set(["external", "kimi", "kimi-cli"]);
|
||||
|
||||
export function FilesTab({ workspaceId, data }: Props) {
|
||||
// Early-return for runtimes whose filesystem is not platform-owned.
|
||||
@ -43,7 +45,7 @@ export function FilesTab({ workspaceId, data }: Props) {
|
||||
// "0 files / No config files yet" reads as a bug. The placeholder
|
||||
// makes the absence intentional and points the user at the right
|
||||
// surface (Chat).
|
||||
if (data && RUNTIMES_WITHOUT_FILES.has(data.runtime)) {
|
||||
if (data && isExternalLikeRuntime(data.runtime)) {
|
||||
return <NotAvailablePanel runtime={data.runtime} />;
|
||||
}
|
||||
return <PlatformOwnedFilesTab workspaceId={workspaceId} />;
|
||||
|
||||
@ -13,6 +13,7 @@ interface Props {
|
||||
}
|
||||
|
||||
import { deriveWsBaseUrl } from "@/lib/ws-url";
|
||||
import { isExternalLikeRuntime } from "@/lib/externalRuntimes";
|
||||
|
||||
const WS_URL = deriveWsBaseUrl();
|
||||
|
||||
@ -87,7 +88,7 @@ function NotAvailablePanel({ runtime }: { runtime: string }) {
|
||||
/** Runtimes that don't expose a TTY. Keep narrow — only add a runtime
|
||||
* here when its provisioner genuinely has no shell endpoint, otherwise
|
||||
* the user loses access to a real debugging surface. */
|
||||
const RUNTIMES_WITHOUT_TERMINAL = new Set(["external"]);
|
||||
const RUNTIMES_WITHOUT_TERMINAL = new Set(["external", "kimi", "kimi-cli"]);
|
||||
|
||||
export function TerminalTab({ workspaceId, data }: Props) {
|
||||
// Early-return for runtimes that have no shell. Skips the entire
|
||||
@ -96,7 +97,7 @@ export function TerminalTab({ workspaceId, data }: Props) {
|
||||
// workspace-server (no /ws/terminal/<id> route registered for it),
|
||||
// and shows "Connection failed" with a Reconnect button — confusing
|
||||
// because the workspace IS healthy, just doesn't have a TTY.
|
||||
if (data && RUNTIMES_WITHOUT_TERMINAL.has(data.runtime)) {
|
||||
if (data && isExternalLikeRuntime(data.runtime)) {
|
||||
return <NotAvailablePanel runtime={data.runtime} />;
|
||||
}
|
||||
|
||||
|
||||
21
canvas/src/lib/externalRuntimes.ts
Normal file
21
canvas/src/lib/externalRuntimes.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* External-like (BYO-compute) runtime detection.
|
||||
*
|
||||
* Mirrors the backend's isExternalLikeRuntime() in
|
||||
* workspace-server/internal/handlers/runtime_registry.go.
|
||||
*
|
||||
* These runtimes have no platform-owned container — the operator installs
|
||||
* the agent CLI locally and calls /registry/register. They share UX
|
||||
* behaviour: no Files tab, no Terminal tab, no Docker config, and the
|
||||
* connection modal shows copy-paste snippets.
|
||||
*/
|
||||
|
||||
const EXTERNAL_LIKE_RUNTIMES = new Set([
|
||||
"external",
|
||||
"kimi",
|
||||
"kimi-cli",
|
||||
]);
|
||||
|
||||
export function isExternalLikeRuntime(runtime: string | undefined): boolean {
|
||||
return !!runtime && EXTERNAL_LIKE_RUNTIMES.has(runtime);
|
||||
}
|
||||
@ -9,6 +9,8 @@ const RUNTIME_NAMES: Record<string, string> = {
|
||||
openclaw: "OpenClaw",
|
||||
crewai: "CrewAI",
|
||||
autogen: "AutoGen",
|
||||
kimi: "Kimi",
|
||||
"kimi-cli": "Kimi CLI",
|
||||
};
|
||||
|
||||
export function runtimeDisplayName(runtime: string): string {
|
||||
|
||||
@ -428,13 +428,20 @@ func (h *WorkspaceHandler) Create(c *gin.Context) {
|
||||
// implies docker work in flight) so the canvas can render
|
||||
// a "waiting for external agent to connect" state without
|
||||
// tripping the provisioning-timeout UX.
|
||||
if payload.External || payload.Runtime == "external" {
|
||||
if payload.External || isExternalLikeRuntime(payload.Runtime) {
|
||||
var connectionToken string
|
||||
if payload.URL != "" {
|
||||
// URL already validated by validateAgentURL above (before BeginTx).
|
||||
// Now persist it: the external URL is set after the workspace row
|
||||
// commits so that a failed URL UPDATE doesn't roll back the row.
|
||||
db.DB.ExecContext(ctx, `UPDATE workspaces SET url = $1, status = $2, runtime = 'external', updated_at = now() WHERE id = $3`, payload.URL, models.StatusOnline, id)
|
||||
// Preserve BYO-compute runtime label (kimi, kimi-cli, external) —
|
||||
// don't coerce to generic "external" so the canvas can show the
|
||||
// correct runtime name in the node card.
|
||||
rt := payload.Runtime
|
||||
if rt == "" {
|
||||
rt = "external"
|
||||
}
|
||||
db.DB.ExecContext(ctx, `UPDATE workspaces SET url = $1, status = $2, runtime = $3, updated_at = now() WHERE id = $4`, payload.URL, models.StatusOnline, rt, id)
|
||||
if err := db.CacheURL(ctx, id, payload.URL); err != nil {
|
||||
log.Printf("External workspace: failed to cache URL for %s: %v", id, err)
|
||||
}
|
||||
@ -446,7 +453,12 @@ func (h *WorkspaceHandler) Create(c *gin.Context) {
|
||||
// in awaiting_agent. First POST /registry/register call
|
||||
// from the external agent (with this token + its URL)
|
||||
// flips the row to online.
|
||||
db.DB.ExecContext(ctx, `UPDATE workspaces SET status = $1, runtime = 'external', updated_at = now() WHERE id = $2`, models.StatusAwaitingAgent, id)
|
||||
// Preserve BYO-compute runtime label (kimi, kimi-cli, external).
|
||||
rt := payload.Runtime
|
||||
if rt == "" {
|
||||
rt = "external"
|
||||
}
|
||||
db.DB.ExecContext(ctx, `UPDATE workspaces SET status = $1, runtime = $2, updated_at = now() WHERE id = $3`, models.StatusAwaitingAgent, rt, id)
|
||||
tok, tokErr := wsauth.IssueToken(ctx, db.DB, id)
|
||||
if tokErr != nil {
|
||||
log.Printf("External workspace %s: token issuance failed: %v", id, tokErr)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user