Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6621ada
feat(agents): add descriptor-driven agent contract
raivieiraadriano92 Jul 28, 2026
d2e5948
feat(config): add agentDeploy feature flag
raivieiraadriano92 Jul 28, 2026
8c66697
feat(agents): extend provider seam with catalog/deploy/status
raivieiraadriano92 Jul 28, 2026
6c18f61
feat(haystack): add Deepset management client
raivieiraadriano92 Jul 28, 2026
218a0b1
feat(haystack): implement catalog/deploy/status
raivieiraadriano92 Jul 28, 2026
501389f
refactor(agents): support async provider list()
raivieiraadriano92 Jul 29, 2026
ac1c0da
feat(haystack): discover pipelines from Deepset API
raivieiraadriano92 Jul 29, 2026
723642c
feat(agents): add deploy, status, and catalog endpoints
raivieiraadriano92 Jul 29, 2026
4e69b53
feat(agents): add frontend deploy API client
raivieiraadriano92 Jul 29, 2026
9073685
feat(agents): add descriptor-driven form renderer
raivieiraadriano92 Jul 29, 2026
b661de2
feat(agents): add deploy flow to agents settings
raivieiraadriano92 Jul 29, 2026
01982c2
fix(agents): persist deploy to agents table without polling
raivieiraadriano92 Jul 29, 2026
998adcb
feat(agents): unify add-agent flow into one panel with back nav
raivieiraadriano92 Jul 29, 2026
3c74244
feat(agents): reuse unified add-agent flow in chat picker
raivieiraadriano92 Jul 29, 2026
959636a
style(agents): align deploy form with the connect form
raivieiraadriano92 Jul 29, 2026
3f249e8
feat(agents): normalize haystack deploy statuses to pending/running/f…
raivieiraadriano92 Jul 29, 2026
667afa1
feat(agents): live deploy-status badge on the agents list
raivieiraadriano92 Jul 29, 2026
7a63598
fix(haystack): buffer ws frames arriving before async open() completes
raivieiraadriano92 Jul 29, 2026
c458ab6
feat(config): add openclaw/e2b settings
raivieiraadriano92 Jul 30, 2026
ba78c5a
feat(openclaw): add E2B provisioning helpers, readiness probe, ACP relay
raivieiraadriano92 Jul 30, 2026
262ba72
feat(openclaw): add AgentProvider (catalog/deploy/status)
raivieiraadriano92 Jul 30, 2026
983ce0c
feat(openclaw): mount WS relay route + register provider
raivieiraadriano92 Jul 30, 2026
2a71a2a
docs: add agent deployment architecture guide
raivieiraadriano92 Jul 30, 2026
0023728
chore(haystack): use Haystack in deploy descriptor description
raivieiraadriano92 Jul 30, 2026
23c424d
feat(agents): add undeploy verb + DELETE deployments endpoint
raivieiraadriano92 Jul 30, 2026
6a6b1e7
feat(agents): undeploy on agent delete
raivieiraadriano92 Jul 30, 2026
122b3d2
docs: document agent undeploy endpoint
raivieiraadriano92 Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -112,23 +112,37 @@ OTEL_EXPORTER_OTLP_TOKEN=
# Web search via Exa.
EXA_API_KEY=

# === Agents — optional ===
# AGENT_DEPLOY: expose the descriptor-driven "Add agent" deploy flow (THU-743).
# Opt-in; defaults off. Haystack is the first deployable provider.
AGENT_DEPLOY=false

# === Haystack / Deepset Cloud — optional ===
# Configure to expose Deepset RAG pipelines as managed ACP agents.
# Configure to expose Deepset pipelines as managed ACP agents. Pipelines are
# discovered live from the Deepset API (GET /agents lists DEPLOYED, prompt-capable
# pipelines, excluding Thunderbolt-deployed `tb-*` ones) — no static list to maintain.
# HAYSTACK_BASE_URL: Deepset Cloud root (e.g. https://api.cloud.deepset.ai).
# HAYSTACK_API_KEY: bearer token used as `Authorization: Bearer …`.
# HAYSTACK_WORKSPACE: workspace slug. URLs become
# `${HAYSTACK_BASE_URL}/api/v1/workspaces/${HAYSTACK_WORKSPACE}/...`.
# HAYSTACK_PIPELINES: JSON array. Each entry needs:
# - id public slug surfaced to the FE (`rag-chat`).
# - name human-readable label.
# - pipelineName Deepset URL slug used in /pipelines/<slug>/chat-stream.
# - pipelineId Deepset UUID, sent as `pipeline_id` when bootstrapping
# a `search_session`.
# - icon (optional) Phosphor icon name shown in the agent picker.
HAYSTACK_BASE_URL=
HAYSTACK_API_KEY=
HAYSTACK_WORKSPACE=
HAYSTACK_PIPELINES='[{"id":"rag-chat","name":"RAG Chat","pipelineName":"rag-chat-pipeline","pipelineId":"15cf8b39-0000-0000-0000-000000000000","icon":"book"}]'
# HAYSTACK_TEMPLATE_PIPELINE: name of an existing pipeline whose YAML is cloned when a
# user deploys a Haystack agent (requires AGENT_DEPLOY=true). Empty = not deployable.
HAYSTACK_TEMPLATE_PIPELINE=

