Skip to content
Open
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 @@ -18,6 +18,8 @@
# Petrinaut bundles its user-facing docs into the build via `?raw` imports,
# so they must be present in the Docker context (not just README.md).
!libs/@hashintel/petrinaut/docs/*.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
78 changes: 78 additions & 0 deletions apps/brunch-agent/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 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

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 3000

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to use another port, the frontend is already using 3000

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We agreed on port 3002 in slack.


HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["node", "-e", "fetch('http://127.0.0.1:3000/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