Skip to content

Commit 1c79da4

Browse files
authored
feat: Kubernetes deployment for HyperAgent on AKS and KIND (#54)
* feat: Kubernetes deployment for HyperAgent on AKS and KIND - Add K8s Job manifests with Key Vault and K8s Secret auth - Add hyperagent-k8s CLI with log streaming, file retrieval, input injection - Add --input-dir support via init containers - Add device plugin DaemonSet, K8s Dockerfile, KIND local setup - Add Azure infra scripts (AKS + ACR + KVM node pool) - Fix code validator false positives on prose containing from in strings - Fix Azure CLI Graph API: use --assignee-object-id for RBAC - Use fine-grained PATs (classic PATs silently fail with Copilot SDK) - Use HYPERAGENT_PROMPT env var instead of CLI arg for reliability * fix: address PR review feedback - Fix import detection: use starts_with for from-line matching (no mid-line false positives) - Reject classic PATs (ghp_) early with clear error in both auth scripts - Fix base64url JWT decode for Azure OID extraction (proper padding + urlsafe) - Fix entrypoint.sh: disable set -e around agent run so output collection works on failure - Fix k8s-local-down: use lighter dependency check (no KVM required for teardown) - Fix k8s-infra-down: only require az CLI (not kubectl/envsubst) for teardown - Parameterise namespace in job manifests (uses NAMESPACE from envsubst) - Escape prompt for safe YAML embedding (backslashes, quotes, newlines) Note: edit_handler validation bypass (review comment #7) is a pre-existing issue requiring a larger refactor — tracked separately. * fix: use strip_prefix for clippy compliance in import detection * fix: system prompt, edit_handler validation, table contrast, bare exports - System message: mandatory handler pattern box, edit_handler guidance - edit_handler: validate edited code before applying (closes security bypass) - Validator: handle bare 'export { name }' in .d.ts (fixes getThemeNames) - PPTX tables: always autoTextColor for row text (no overrides) - PDF tables: auto-contrast body text per row, auto-fix poor contrast - Both: dark theme rows get explicit fill for readability on image backgrounds * fix: update tests for auto-contrast table text (CI failures) - Update pptx-readability tests: autoTextColor always wins, no overrides - Regenerate PDF golden baseline for table-styles (contrast-corrected colors)
1 parent e7098c2 commit 1c79da4

31 files changed

+2661
-312
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,4 @@ plugins/shared/*.js
5353
plugins/plugin-schema-types.d.ts
5454
plugins/plugin-schema-types.js
5555
plugins/host-modules.d.ts
56+
output-hyperagent**/**

Justfile

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,3 +326,246 @@ docker-build:
326326
# Run hyperagent in Docker (requires /dev/kvm or /dev/mshv)
327327
docker-run *ARGS:
328328
./scripts/hyperagent-docker {{ARGS}}
329+
330+
# ── Kubernetes Deployment ─────────────────────────────────────────────
331+
332+
# Internal: check common K8s prerequisites
333+
_k8s-check-common:
334+
#!/usr/bin/env bash
335+
source deploy/k8s/common.sh
336+
require_cmd docker "https://docs.docker.com/get-docker/" || exit 1
337+
require_cmd kubectl "https://kubernetes.io/docs/tasks/tools/" || exit 1
338+
339+
# Internal: check Azure prerequisites
340+
_k8s-check-azure:
341+
#!/usr/bin/env bash
342+
source deploy/k8s/common.sh
343+
require_cmd az "https://docs.microsoft.com/en-us/cli/azure/install-azure-cli" || exit 1
344+
require_cmd kubectl "https://kubernetes.io/docs/tasks/tools/" || exit 1
345+
require_cmd envsubst "apt install gettext-base" || exit 1
346+
if ! az account show &>/dev/null; then
347+
log_error "Not logged in to Azure CLI. Run 'az login' first."
348+
exit 1
349+
fi
350+
351+
# Internal: check local (KIND) prerequisites
352+
_k8s-check-local:
353+
#!/usr/bin/env bash
354+
source deploy/k8s/common.sh
355+
require_cmd docker "https://docs.docker.com/get-docker/" || exit 1
356+
require_cmd kind "go install sigs.k8s.io/kind@latest" || exit 1
357+
require_cmd kubectl "https://kubernetes.io/docs/tasks/tools/" || exit 1
358+
if [ ! -e /dev/kvm ]; then
359+
log_error "/dev/kvm not found — Hyperlight requires hardware virtualisation"
360+
exit 1
361+
fi
362+
363+
# ── Local (KIND) ──────────────────────────────────────────────────────
364+
365+
# Create local KIND cluster with /dev/kvm and local registry
366+
k8s-local-up: _k8s-check-local
367+
./deploy/k8s/local/setup.sh
368+
369+
# Tear down local KIND cluster and registry
370+
k8s-local-down: _k8s-check-common
371+
./deploy/k8s/local/teardown.sh
372+
373+
# Build and load image into local KIND cluster
374+
k8s-local-build version="0.0.0-dev": _k8s-check-common
375+
#!/usr/bin/env bash
376+
# Resolve symlinks for Docker COPY
377+
if [ -L deps/js-host-api ]; then
378+
target=$(readlink -f deps/js-host-api)
379+
rm deps/js-host-api
380+
cp -r "$target" deps/js-host-api
381+
trap 'rm -rf deps/js-host-api && ln -sfn "'"$target"'" deps/js-host-api' EXIT
382+
fi
383+
docker build -t hyperagent --build-arg VERSION="{{version}}" .
384+
docker build -f deploy/k8s/Dockerfile -t hyperagent-k8s .
385+
# Push to local registry
386+
docker tag hyperagent-k8s localhost:5000/hyperagent:latest
387+
docker push localhost:5000/hyperagent:latest
388+
389+
# Deploy device plugin to local KIND cluster
390+
k8s-local-deploy-plugin: _k8s-check-common
391+
#!/usr/bin/env bash
392+
source deploy/k8s/common.sh
393+
export IMAGE="ghcr.io/hyperlight-dev/hyperlight-device-plugin:latest" DEVICE_COUNT="2000" DEVICE_UID="65534" DEVICE_GID="65534"
394+
envsubst < deploy/k8s/manifests/device-plugin.yaml | kubectl apply -f -
395+
kubectl apply -f deploy/k8s/manifests/namespace.yaml
396+
echo "Waiting for device plugin pods..."
397+
kubectl rollout status daemonset/hyperlight-device-plugin -n hyperlight-system --timeout=120s
398+
399+
# Run a prompt on local KIND cluster
400+
k8s-local-run +ARGS:
401+
HYPERAGENT_K8S_IMAGE=localhost:5000/hyperagent:latest ./scripts/hyperagent-k8s {{ARGS}}
402+
403+
# ── Azure (AKS) ──────────────────────────────────────────────────────
404+
405+
# Create AKS cluster + ACR + KVM node pool
406+
k8s-infra-up: _k8s-check-azure
407+
./deploy/k8s/azure/setup.sh
408+
409+
# Tear down all Azure resources (only requires az CLI)
410+
k8s-infra-down:
411+
#!/usr/bin/env bash
412+
command -v az >/dev/null 2>&1 || { echo "Azure CLI (az) is required"; exit 1; }
413+
az account show >/dev/null 2>&1 || { echo "Please log in: az login"; exit 1; }
414+
./deploy/k8s/azure/teardown.sh
415+
416+
# Stop AKS cluster (save costs when not in use)
417+
k8s-stop:
418+
#!/usr/bin/env bash
419+
source deploy/k8s/azure/config.env
420+
az aks stop -g "${RESOURCE_GROUP}" -n "${CLUSTER_NAME}"
421+
422+
# Start AKS cluster
423+
k8s-start:
424+
#!/usr/bin/env bash
425+
source deploy/k8s/azure/config.env
426+
az aks start -g "${RESOURCE_GROUP}" -n "${CLUSTER_NAME}"
427+
428+
# Get AKS credentials for kubectl
429+
k8s-credentials:
430+
#!/usr/bin/env bash
431+
source deploy/k8s/azure/config.env
432+
az aks get-credentials -g "${RESOURCE_GROUP}" -n "${CLUSTER_NAME}" --overwrite-existing
433+
434+
# Deploy hyperlight device plugin to cluster
435+
k8s-deploy-plugin: _k8s-check-common
436+
#!/usr/bin/env bash
437+
source deploy/k8s/azure/config.env
438+
export IMAGE="${DEVICE_PLUGIN_IMAGE}" DEVICE_COUNT="${DEVICE_COUNT}" DEVICE_UID="${DEVICE_UID}" DEVICE_GID="${DEVICE_GID}"
439+
envsubst < deploy/k8s/manifests/device-plugin.yaml | kubectl apply -f -
440+
kubectl apply -f deploy/k8s/manifests/namespace.yaml
441+
echo "Waiting for device plugin pods..."
442+
kubectl rollout status daemonset/hyperlight-device-plugin -n hyperlight-system --timeout=120s
443+
444+
# Build HyperAgent K8s image (builds base image first)
445+
k8s-build version="0.0.0-dev": _k8s-check-common
446+
#!/usr/bin/env bash
447+
# Resolve symlinks for Docker COPY
448+
if [ -L deps/js-host-api ]; then
449+
target=$(readlink -f deps/js-host-api)
450+
rm deps/js-host-api
451+
cp -r "$target" deps/js-host-api
452+
trap 'rm -rf deps/js-host-api && ln -sfn "'"$target"'" deps/js-host-api' EXIT
453+
fi
454+
docker build -t hyperagent --build-arg VERSION="{{version}}" .
455+
docker build -f deploy/k8s/Dockerfile -t hyperagent-k8s .
456+
457+
# Push HyperAgent K8s image to ACR
458+
k8s-push: _k8s-check-azure
459+
#!/usr/bin/env bash
460+
source deploy/k8s/azure/config.env
461+
az acr login --name "${ACR_NAME}"
462+
docker tag hyperagent-k8s "${ACR_NAME}.azurecr.io/${HYPERAGENT_IMAGE_NAME}:${HYPERAGENT_IMAGE_TAG}"
463+
docker push "${ACR_NAME}.azurecr.io/${HYPERAGENT_IMAGE_NAME}:${HYPERAGENT_IMAGE_TAG}"
464+
465+
# Set up GitHub authentication (K8s Secret — simple but less secure)
466+
k8s-setup-auth:
467+
./deploy/k8s/setup-auth.sh
468+
469+
# Set up GitHub authentication via Azure Key Vault
470+
k8s-setup-auth-keyvault:
471+
./deploy/k8s/setup-auth-keyvault.sh
472+
473+
# Run a prompt as a K8s Job
474+
k8s-run +ARGS:
475+
./scripts/hyperagent-k8s {{ARGS}}
476+
477+
# Show cluster, device plugin, and job status
478+
k8s-status:
479+
#!/usr/bin/env bash
480+
source deploy/k8s/common.sh
481+
echo ""
482+
log_step "Cluster nodes:"
483+
kubectl get nodes -o custom-columns='NAME:.metadata.name,HYPERVISOR:.metadata.labels.hyperlight\.dev/hypervisor,CAPACITY:.status.allocatable.hyperlight\.dev/hypervisor' 2>/dev/null || echo " (not connected)"
484+
echo ""
485+
log_step "Device plugin:"
486+
kubectl get pods -n hyperlight-system -l app.kubernetes.io/name=hyperlight-device-plugin 2>/dev/null || echo " (not deployed)"
487+
echo ""
488+
log_step "HyperAgent jobs:"
489+
kubectl get jobs -n hyperagent -l hyperagent.dev/type=prompt-job 2>/dev/null || echo " (none)"
490+
echo ""
491+
492+
# Smoke test: verify cluster, device plugin, auth, and image are all working
493+
k8s-smoke-test:
494+
#!/usr/bin/env bash
495+
source deploy/k8s/common.sh
496+
PASS=0
497+
FAIL=0
498+
echo ""
499+
log_step "Running K8s smoke tests..."
500+
echo ""
501+
502+
# 1. kubectl connected?
503+
if kubectl cluster-info &>/dev/null; then
504+
log_success "✅ kubectl connected to cluster"
505+
PASS=$((PASS + 1))
506+
else
507+
log_error "❌ kubectl not connected — run 'just k8s-credentials' or 'just k8s-local-up'"
508+
FAIL=$((FAIL + 1))
509+
fi
510+
511+
# 2. KVM nodes available?
512+
KVM_NODES=$(kubectl get nodes -l hyperlight.dev/hypervisor=kvm -o name 2>/dev/null | wc -l)
513+
if [ "$KVM_NODES" -gt 0 ]; then
514+
log_success "✅ ${KVM_NODES} KVM node(s) available"
515+
PASS=$((PASS + 1))
516+
else
517+
log_error "❌ No KVM nodes found — check node pool labels"
518+
FAIL=$((FAIL + 1))
519+
fi
520+
521+
# 3. Device plugin running?
522+
PLUGIN_READY=$(kubectl get pods -n hyperlight-system -l app.kubernetes.io/name=hyperlight-device-plugin -o jsonpath='{.items[*].status.phase}' 2>/dev/null)
523+
if echo "$PLUGIN_READY" | grep -q "Running"; then
524+
log_success "✅ Device plugin running"
525+
PASS=$((PASS + 1))
526+
else
527+
log_error "❌ Device plugin not running — run 'just k8s-deploy-plugin' or 'just k8s-local-deploy-plugin'"
528+
FAIL=$((FAIL + 1))
529+
fi
530+
531+
# 4. Hypervisor resource allocatable?
532+
CAPACITY=$(kubectl get nodes -o jsonpath='{.items[*].status.allocatable.hyperlight\.dev/hypervisor}' 2>/dev/null | tr ' ' '\n' | grep -v '^$' | head -1)
533+
if [ -n "$CAPACITY" ] && [ "$CAPACITY" != "0" ]; then
534+
log_success "✅ hyperlight.dev/hypervisor resource available (capacity: ${CAPACITY})"
535+
PASS=$((PASS + 1))
536+
else
537+
log_error "❌ No hyperlight.dev/hypervisor resource — device plugin may not be working"
538+
FAIL=$((FAIL + 1))
539+
fi
540+
541+
# 5. Namespace exists?
542+
if kubectl get namespace hyperagent &>/dev/null; then
543+
log_success "✅ hyperagent namespace exists"
544+
PASS=$((PASS + 1))
545+
else
546+
log_error "❌ hyperagent namespace missing — run 'just k8s-deploy-plugin' (creates namespace)"
547+
FAIL=$((FAIL + 1))
548+
fi
549+
550+
# 6. Auth secret exists?
551+
if kubectl get secret hyperagent-auth -n hyperagent &>/dev/null; then
552+
log_success "✅ hyperagent-auth secret exists"
553+
PASS=$((PASS + 1))
554+
else
555+
log_error "❌ hyperagent-auth secret missing — run 'just k8s-setup-auth'"
556+
FAIL=$((FAIL + 1))
557+
fi
558+
559+
# Summary
560+
echo ""
561+
echo "════════════════════════════════════════"
562+
if [ "$FAIL" -eq 0 ]; then
563+
log_success "All ${PASS} checks passed — ready to run prompts! 🚀"
564+
else
565+
log_error "${FAIL} check(s) failed, ${PASS} passed"
566+
echo ""
567+
log_info "Fix the issues above, then re-run: just k8s-smoke-test"
568+
fi
569+
echo "════════════════════════════════════════"
570+
echo ""
571+
[ "$FAIL" -eq 0 ]

builtin-modules/pdf.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
"description": "PDF 1.7 document generation — text, graphics, metadata, standard fonts. Flow-based layout for auto-paginating documents.",
44
"author": "system",
55
"mutable": false,
6-
"sourceHash": "sha256:202d5c76da3d341a",
7-
"dtsHash": "sha256:38f8a8a62174a4f7",
6+
"sourceHash": "sha256:c8716bcb3295f5bc",
7+
"dtsHash": "sha256:f30fba88bfe5f977",
88
"importStyle": "named",
99
"hints": {
1010
"overview": "Generate PDF documents with text, shapes, and metadata. Uses PDF's 14 standard fonts (no embedding required). Coordinates are in points (72 points = 1 inch), with top-left origin.",

builtin-modules/pptx-tables.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "Styled tables for PPTX presentations - headers, borders, alternating rows",
44
"author": "system",
55
"mutable": false,
6-
"sourceHash": "sha256:2fd1b0318ee87ab2",
6+
"sourceHash": "sha256:e03a2365c45ab0e6",
77
"dtsHash": "sha256:130d021921083af6",
88
"importStyle": "named",
99
"hints": {

builtin-modules/src/pdf.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
import {
3434
autoTextColor,
35+
contrastRatio,
3536
getTheme,
3637
hexColor,
3738
requireArray,
@@ -3004,7 +3005,7 @@ export interface TableStyle {
30043005
headerFg: string;
30053006
/** Header font. */
30063007
headerFont: string;
3007-
/** Body text colour (6-char hex). */
3008+
/** Body text colour (6-char hex). Auto-contrasted if omitted or poor contrast. */
30083009
bodyFg: string;
30093010
/** Body font. */
30103011
bodyFont: string;
@@ -3014,6 +3015,8 @@ export interface TableStyle {
30143015
borderColor: string;
30153016
/** Border line width in points. */
30163017
borderWidth: number;
3018+
/** Page background colour (set internally for contrast checking). */
3019+
_pageBg?: string;
30173020
}
30183021

30193022
/** Built-in table styles matching PPTX table styles. */
@@ -3484,6 +3487,43 @@ function renderTable(
34843487
const rowH = tableRowHeight(fontSize, compact);
34853488
const headerH = rowH;
34863489

3490+
// Ensure page background is available for contrast checking
3491+
if (!style._pageBg) {
3492+
style._pageBg = doc.theme.bg;
3493+
}
3494+
3495+
// ── Contrast auto-correction ─────────────────────────────────────
3496+
// Automatically fix text colors that have poor contrast against the
3497+
// page background. No errors — just silently correct to readable.
3498+
const MIN_CONTRAST = 3.0;
3499+
const pageBg = style._pageBg || "FFFFFF";
3500+
3501+
if (style.bodyFg) {
3502+
const bodyRatio = contrastRatio(style.bodyFg, pageBg);
3503+
if (bodyRatio < MIN_CONTRAST) {
3504+
style.bodyFg = autoTextColor(pageBg);
3505+
}
3506+
}
3507+
3508+
if (style.headerFg && style.headerBg) {
3509+
const headerRatio = contrastRatio(style.headerFg, style.headerBg);
3510+
if (headerRatio < MIN_CONTRAST) {
3511+
style.headerFg = autoTextColor(style.headerBg);
3512+
}
3513+
}
3514+
3515+
if (style.headerBg && style.headerFg) {
3516+
const headerRatio = contrastRatio(style.headerFg, style.headerBg);
3517+
if (headerRatio < MIN_CONTRAST) {
3518+
const suggested = autoTextColor(style.headerBg);
3519+
throw new Error(
3520+
`table: headerFg "${style.headerFg}" has poor contrast (${headerRatio.toFixed(1)}:1) against ` +
3521+
`headerBg "${style.headerBg}". Minimum is ${MIN_CONTRAST}:1. ` +
3522+
`Use "${suggested}" instead.`
3523+
);
3524+
}
3525+
}
3526+
34873527
// Text baseline offset within a row: top padding + font size
34883528
// (drawText Y is the baseline position in top-left coords)
34893529
const padV = compact ? 2 : CELL_PAD_V;
@@ -3574,7 +3614,7 @@ function renderTable(
35743614
doc.drawText(headerText, textX, curY + textYOffset, {
35753615
font: style.headerFont,
35763616
fontSize,
3577-
color: style.headerFg,
3617+
color: style.headerBg ? autoTextColor(style.headerBg) : style.headerFg,
35783618
});
35793619
cellX += colWidths[c];
35803620
}
@@ -3606,6 +3646,13 @@ function renderTable(
36063646
doc.drawRect(x, curY, totalWidth, rowH, { fill: style.altRowBg });
36073647
}
36083648

3649+
// Auto-contrast body text against effective row background
3650+
const isAlt = !!(style.altRowBg && r % 2 === 1);
3651+
const rowBg = isAlt
3652+
? style.altRowBg
3653+
: (style._pageBg || "FFFFFF");
3654+
const rowFg = autoTextColor(rowBg);
3655+
36093656
// Cell text AFTER background
36103657
const isBoldRow = rowBold?.[r] ?? false;
36113658
const cellFont = isBoldRow
@@ -3619,7 +3666,7 @@ function renderTable(
36193666
doc.drawText(cellText, textX, curY + textYOffset, {
36203667
font: cellFont,
36213668
fontSize,
3622-
color: style.bodyFg,
3669+
color: rowFg,
36233670
});
36243671
cellX += colWidths[c];
36253672
}

0 commit comments

Comments
 (0)