# === OpenClaw (sandbox-hosted agent) — optional ===
# Configure to expose OpenClaw as a one-click deployable agent (requires AGENT_DEPLOY=true).
# Owner-managed: we spin a per-user E2B microVM from a prebuilt template (name hardcoded
# as `thunderbolt-openclaw`) and relay ACP frames to it. All three values must be set,
# else OpenClaw is not deployable.
# E2B_API_KEY: E2B account key used to create/connect/kill sandboxes.
# OPENCLAW_MODEL: "provider/model" the agent runs (e.g. openrouter/...).
# OPENCLAW_OPENROUTER_API_KEY: shared OpenRouter key injected into the sandbox for inference.
E2B_API_KEY=
OPENCLAW_MODEL=
OPENCLAW_OPENROUTER_API_KEY=

# === Trusted proxy ===
# Controls which proxy headers are trusted for IP extraction in rate limiting.
Expand Down
39 changes: 39 additions & 0 deletions backend/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"bun-types": "^1.2.22",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.45.2",
"e2b": "^2.36.1",
"elysia": "^1.4.28",
"exa-js": "^2.16.0",
"jose": "^6.2.2",
Expand Down
33 changes: 33 additions & 0 deletions backend/src/agents/deployment-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { describe, expect, it } from 'bun:test'
import { decodeDeploymentId, encodeDeploymentId } from './deployment-id'

describe('deployment id codec', () => {
it('round-trips provider + ref', () => {
const id = encodeDeploymentId('haystack', 'tb-my-agent')
expect(id).toBe('haystack:tb-my-agent')
expect(decodeDeploymentId(id)).toEqual({ provider: 'haystack', ref: 'tb-my-agent' })
})

it('splits on the first separator so refs may contain colons', () => {
expect(decodeDeploymentId('haystack:a:b:c')).toEqual({ provider: 'haystack', ref: 'a:b:c' })
})

it('rejects a provider containing the separator', () => {
expect(() => encodeDeploymentId('hay:stack', 'ref')).toThrow()
})

it('rejects an empty provider or ref', () => {
expect(() => encodeDeploymentId('', 'ref')).toThrow()
expect(() => encodeDeploymentId('haystack', '')).toThrow()
})

it('rejects malformed ids', () => {
expect(() => decodeDeploymentId('haystack')).toThrow()
expect(() => decodeDeploymentId(':ref')).toThrow()
expect(() => decodeDeploymentId('haystack:')).toThrow()
})
})
33 changes: 33 additions & 0 deletions backend/src/agents/deployment-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
* A deployment id is `<provider>:<ref>` — self-describing so the deployment-status
* endpoint can resolve the owning provider and poll the host directly, with no
* server-side deployment table (THU-743). `provider` is the registry key (the
* agent kind); `ref` is the host-scoped identifier (e.g. a Deepset pipeline name).
*/

const separator = ':'

/** Build a deployment id from a provider id and a host reference. */
export const encodeDeploymentId = (provider: string, ref: string): string => {
if (!provider || provider.includes(separator)) {
throw new Error(`invalid provider for deployment id: ${JSON.stringify(provider)}`)
}
if (!ref) {
throw new Error('deployment id requires a non-empty ref')
}
return `${provider}${separator}${ref}`
}

/** Parse a deployment id back into its provider and ref. Splits on the first
* separator so a ref may itself contain `:`. */
export const decodeDeploymentId = (deploymentId: string): { provider: string; ref: string } => {
const index = deploymentId.indexOf(separator)
if (index <= 0 || index === deploymentId.length - 1) {
throw new Error(`malformed deployment id: ${JSON.stringify(deploymentId)}`)
}
return { provider: deploymentId.slice(0, index), ref: deploymentId.slice(index + 1) }
}
Loading
Loading