Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
# Atlas compiles its wire contract into the binary with `include_str!`, so the
# file is a build input rather than documentation the image can do without.
!libs/@local/graph/atlas/docs/wire.md
# Brunch packages prompts and skills through `?raw` imports in their builds.
!libs/@hashintel/brunch-agent/packages/*/src/**/*.md

########################
## Same as in .gitignore
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ jobs:
build_args: "API_ORIGIN=http://localhost:5001\nFRONTEND_URL=http://localhost:3000\n",
ecs: []
},
{
package: "@apps/brunch-agent",
service: "brunch-agent",
push: ["ecr", "ghcr"],
dockerfile: "apps/brunch-agent/docker/Dockerfile",
context: ".",
ecs: []
},
{
package: "@apps/hash-ai-worker-ts",
service: "ai-worker-ts",
Expand Down
79 changes: 79 additions & 0 deletions apps/brunch-agent/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# syntax=docker/dockerfile:1

# Build from the repository root:
# docker buildx build --file apps/brunch-agent/docker/Dockerfile . --load

FROM debian:13.3-slim AS tools

SHELL ["/bin/bash", "-euo", "pipefail", "-c"]

ENV MISE_DATA_DIR="/mise"
ENV MISE_CACHE_DIR="/mise/cache"
ENV PATH="/mise/shims:$PATH"

COPY .config/mise /etc/mise

RUN --mount=type=secret,id=GITHUB_TOKEN,env=GITHUB_TOKEN \
apt-get update && \
apt-get install -y --no-install-recommends ca-certificates curl && \
/etc/mise/install.sh && \
mise --version && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
eval "$(mise activate bash)" && \
mise install --locked node npm:turbo yq


FROM tools AS pruner

WORKDIR /repo

COPY . .

RUN mise trust && \
turbo prune $(.github/scripts/prune-scopes.sh '@apps/brunch-agent') --docker


FROM tools AS builder

WORKDIR /repo

RUN corepack enable

COPY --from=pruner /repo/out/json/ ./
COPY --from=pruner /repo/out/yarn.lock ./yarn.lock
COPY --from=pruner /repo/out/full/.yarn ./.yarn
COPY --from=pruner /repo/out/full/turbo.json ./turbo.json

RUN --mount=type=cache,target=/root/.yarn/berry/cache \
yarn install --immutable

COPY --from=pruner /repo/out/full/ ./

RUN turbo run build --filter '@apps/brunch-agent' --env-mode=loose && \
yarn workspaces focus @apps/brunch-agent --production && \
rm -rf apps/brunch-agent/test


FROM tools AS runner

ENV NODE_ENV=production
ENV PORT=3002

WORKDIR /repo/apps/brunch-agent

COPY --from=builder /repo /repo

RUN groupadd --system --gid 60000 hash && \
useradd --system --uid 60000 --gid hash --create-home brunch && \
install --directory --owner brunch --group hash \
/repo/apps/brunch-agent/.data-wipe-me

USER brunch:hash
Comment thread
cursor[bot] marked this conversation as resolved.

EXPOSE 3002

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["node", "-e", "fetch('http://127.0.0.1:3002/health').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"]

CMD ["node", "dist/server.mjs"]
1 change: 1 addition & 0 deletions apps/brunch-agent/docs/task-dependencies.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@hashintel/brunch-agent-transport-aisdk#build",
"@hashintel/petrinaut-core#build"
],
"build:docker": [],
"dev": [
"@hashintel/brunch-agent#build",
"@hashintel/brunch-agent-binding-flue#build",
Expand Down
1 change: 1 addition & 0 deletions apps/brunch-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"type": "module",
"scripts": {
"build": "vite build && vite build --config vite.client.config.ts",
"build:docker": "docker buildx build --tag brunch-agent --file docker/Dockerfile ../../ --load",
"dev": "vite dev",
"fix:eslint": "oxlint --fix --type-aware --type-check --report-unused-disable-directives-severity=error .",
"lint:eslint": "oxlint --type-aware --type-check --report-unused-disable-directives-severity=error .",
Expand Down
9 changes: 8 additions & 1 deletion apps/brunch-agent/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ import { createAgentRouter } from "@flue/runtime/routing";
import { Hono } from "hono";

import { ChatAgent } from "./agents/chat-agent/agent.ts";
import { healthHandler } from "./health.ts";
import { assetHandler } from "./http/assets.ts";
import { agentOwnershipGuard } from "./http/ownership.ts";
import { createPetrinautChatHandler } from "./http/petrinaut-chat.ts";
import { CHAT_AGENT_ROUTE, PETRINAUT_CHAT_ROUTE } from "./http/routes.ts";
import {
CHAT_AGENT_ROUTE,
HEALTH_ROUTE,
PETRINAUT_CHAT_ROUTE,
} from "./http/routes.ts";

instrument(createOpenTelemetryInstrumentation({ content: false }));

Expand All @@ -35,6 +40,8 @@ app.on(["GET", "POST", "OPTIONS"], PETRINAUT_CHAT_ROUTE, (c) =>
petrinautChatHandler(c.req.raw),
);

app.get(HEALTH_ROUTE, healthHandler);

const uiRoot = new URL(
// oxlint-disable-next-line typescript/no-unnecessary-condition -- import.meta.env is absent when Node executes this module directly.
import.meta.env?.DEV === false ? "./client/" : "../",
Expand Down
7 changes: 7 additions & 0 deletions apps/brunch-agent/src/health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Context } from "hono";

export const healthHandler = (context: Context): Response =>
context.json({ status: "pass" }, 200, {
"cache-control": "no-store",
"content-type": "application/health+json",
});
3 changes: 3 additions & 0 deletions apps/brunch-agent/src/http/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@

export const CHAT_AGENT_ROUTE = "chat";

/** Cheap process-liveness probe; dependency readiness is established before listen. */
export const HEALTH_ROUTE = "/health";

/** Stock `DefaultChatTransport` endpoint used by Petrinaut's local panel. */
export const PETRINAUT_CHAT_ROUTE = "/api/chat";
24 changes: 24 additions & 0 deletions apps/brunch-agent/test/health.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Hono } from "hono";
import { expect, test, vi } from "vitest";

import { healthHandler } from "../src/health.ts";

test("health reports liveness without consulting dependencies", async () => {
const dependency = vi.fn<() => void>();
const app = new Hono();
app.get("/health", (context) => {
const response = healthHandler(context);
expect(dependency).not.toHaveBeenCalled();
return response;
});

const response = await app.request("/health");

expect(response.status).toBe(200);
expect(response.headers.get("cache-control")).toBe("no-store");
expect(response.headers.get("content-type")).toContain(
"application/health+json",
);
await expect(response.json()).resolves.toEqual({ status: "pass" });
expect(dependency).not.toHaveBeenCalled();
});
Loading