feat: add new skills and reviewers for architecture checks, README drift, and security audits
This commit is contained in:
parent
685b6e2054
commit
5c08e69bcb
50
.claude/agents/hexagonal-layer-reviewer.md
Normal file
50
.claude/agents/hexagonal-layer-reviewer.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
name: hexagonal-layer-reviewer
|
||||||
|
description: Use proactively after changes to internal/core, internal/app, or internal/adapters in ha-gateway, ai-gateway, or discord-bot to check that hexagonal dependency direction wasn't violated. Also invoke on request ("check layering", "did I break the architecture").
|
||||||
|
tools: Read, Grep, Glob, Bash
|
||||||
|
---
|
||||||
|
|
||||||
|
You review Go import graphs in this repo against its hexagonal architecture rule:
|
||||||
|
**dependencies point inward only.**
|
||||||
|
|
||||||
|
```
|
||||||
|
adapters/primary ─┐
|
||||||
|
adapters/secondary ─┼─→ app ─→ core/domain
|
||||||
|
│ core/ports (driving, driven)
|
||||||
|
cmd/<entrypoint> ──┘ (wiring only — allowed to import everything)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules to check
|
||||||
|
|
||||||
|
1. **`internal/core/domain` and `internal/core/ports`** must not import anything from
|
||||||
|
`internal/app` or `internal/adapters`. These packages define the domain and the
|
||||||
|
interfaces — they should have almost no internal imports at all.
|
||||||
|
2. **`internal/app`** must not import `internal/adapters/*` directly. It may only depend
|
||||||
|
on `internal/core/domain` and `internal/core/ports`. Adapters are injected as
|
||||||
|
interface values (driven ports) through constructors — `app` should never construct
|
||||||
|
or reference a concrete adapter type.
|
||||||
|
3. **`internal/adapters/primary/*`** (gRPC servers, Discord handlers) may import
|
||||||
|
`internal/app` and `internal/core`, but should not import
|
||||||
|
`internal/adapters/secondary/*` directly — that dependency should flow through `app`.
|
||||||
|
4. **`internal/adapters/secondary/*`** should only implement `core/ports/driven`
|
||||||
|
interfaces and depend on `core/domain`; it should not import `internal/app` or
|
||||||
|
`internal/adapters/primary/*`.
|
||||||
|
5. Only `cmd/<entrypoint>/main.go` is allowed to import across all of the above to wire
|
||||||
|
the dependency graph together.
|
||||||
|
|
||||||
|
## How to check
|
||||||
|
|
||||||
|
For each changed `.go` file (or the whole service if asked generally):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -n "gitea.nik4nao.com/nik/home-services/<service>/internal" <file>
|
||||||
|
```
|
||||||
|
|
||||||
|
Compare the importing package's path against the rules above. `go list -deps` can also
|
||||||
|
confirm a suspicious transitive dependency if a direct grep is ambiguous.
|
||||||
|
|
||||||
|
## Report format
|
||||||
|
|
||||||
|
List violations as `file:line — imports X from Y, violates rule <n>`. If a file being
|
||||||
|
reviewed is `cmd/**/main.go`, skip it — wiring code is exempt by design. If nothing
|
||||||
|
violates the rules, say so in one line; don't enumerate every clean import.
|
||||||
43
.claude/agents/internal-exposure-reviewer.md
Normal file
43
.claude/agents/internal-exposure-reviewer.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
name: internal-exposure-reviewer
|
||||||
|
description: Use proactively when changes touch gRPC server setup, TLS_DIR/mTLS handling, listen addresses, reflection/health registration, or anything handling HA_TOKEN/DISCORD_TOKEN, across ha-gateway, ai-gateway, or discord-bot. Also invoke on request for a security pass before deploying a service change.
|
||||||
|
tools: Read, Grep, Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
You review changes against this repo's one documented, repo-wide security limitation:
|
||||||
|
**none of the three services (`ha-gateway`, `ai-gateway`, `discord-bot`) implement
|
||||||
|
app-layer authorization.** Every README says the same thing almost verbatim — they rely
|
||||||
|
entirely on staying on a trusted internal network, or on mTLS via `TLS_DIR`. Your job is
|
||||||
|
to catch anything that quietly widens what's reachable or weakens that boundary.
|
||||||
|
|
||||||
|
## What to look for
|
||||||
|
|
||||||
|
1. **New or changed listen bindings.** Any change to how `GRPC_PORT` is bound — flag
|
||||||
|
binding to `0.0.0.0` or an external interface where it previously bound loopback-only,
|
||||||
|
or removing the existing bind logic entirely.
|
||||||
|
2. **`TLS_DIR` / mTLS handling.** Any change to the code paths in
|
||||||
|
`internal/adapters/primary/grpc/interceptor.go` or server setup in `cmd/*/main.go`
|
||||||
|
that reads `TLS_DIR` — flag anything that makes mTLS optional where it was previously
|
||||||
|
required, weakens client cert verification, or adds a plaintext fallback that wasn't
|
||||||
|
there before.
|
||||||
|
3. **Reflection and health registration.** `ha-gateway` always registers gRPC reflection;
|
||||||
|
`ai-gateway` only does so when `LOG_LEVEL=debug`. Flag any change that registers
|
||||||
|
reflection unconditionally in `ai-gateway`, or exposes new debug-only surface without
|
||||||
|
a similar guard.
|
||||||
|
4. **New RPCs that skip the interceptor chain.** Every RPC should go through the same
|
||||||
|
auth/telemetry interceptor setup as existing ones — flag a new service registration
|
||||||
|
that bypasses `interceptor.go`.
|
||||||
|
5. **Token handling.** `HA_TOKEN` (ha-gateway), `DISCORD_TOKEN` (discord-bot), and any
|
||||||
|
Ollama credentials should never be logged, echoed back in a gRPC response/error
|
||||||
|
message, or written anywhere outside `internal/config`. Flag any `log.*`/`slog.*` call
|
||||||
|
or returned error that could include a token value, and any new field that echoes
|
||||||
|
request-supplied credentials back to the caller.
|
||||||
|
6. **New inbound endpoints without corresponding docs.** If a new RPC is genuinely
|
||||||
|
internet- or cross-trust-boundary-facing (not just internal service-to-service),
|
||||||
|
flag that this repo's model assumes a trusted network and ask whether that still
|
||||||
|
holds.
|
||||||
|
|
||||||
|
## Report format
|
||||||
|
|
||||||
|
List findings as `file:line — <what changed> — <why it matters>`, ordered most severe
|
||||||
|
first. If nothing in the diff touches these areas, say so in one line.
|
||||||
19
.claude/settings.json
Normal file
19
.claude/settings.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Edit|Write",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "jq -r '.tool_input.file_path // empty' | { read -r f; if [[ \"$f\" == *.env && \"$f\" != *.env.example ]]; then echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Refusing to edit a real .env file - it holds live secrets (HA_TOKEN, DISCORD_TOKEN). Edit the corresponding .env.example instead.\"}}'; fi; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "jq -r '.tool_input.file_path // empty' | { read -r f; if [[ \"$f\" == */gen/* ]]; then echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"gen/ is committed buf-generated protobuf/gRPC code. Edit the source .proto file under proto/ and run buf generate instead.\"}}'; fi; }"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
68
.claude/skills/add-rpc/SKILL.md
Normal file
68
.claude/skills/add-rpc/SKILL.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
name: add-rpc
|
||||||
|
description: Scaffold a new gRPC RPC end-to-end (proto, buf generate, hexagonal ports/app/adapter layers, tests, README) following this repo's established pattern across ha-gateway, ai-gateway, and discord-bot.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Add a gRPC RPC
|
||||||
|
|
||||||
|
Use this whenever a new RPC or field is being added to `ha-gateway`, `ai-gateway`, or a new
|
||||||
|
gRPC client call is being added to `discord-bot`. All three services share the same
|
||||||
|
hexagonal layering, so follow this order — do not skip layers or reach across them.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. **Identify the target service and proto package.**
|
||||||
|
- `ha-gateway` → `proto/ha/v1/*.proto` (packages: `entity`, `light`, `switch`, `event`, `common`)
|
||||||
|
- `ai-gateway` → `proto/ai/v1/ai.proto`
|
||||||
|
- `discord-bot` has no proto of its own; it only consumes `ha.v1` / `ai.v1` clients.
|
||||||
|
|
||||||
|
2. **Edit the `.proto` file** under `proto/<pkg>/v1/`. Add the message/RPC there first —
|
||||||
|
it is the source of truth; `gen/` is derived from it.
|
||||||
|
|
||||||
|
3. **Regenerate generated code:**
|
||||||
|
```bash
|
||||||
|
buf generate
|
||||||
|
```
|
||||||
|
This writes into `gen/<pkg>/v1/`. Never hand-edit files under `gen/`.
|
||||||
|
|
||||||
|
4. **Extend the port interface** in `internal/core/ports/`:
|
||||||
|
- `ports/driving/*.go` — the interface the gRPC adapter calls *into* (app-facing, inbound).
|
||||||
|
- `ports/driven/*.go` — the interface the app calls *out* to (HA REST, Ollama, ha-gateway
|
||||||
|
client, etc.), if the new RPC needs a new external call.
|
||||||
|
- `core/domain/*.go` — add/extend domain types here, not in adapter or proto types.
|
||||||
|
|
||||||
|
5. **Implement orchestration in `internal/app/`.** This is where business logic and
|
||||||
|
validation live — it depends only on `core/domain` and `core/ports`, never on
|
||||||
|
`adapters/*` types directly. Look at an existing file (e.g. `ha-gateway/internal/app/light.go`)
|
||||||
|
for the shape: a struct holding driven-port dependencies, constructed via `NewXApp(...)`.
|
||||||
|
|
||||||
|
6. **Wire the primary adapter** in `internal/adapters/primary/grpc/<feature>.go`. Implement
|
||||||
|
the generated server interface method, translate between proto types and domain types
|
||||||
|
(see `mapping.go` in ha-gateway for the existing translation pattern), and call the app
|
||||||
|
layer. Keep proto <-> domain conversion here, not in `app`.
|
||||||
|
|
||||||
|
7. **If the RPC needs a new outbound call**, implement it in the relevant
|
||||||
|
`internal/adapters/secondary/<name>/client.go`, implementing the `driven` port interface
|
||||||
|
from step 4.
|
||||||
|
|
||||||
|
8. **Add a test** using the repo's convention: plain `testing` package (no testify), with a
|
||||||
|
hand-written mock struct that implements the driven port interface using function fields
|
||||||
|
(see `ha-gateway/internal/app/entity_test.go` or `ai-gateway/internal/app/query_test.go`
|
||||||
|
for the pattern). Table-driven `t.Run` subtests are the norm.
|
||||||
|
|
||||||
|
9. **Update the service's `README.md`:**
|
||||||
|
- Move the RPC between "Implemented" and "Stubbed" in the gRPC API section if its status
|
||||||
|
changed.
|
||||||
|
- Add any new environment variables to the Configuration table (and to `.env.example`).
|
||||||
|
- Add any new package to the Package Map section if you created one.
|
||||||
|
|
||||||
|
10. **Verify:**
|
||||||
|
```bash
|
||||||
|
cd <service> && go vet ./... && go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layering rule to enforce
|
||||||
|
|
||||||
|
Dependencies point inward only: `adapters` → `app` → `core`. Never import
|
||||||
|
`internal/adapters/*` from `internal/app` or `internal/core`. Only `cmd/<entrypoint>/main.go`
|
||||||
|
is allowed to import across all layers to wire dependencies together.
|
||||||
58
.claude/skills/readme-drift-check/SKILL.md
Normal file
58
.claude/skills/readme-drift-check/SKILL.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
name: readme-drift-check
|
||||||
|
description: Audit each service README (ha-gateway, ai-gateway, discord-bot) against the actual code — env var tables vs config.go, Implemented/Stubbed RPC lists vs proto and adapter code, and Package Map vs the real directory tree. Reports drift without editing anything.
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# README Drift Check
|
||||||
|
|
||||||
|
This repo documents contracts by hand in three READMEs (`ha-gateway/README.md`,
|
||||||
|
`ai-gateway/README.md`, `discord-bot/README.md`), and those have gone stale before
|
||||||
|
(e.g. `OLLAMA_TIMEOUT`'s default needed a manual doc update after a config change). This
|
||||||
|
skill finds the next drift before it ships. **Read-only** — report findings, do not edit
|
||||||
|
README or code unless the user asks you to apply a fix afterward.
|
||||||
|
|
||||||
|
## Checks, per service
|
||||||
|
|
||||||
|
### 1. Environment variable table vs `internal/config/config.go`
|
||||||
|
|
||||||
|
- Read the Configuration table in the service's README.
|
||||||
|
- Read `internal/config/config.go` and find every env var read (`os.Getenv`,
|
||||||
|
`os.LookupEnv`, or equivalent helper) plus its default.
|
||||||
|
- Flag: vars in code missing from the README table, vars in the README table no longer
|
||||||
|
read in code, and default values that don't match.
|
||||||
|
|
||||||
|
### 2. Implemented / Stubbed RPC list vs actual server code
|
||||||
|
|
||||||
|
- Read the gRPC API section's Implemented/Stubbed split in the README.
|
||||||
|
- Read the corresponding `proto/<pkg>/v1/*.proto` for the full RPC list.
|
||||||
|
- Read `internal/adapters/primary/grpc/*.go` and check each method body: a method that
|
||||||
|
just returns `status.Errorf(codes.Unimplemented, ...)` (or equivalent) is Stubbed;
|
||||||
|
anything else is Implemented.
|
||||||
|
- Flag: any RPC whose README status doesn't match what the code actually does, and any
|
||||||
|
RPC in the proto missing from the README entirely.
|
||||||
|
|
||||||
|
### 3. Package Map vs real directory tree
|
||||||
|
|
||||||
|
- Read the Package Map section in the README.
|
||||||
|
- Run `find <service>/internal -type d | sort` (and `cmd/`) and compare.
|
||||||
|
- Flag: directories that exist but aren't documented, and documented paths that no longer
|
||||||
|
exist.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Report per service, grouped by check, e.g.:
|
||||||
|
|
||||||
|
```
|
||||||
|
## ha-gateway
|
||||||
|
### Env vars
|
||||||
|
- MISSING FROM README: `NEW_VAR` (default "x") — read in config.go:42
|
||||||
|
### RPC status
|
||||||
|
- README says `SwitchService.TurnOn` is Stubbed, but internal/adapters/primary/grpc/switch.go:18
|
||||||
|
now implements it — README is stale.
|
||||||
|
### Package map
|
||||||
|
- OK
|
||||||
|
```
|
||||||
|
|
||||||
|
If everything matches for a service, say so briefly rather than listing every checked
|
||||||
|
item — don't pad the report with confirmations of things that are fine.
|
||||||
117
AGENTS.md
Normal file
117
AGENTS.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Repo Overview
|
||||||
|
|
||||||
|
`home-services` is a Go workspace of three internal services for home control, connected by gRPC and sharing committed protobuf-generated code:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Discord users
|
||||||
|
|
|
||||||
|
v
|
||||||
|
discord-bot -----> ha-gateway -----> Home Assistant REST API
|
||||||
|
|
|
||||||
|
v
|
||||||
|
ai-gateway ------> Ollama
|
||||||
|
|
|
||||||
|
v
|
||||||
|
ha-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
- **ha-gateway** (port `50051`) — gRPC boundary for Home Assistant. Talks to HA's REST API; implements entity state and light control/discovery and switch discovery; switch control and event streaming are stubbed.
|
||||||
|
- **ai-gateway** (port `50052`) — gRPC service that turns free-form text into home actions. Calls Ollama for intent extraction, resolves intents against a cached light list from `ha-gateway`, and calls `ha-gateway` to execute approved actions.
|
||||||
|
- **discord-bot** — registers `/light`, `/switch`, `/ai` slash commands and calls `ha-gateway`/`ai-gateway` via gRPC clients.
|
||||||
|
|
||||||
|
Each service is a separate Go module (own `go.mod`) joined by `go.work` at the root, plus a `gen` module for shared generated code. Module paths are `gitea.nik4nao.com/nik/home-services/{ha-gateway,ai-gateway,discord-bot,gen}`.
|
||||||
|
|
||||||
|
## Architecture (hexagonal, per service)
|
||||||
|
|
||||||
|
Every service follows the same internal layout, with dependencies pointing inward:
|
||||||
|
|
||||||
|
```text
|
||||||
|
cmd/<entrypoint>/ # process entrypoint and wiring (loads .env, builds adapters, starts gRPC)
|
||||||
|
internal/adapters/primary/ # inbound edges: gRPC servers, Discord handlers
|
||||||
|
internal/adapters/secondary/ # outbound edges: HA REST client, Ollama client, ha-gateway/ai-gateway gRPC clients
|
||||||
|
internal/app/ # use-case orchestration
|
||||||
|
internal/core/domain/ # domain types
|
||||||
|
internal/core/ports/ # driving (inbound) and driven (outbound) interfaces
|
||||||
|
internal/config/ # environment loading
|
||||||
|
internal/logger/ # slog setup
|
||||||
|
internal/telemetry/ # OpenTelemetry setup
|
||||||
|
```
|
||||||
|
|
||||||
|
`internal/core` has no dependency on adapters — ports are interfaces that adapters implement (driven) or call into (driving). When adding a capability, the usual path is: define/extend a port in `core/ports`, implement orchestration in `app`, then wire an adapter in `adapters/primary` or `adapters/secondary`.
|
||||||
|
|
||||||
|
Protobuf contracts live in `proto/` (buf module, `ai/v1` and `ha/v1` packages). Generated Go code is committed under `gen/` and consumed by all three services through the Go workspace — do not hand-edit files in `gen/`.
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
Regenerate protobuf code after changing anything under `proto/` (requires `buf`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
buf generate
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests / vet (from repo root, or `cd` into a service and drop the prefix):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./ha-gateway/... ./ai-gateway/... ./discord-bot/...
|
||||||
|
go vet ./ha-gateway/... ./ai-gateway/... ./discord-bot/...
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a single test:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ha-gateway && go test ./internal/app/... -run TestEntityAppGetState
|
||||||
|
```
|
||||||
|
|
||||||
|
Build binaries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build ./ha-gateway/... ./ai-gateway/... ./discord-bot/...
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a service locally (each loads `.env` from its own working directory via `godotenv`, so `cd` into the service dir first):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ha-gateway && cp .env.example .env && go run ./cmd/gateway
|
||||||
|
cd ai-gateway && cp .env.example .env && go run ./cmd/gateway
|
||||||
|
cd discord-bot && cp .env.example .env && go run ./cmd/bot
|
||||||
|
```
|
||||||
|
|
||||||
|
For local plaintext dev, point gateways at each other with `TLS_DIR` empty, e.g. `HA_GATEWAY_ADDR=localhost:50051`, `AI_GATEWAY_ADDR=localhost:50052`.
|
||||||
|
|
||||||
|
Build container images (from repo root, since Dockerfiles reference the whole workspace):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f ha-gateway/Dockerfile -t ha-gateway:dev .
|
||||||
|
docker build -f ai-gateway/Dockerfile -t ai-gateway:dev .
|
||||||
|
docker build -f discord-bot/Dockerfile -t discord-bot:dev .
|
||||||
|
```
|
||||||
|
|
||||||
|
gRPC smoke checks (with the relevant service running):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grpcurl -plaintext -d '{"domain":"light"}' localhost:50051 ha.v1.EntityService/ListStates
|
||||||
|
grpcurl -plaintext -d '{"entity_id":"light.living_room","brightness_pct":80}' localhost:50051 ha.v1.LightService/TurnOn
|
||||||
|
grpcurl -plaintext -d '{"text":"turn on the desk lamp","source":"local"}' localhost:50052 ai.v1.AIService/Query
|
||||||
|
grpcurl -plaintext -d '{}' localhost:50052 ai.v1.AIService/ListModels
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `ha-gateway` always registers gRPC reflection; `ai-gateway` only registers reflection when `LOG_LEVEL=debug`.
|
||||||
|
|
||||||
|
## Testing Conventions
|
||||||
|
|
||||||
|
Tests use the standard library `testing` package only (no testify). Mocks are hand-written structs implementing the relevant `core/ports/driven` interface with function fields (see `ha-gateway/internal/app/entity_test.go` for the pattern).
|
||||||
|
|
||||||
|
## Configuration Notes
|
||||||
|
|
||||||
|
- Every service reads `TLS_DIR` to enable optional mTLS; when set, the directory must contain `tls.crt`, `tls.key`, and `ca.crt`.
|
||||||
|
- `OTEL_ENDPOINT` enables OTLP gRPC traces/metrics; leave empty for local no-op telemetry.
|
||||||
|
- `LOG_FORMAT=json` is the production default; `text` is easier to read locally.
|
||||||
|
- None of the services implement app-layer authorization — they rely on being kept on a trusted internal network or on mTLS. Keep this in mind before adding any endpoint that wasn't previously reachable.
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
`.gitea/workflows/ci.yaml` runs `go vet` and `go test` for all four modules (`gen`, `ai-gateway`, `ha-gateway`, `discord-bot`), then builds and pushes Docker images for the three services on pushes to `main`.
|
||||||
117
CLAUDE.md
Normal file
117
CLAUDE.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Repo Overview
|
||||||
|
|
||||||
|
`home-services` is a Go workspace of three internal services for home control, connected by gRPC and sharing committed protobuf-generated code:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Discord users
|
||||||
|
|
|
||||||
|
v
|
||||||
|
discord-bot -----> ha-gateway -----> Home Assistant REST API
|
||||||
|
|
|
||||||
|
v
|
||||||
|
ai-gateway ------> Ollama
|
||||||
|
|
|
||||||
|
v
|
||||||
|
ha-gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
- **ha-gateway** (port `50051`) — gRPC boundary for Home Assistant. Talks to HA's REST API; implements entity state and light control/discovery and switch discovery; switch control and event streaming are stubbed.
|
||||||
|
- **ai-gateway** (port `50052`) — gRPC service that turns free-form text into home actions. Calls Ollama for intent extraction, resolves intents against a cached light list from `ha-gateway`, and calls `ha-gateway` to execute approved actions.
|
||||||
|
- **discord-bot** — registers `/light`, `/switch`, `/ai` slash commands and calls `ha-gateway`/`ai-gateway` via gRPC clients.
|
||||||
|
|
||||||
|
Each service is a separate Go module (own `go.mod`) joined by `go.work` at the root, plus a `gen` module for shared generated code. Module paths are `gitea.nik4nao.com/nik/home-services/{ha-gateway,ai-gateway,discord-bot,gen}`.
|
||||||
|
|
||||||
|
## Architecture (hexagonal, per service)
|
||||||
|
|
||||||
|
Every service follows the same internal layout, with dependencies pointing inward:
|
||||||
|
|
||||||
|
```text
|
||||||
|
cmd/<entrypoint>/ # process entrypoint and wiring (loads .env, builds adapters, starts gRPC)
|
||||||
|
internal/adapters/primary/ # inbound edges: gRPC servers, Discord handlers
|
||||||
|
internal/adapters/secondary/ # outbound edges: HA REST client, Ollama client, ha-gateway/ai-gateway gRPC clients
|
||||||
|
internal/app/ # use-case orchestration
|
||||||
|
internal/core/domain/ # domain types
|
||||||
|
internal/core/ports/ # driving (inbound) and driven (outbound) interfaces
|
||||||
|
internal/config/ # environment loading
|
||||||
|
internal/logger/ # slog setup
|
||||||
|
internal/telemetry/ # OpenTelemetry setup
|
||||||
|
```
|
||||||
|
|
||||||
|
`internal/core` has no dependency on adapters — ports are interfaces that adapters implement (driven) or call into (driving). When adding a capability, the usual path is: define/extend a port in `core/ports`, implement orchestration in `app`, then wire an adapter in `adapters/primary` or `adapters/secondary`.
|
||||||
|
|
||||||
|
Protobuf contracts live in `proto/` (buf module, `ai/v1` and `ha/v1` packages). Generated Go code is committed under `gen/` and consumed by all three services through the Go workspace — do not hand-edit files in `gen/`.
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
Regenerate protobuf code after changing anything under `proto/` (requires `buf`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
buf generate
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests / vet (from repo root, or `cd` into a service and drop the prefix):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./ha-gateway/... ./ai-gateway/... ./discord-bot/...
|
||||||
|
go vet ./ha-gateway/... ./ai-gateway/... ./discord-bot/...
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a single test:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ha-gateway && go test ./internal/app/... -run TestEntityAppGetState
|
||||||
|
```
|
||||||
|
|
||||||
|
Build binaries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build ./ha-gateway/... ./ai-gateway/... ./discord-bot/...
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a service locally (each loads `.env` from its own working directory via `godotenv`, so `cd` into the service dir first):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ha-gateway && cp .env.example .env && go run ./cmd/gateway
|
||||||
|
cd ai-gateway && cp .env.example .env && go run ./cmd/gateway
|
||||||
|
cd discord-bot && cp .env.example .env && go run ./cmd/bot
|
||||||
|
```
|
||||||
|
|
||||||
|
For local plaintext dev, point gateways at each other with `TLS_DIR` empty, e.g. `HA_GATEWAY_ADDR=localhost:50051`, `AI_GATEWAY_ADDR=localhost:50052`.
|
||||||
|
|
||||||
|
Build container images (from repo root, since Dockerfiles reference the whole workspace):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f ha-gateway/Dockerfile -t ha-gateway:dev .
|
||||||
|
docker build -f ai-gateway/Dockerfile -t ai-gateway:dev .
|
||||||
|
docker build -f discord-bot/Dockerfile -t discord-bot:dev .
|
||||||
|
```
|
||||||
|
|
||||||
|
gRPC smoke checks (with the relevant service running):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grpcurl -plaintext -d '{"domain":"light"}' localhost:50051 ha.v1.EntityService/ListStates
|
||||||
|
grpcurl -plaintext -d '{"entity_id":"light.living_room","brightness_pct":80}' localhost:50051 ha.v1.LightService/TurnOn
|
||||||
|
grpcurl -plaintext -d '{"text":"turn on the desk lamp","source":"local"}' localhost:50052 ai.v1.AIService/Query
|
||||||
|
grpcurl -plaintext -d '{}' localhost:50052 ai.v1.AIService/ListModels
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `ha-gateway` always registers gRPC reflection; `ai-gateway` only registers reflection when `LOG_LEVEL=debug`.
|
||||||
|
|
||||||
|
## Testing Conventions
|
||||||
|
|
||||||
|
Tests use the standard library `testing` package only (no testify). Mocks are hand-written structs implementing the relevant `core/ports/driven` interface with function fields (see `ha-gateway/internal/app/entity_test.go` for the pattern).
|
||||||
|
|
||||||
|
## Configuration Notes
|
||||||
|
|
||||||
|
- Every service reads `TLS_DIR` to enable optional mTLS; when set, the directory must contain `tls.crt`, `tls.key`, and `ca.crt`.
|
||||||
|
- `OTEL_ENDPOINT` enables OTLP gRPC traces/metrics; leave empty for local no-op telemetry.
|
||||||
|
- `LOG_FORMAT=json` is the production default; `text` is easier to read locally.
|
||||||
|
- None of the services implement app-layer authorization — they rely on being kept on a trusted internal network or on mTLS. Keep this in mind before adding any endpoint that wasn't previously reachable.
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
`.gitea/workflows/ci.yaml` runs `go vet` and `go test` for all four modules (`gen`, `ai-gateway`, `ha-gateway`, `discord-bot`), then builds and pushes Docker images for the three services on pushes to `main`.
|
||||||
Loading…
x
Reference in New Issue
Block a user