Skip to main content

06d-appkit-agent-app-proxy

Wire an AppKit frontend (Databricks App) to a separate Agent App backend (Databricks App hosting mlflow.genai.agent_server or ResponsesAgent) with OBO token forwarding via x-forwarded-access-token, app-to-app service principal auth, dual-format SSE streaming, and the /api/chat contract. Use this skill when the agent is deployed as its own Databricks App — not as a Model Serving endpoint. Triggers on "agent app backend", "two apps", "appkit plus agent app", "proxy agent app", "agent-backend resource", "x-forwarded-access-token", "migrate agent to apps", "agent app proxy".

설치로 이동

소스 정보

저장소
databricks-solutions/vibe-coding-workshop-template
최근 소스 활동
2026년 6월 25일 06:16
감지된 SKILL.md 언어
영어
스타
6
포크
7

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
6 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
06d-appkit-agent-app-proxy
description
Wire an AppKit frontend (Databricks App) to a separate Agent App backend (Databricks App hosting mlflow.genai.agent_server or ResponsesAgent) with OBO token forwarding via x-forwarded-access-token, app-to-app service principal auth, dual-format SSE streaming, and the /api/chat contract. Use this skill when the agent is deployed as its own Databricks App — not as a Model Serving endpoint. Triggers on "agent app backend", "two apps", "appkit plus agent app", "proxy agent app", "agent-backend resource", "x-forwarded-access-token", "migrate agent to apps", "agent app proxy".
license
Apache-2.0
compatibility
Node.js v22+, Databricks CLI >= 0.295.0, mlflow >= 3.6, AppKit >= 0.x
applies_to
[{"variant":"4-agentapp-plus-appkit"}]
when
AppKit scaffold exists (01-03) and the agent is deployed as its own Databricks App via Track A (07-deploy-and-query, target=databricks_apps).
prereqs
["Agent App deployed and RUNNING (Track A Prompt 17)","AppKit App has CAN_USE on the Agent App (resource binding)","Node.js v22+, Databricks CLI >= 0.295.0"]
produces
["apps_lakebase/$APP_NAME/server/agent-proxy.ts","Edits to apps_lakebase/$APP_NAME/server/server.ts (extend + /api/chat)","Edits to apps_lakebase/$APP_NAME/app.yaml (AGENT_APP_URL via valueFrom)","Edits to apps_lakebase/$APP_NAME/databricks.yml (app resource block)"]
fields_read
["agent.backend_url","agent.stream_format"]
allowed-tools
Bash(databricks:*) Bash(npm:*) Bash(curl:*) Bash(node:*) Read
clients
["ide_cli","genie_code"]
bundle_resource
apps
deploy_verb
apps_deploy
deploy_note
The proxy handler + OBO header forwarding (`x-forwarded-access-token`, `x-app-user-email`) is server-side code — **client-agnostic**, runs identically on both clients. IDE: `databricks apps get/bundle validate --profile $PROFILE`, `npm run build`, then `databricks apps deploy --profile $PROFILE`. Genie Code: run `apps get` / `bundle validate` via `runDatabricksCli` (omit `--profile`; a targetless `bundle validate` may need `--target dev`); local `npm run build` is an IDE convenience (server-side build on deploy); deploy per `03-appkit-deploy`. Verify via browser + `apps logs` or the OAuth-session test — `auth token` + raw Bearer `curl` to `/invocations` is hard-blocked on Genie Code.
coverage
full
metadata
{"author":"prashanth subrahmanyam","version":"1.1.0","domain":"apps","role":"serving-wiring","standalone":false,"last_verified":"2026-06-02","volatility":"medium","upstream_sources":[{"name":"databricks-agent-skills/databricks-model-serving","repo":"databricks/databricks-agent-skills","paths":"[Truncated]","relationship":"extended","last_synced":"2026-04-27","sync_commit":"manifest-v2-2026-04-22"},{"name":"databricks-agent-skills/databricks-apps","repo":"databricks/databricks-agent-skills","paths":"[Truncated]","relationship":"extended","last_synced":"2026-04-27","sync_commit":"manifest-v2-2026-04-22"}]}
# Wire AppKit Frontend to Agent App Backend (2-Apps Canonical) Proxy requests from an AppKit Databricks App (frontend) to a separate Agent Databricks App (backend) with end-user identity forwarded via `x-forwarded-access-token`. This is the canonical wiring for **Variant 4** of the workshop — the 2-Apps pattern where agent runtime and product UI are on independent deployment cadences. ## When to Use - The agent is deployed as its **own Databricks App** (per [Track A 07-deploy-and-query](../../../genai-agents/tracks/A-custom-agent-apps/07-deploy-and-query/SKILL.md) with `target=databricks_apps`). - You want a rich AppKit dashboard (Lakebase tables, admin views, custom React) **plus** a separate agent service. - You need **user-scoped** tool calls (Lakebase row-level auth, Genie, per-user UC grants) — OBO is non-negotiable. - You accept two deployables and two deploy cadences. ## When NOT to Use - **Agent deployed to Model Serving / Agent Serving endpoint** — use [`06-appkit-serving-wiring`](../06-appkit-serving-wiring/SKILL.md). URL shape, auth model, and resource type differ. - **Supervisor API (Beta) hosted agent** — use [`06b-appkit-supervisor-wiring`](../06b-appkit-supervisor-wiring/SKILL.md). That path uses a Python sidecar, not a separate Databricks App. - **Agent in the same AppKit process (Node-native)** — keep the agent in-process and skip this proxy skill. The older `06c-appkit-integrated-agent` path is not bundled in this template. - **Template-only chat app** (Variant 3) — no AppKit at all; just use a Databricks Apps template directly. ## Architecture ```mermaid flowchart LR Browser["Browser"] AppKit["AppKit App (Node/Express)"] AgentApp["Agent App (FastAPI + mlflow.genai.agent_server)"] Tools["UC Functions / Genie / Vector Search / Lakebase"] Browser -->|"POST /api/chat (SSE)"| AppKit AppKit -->|"POST /invocations<br/>Authorization: Bearer SP<br/>x-forwarded-access-token"| AgentApp AgentApp -->|"get_user_workspace_client(request)"| Tools ``` Two independent auth layers on every request: - **App-to-App**: AppKit service principal has `CAN_USE` on the Agent App (carried as `Authorization: Bearer <SP>`). - **End-user OBO**: forwarded verbatim as `x-forwarded-access-token` and consumed by the agent handler. See [`references/obo-forwarding.md`](references/obo-forwarding.md) for the full contract. --- ## Header Contract (Canonical) ### Inbound headers (browser → AppKit App) Databricks Apps canonical user headers, set by the Apps platform on every request: - `x-forwarded-email` — the end user's email (canonical attribute used as MLflow `AssessmentSource.source_id`). - `x-forwarded-preferred-username` — the user's preferred username (often equals the email). - `x-forwarded-user` — the user's stable identifier (workspace user id). - `x-forwarded-access-token` — the downscoped OBO token for user-scoped Databricks API calls (only present when the App declares `user_api_scopes`). `x-forwarded-user-info` is **not** a canonical Databricks Apps header and must not be used. Earlier internal code paths assumed it existed; it does not. Always read the four canonical headers above. ### Outbound header (AppKit App → Agent App) Pathway-C AppKit proxies must set `x-app-user-email` on outbound requests to the Agent App. The agent uses this value as `AssessmentSource.source_id` for MLflow feedback. **Do not infer originating user identity from the app-to-app OAuth `Authorization: Bearer` token** — that token represents the AppKit App service principal hop, not the originating end user. Concretely, in the proxy handler (Step 2 below): ```typescript const email = req.headers["x-forwarded-email"]; if (typeof email === "string" && email.length > 0) { headers.set("x-app-user-email", email); } ``` The Agent App's `@invoke` / `@stream` handler then reads `x-app-user-email` from its incoming request and passes it as `AssessmentSource(source_type="HUMAN", source_id=<email>)` when it logs feedback to MLflow. See [Track A 04-authentication](../../../genai-agents/tracks/A-custom-agent-apps/04-authentication/SKILL.md) for the agent-side contract. --- ## SSE Synthesis Debt (`workspace_sse_guardrail`) The Sonnet endpoints used in the workshop are subject to an AI-Gateway output guardrail that trips on streaming responses. When the selected `agent_chat` endpoint has `streaming_ok == false` in `endpoint_guardrail_audit`, this proxy MUST call the Agent App with `stream: false`, take the unary `/invocations` JSON response, and **synthesize** the SSE stream the frontend expects — typically one `response.output_text.delta` followed by `response.completed` and `[DONE]`. The dual extractor in [`references/dual-format-streaming.md`](references/dual-format-streaming.md) consumes that synthesized stream identically to a real one. `debt: workspace_sse_guardrail` is canonical while the selected `agent_chat` endpoint has `streaming_ok == false`. This is "accepted permanent" only in the sense that the workshop has no admin-ticket or external-key requirement. The debt still has `remove_when` and must be audited by `vibecoding-state.audit_debts`. Operator obligations: - The synthesized-SSE branch MUST be tagged `debt: workspace_sse_guardrail` in `Globals.productized_debts[]`, with a `remove_when` predicate referencing `endpoint_guardrail_audit[llm_role_endpoints.agent_chat.endpoint].streaming_ok == true`. - Do **not** assume the workaround is invisible just because no admin ticket is filed. `audit_debts` (see [`vibecoding-state` SKILL.md](../../../skills/vibecoding-state/SKILL.md)) re-evaluates `remove_when` on every audit run; once the upstream guardrail flips, the debt's `debt_lifted` evaluation flips and the synthesis branch must be removed. - Ship a one-line revert path gated on `debt_lifted` (e.g. `return upstream.body` instead of synthesizing) so the proxy reverts to true streaming without further engineering when the policy changes. Without this discipline, the proxy productizes the workaround forever — Track A users get non-streaming UX even after the underlying policy is fixed, because no operator will know to delete the synthesized-SSE branch. --- ## Request Body Contract (Dual Shape, Normalized) The proxy MUST accept and normalize **both** of the request body shapes that AppKit chat clients and Track A authoring patterns produce: - **Responses-style** — `{ "input": ... }` (the shape `mlflow.genai.agent_server` Responses adapters expect). - **Chat-style** — `{ "messages": [{ "role": "user", "content": "..." }, ...] }` (the OpenAI-compatible shape used by the minimal hook in Step 4). Anything else MUST be rejected at the proxy (before any upstream call) with HTTP `400` and a JSON body: ```json { "error": "invalid_body_shape", "accepted": ["input", "messages"] } ``` Concretely, in the proxy handler: ```typescript type Body = { input?: unknown; messages?: unknown }; const body = (req.body ?? {}) as Body; const hasInput = body.input !== undefined; const hasMessages = Array.isArray(body.messages); if (!hasInput && !hasMessages) { res.status(400).json({ error: "invalid_body_shape", accepted: ["input", "messages"] }); return; } const upstreamBody = hasInput ? { input: body.input } : { messages: body.messages }; ``` Why the dual shape matters: an earlier iteration of the proxy assumed only `{messages}` and dropped `{input}` callers silently — the agent received a malformed body and returned an empty stream that the dual extractor rendered as a blank UI. Rejecting with `invalid_body_shape` makes the contract violation observable instead of silent. See [`references/dual-format-streaming.md`](references/dual-format-streaming.md) for the streaming counterpart of this contract — dual **request** shapes here, dual **response** shapes there. --- ## Before You Begin **Prerequisites — verify these before proceeding:** 1. Agent App is deployed and in `RUNNING` state: ```bash databricks apps get "$AGENT_APP_NAME" --profile "$PROFILE" --output json | jq -r '.status.state' ``` Expect `RUNNING`. If not, complete [Track A Prompt 17](../../../example/skyloyalty/WALKTHROUGH.md#prompt-17---deploy-agent) before continuing. 2. Agent App URL is known: ```bash databricks apps get "$AGENT_APP_NAME" --profile "$PROFILE" --output json | jq -r '.url' ``` 3. Agent App accepts `/invocations`: ```bash AGENT_URL=$(databricks apps get "$AGENT_APP_NAME" --profile "$PROFILE" --output json | jq -r '.url') TOKEN=$(databricks auth token --profile "$PROFILE" --output json | jq -r '.access_token') curl -s -X POST "${AGENT_URL%/}/invocations" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"messages":[{"role":"user","content":"Hello"}]}' | head -c 500 ``` Non-empty response with HTTP 200 means the agent is reachable. A 401 means your SP lacks `CAN_USE` (see Step 1). A 404 means the agent wasn't authored with `/invocations` (it must use `mlflow.genai.agent_server` or `AgentServer.serve()` — see [Track A 02-agent-framework](../../../genai-agents/tracks/A-custom-agent-apps/02-agent-framework/SKILL.md)). 4. Node.js v22+ and Databricks CLI `>= 0.295.0`: ```bash node --version # v22.x or higher databricks --version # >= 0.295.0 ``` ### Working in Genie Code (client routing) The proxy handler and OBO forwarding (Step 2) are **server-side code — identical on both clients**. Only the toolchain commands and the deployed-app probes differ. `$AGENT_APP_NAME` / `$APPKIT_APP_NAME` / `$PROFILE` / `$AGENT_APP_URL` resolve from `.vibecoding-state.md` when a prior phase wrote them (don't re-derive). Apply these substitutions: | IDE/CLI (as written) | Genie Code substitution | |----------------------|--------------------------| | `databricks apps get … --profile $PROFILE` (Prereqs 1–2) | run via `runDatabricksCli` (read-tier), **omit `--profile`** | | `databricks auth token` + `curl … /invocations -H "Authorization: Bearer …"` (Prereq 3) | `auth token` is hard-blocked and raw Bearer is rejected — confirm reachability from the **deployed AppKit proxy** (Step 6) or via the OAuth-session test in `03-appkit-deploy`; for a quick SP-only check use the SDK `executeCode` (`w.config` + `requests`) | | `databricks bundle validate --profile $PROFILE` (Step 1d) | run via `runDatabricksCli` (omit `--profile`); if a targetless validate is guardrail-blocked, pass `--target dev` | | `npm run build` gates (Steps 2, 3, 4, 5) | **IDE-only** convenience — no local Node toolchain. Skip; the platform builds **server-side** on deploy, errors surface in `databricks apps logs <name>` | | `npm run dev` | not available — verify on the deployed app | | `databricks apps deploy …` (Step 6a) | see the `03-appkit-deploy` deploy-routing contract (`runDatabricksCli`, else SDK `w.apps.deploy(... SNAPSHOT)`) | | `test-agent-app-proxy.sh` four-probe E2E (Step 6b) | runs against **deployed** apps; on Genie Code drive its `databricks`/`curl` calls via `runDatabricksCli` / `executeCode` (omit `--profile`). The browser check (Step 6c) is the simplest manual verify. | Paths are relative to `apps_lakebase/$APP_NAME` — inside your git-cloned workshop project (`artifact_root`) on Genie Code, never the read-only `.assistant/skills` copy and never `/tmp`. See `skills/genie-code-environment` for the full manifest. --- ## Decision Defaults | Decision | Default | Rationale | |----------|---------|-----------| | Chat route | `/api/chat` (SSE POST) | Matches the Track A agent app contract and keeps the frontend hook stable across variants | | Frontend hook | Custom `useChat`-shaped hook (raw `fetch` + `ReadableStream`) | Serving plugin's `useServingStream` assumes a Serving endpoint URL shape | | App-to-App auth | AppKit SP via `CAN_USE` on the Agent App | Explicit resource binding; auditable | | End-user auth | Forward `x-forwarded-access-token` verbatim | What the agent's `get_user_workspace_client(request)` expects | | Streaming format | Dual parser (Responses API + OpenAI chat chunks) | `mlflow.genai.agent_server` emits both depending on authoring pattern | | Timeout | 120000 ms (2 min) | Agents can be slow; first-request cold-start ~20 s | | Serving plugin? | **No** — use vanilla `server()` + `server.extend()` | Serving plugin's resource shape is wrong for Apps backends | | Agent App URL env var | `AGENT_APP_URL` via `valueFrom: agent-backend` | Explicit, avoids platform-derived names that vary across CLI versions | --- ## Step 0: Workspace alignment preflight `app:` resource binding (Step 1) **only** works when the AppKit App and the Agent App live in the same Databricks workspace. Cross-workspace bindings are silently dropped at deploy time. Run this preflight before declaring the resource — it takes 30 seconds and prevents a multi-hour migration loop. ```bash profile_host() { local profile="$1" databricks auth profiles --skip-validate --output json \ | jq -r --arg p "$profile" '.profiles[] | select(.name == $p) | .host' } APPKIT_PROFILE="${APPKIT_PROFILE:-$PROFILE}" AGENT_PROFILE="${AGENT_PROFILE:-$PROFILE}" APPKIT_HOST=$(profile_host "$APPKIT_PROFILE") AGENT_HOST=$(profile_host "$AGENT_PROFILE") if [ -z "$APPKIT_HOST" ] || [ -z "$AGENT_HOST" ]; then echo "FAIL: could not resolve both Databricks profile hosts." echo " APPKIT_PROFILE=$APPKIT_PROFILE -> ${APPKIT_HOST:-<missing>}"
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기