@tryhand/ai-gateway
v0.1.11
Published
Internal AI Gateway CLI, local collector, and agent hook bridge
Readme
AI Tracking Gateway
Core Mode implementation for docs/plans/proposal.md: local endpoint-side collection for Codex, Claude Code, and Cursor usage without requiring Team/Enterprise plans or routing model traffic through a provider proxy.
This is an internal company gateway, not a public product. It intentionally avoids product-style signup, billing, tenant self-service, and release/go-live ceremony. User identity and device credentials are managed locally by company tooling.
What Is Implemented
- Local Collector with:
GET /healthPOST /hooks/:agent/:eventPOST /policy/evaluatePOST /v1/logsfor OTLP HTTP JSON logs- offline queue and
POST /sync
- Gateway API with:
POST /api/v1/devices/registerPOST /api/v1/eventsPOST /api/v1/telemetry/health- device-scoped dashboard routes under
/api/v1/dashboard/device/* - temporary admin dashboard and device management routes under
/api/v1/admin/*
- PostgreSQL schema in
migrations/*.sql - Local prompt redaction and policy decisions before upload
- Explicit usage confidence through
usage.quality
Quick Start
For an employee workstation after this package is published to the private npm registry:
npm install -g @tryhand/ai-gateway
ai-gateway setup --gateway-url https://<gateway-host> --enrollment-token <company-enrollment-token>
ai-gateway pingsetup does the local onboarding work in one command:
- registers this machine to an owner email and writes a per-device token to
~/.ai-gateway/device.json - installs machine-level Codex/Cursor hooks
- installs Codex OTel config for account-email verification
- creates Windows collector autostart, using a login Scheduled Task plus a
5-minute watchdog task that health-checks the local collector and only starts
it when it is not already running; if Scheduled Task creation is denied, setup
falls back to the current user's
HKCU\...\Runentry - starts the local collector in the background
- runs the same verification checks as
ai-gateway ping
ping is the operator smoke test. It exits non-zero when device registration,
collector health, gateway health, hooks, OTel, or collector autostart is not
working.
During setup, the first Codex identity check may be reported as pending until
Codex Desktop emits its first OTel identity event; run ai-gateway ping after
opening or restarting Codex Desktop for the strict identity check.
For source checkout development:
npm install
Copy-Item .env.example .env
# Edit .env with DATABASE_URL and internal service settings.
npm run buildOnboard this workstation from source:
npm run ai-gateway -- setup --gateway-url http://127.0.0.1:8080 --enrollment-token <company-enrollment-token> --owner-email <[email protected]>
npm run ai-gateway -- pingsetup and register-device call the Gateway API and write a per-device token
to ~/.ai-gateway/device.json. Device registration requires an owner email from
--owner-email or AI_GATEWAY_OWNER_EMAIL. The owner is the employee
responsible for the machine. The observed AI account is verified from each agent
runtime: Cursor hook payloads provide user_email, and Codex uses the
identity-only OTel channel to provide user.email.
The machine owner and observed AI account are stored separately. If an employee switches AI accounts, uses a borrowed account, or the observed account differs from the registered machine owner, the gateway keeps the event and exposes the mismatch in the admin dashboard instead of rewriting ownership history.
For local-only development, register-device --token <token> still writes a
legacy local token, but shared DEVICE_TOKEN is not the recommended company
rollout path.
Apply the database migration:
npm run db:migrateData access convention:
- Use Kysely for typed application queries in
src/gateway-api/db.ts, especially straightforward lookups, inserts, updates, and transactional repository code. - Keep migrations as explicit SQL files under
migrations/. - Keep complex dashboard analytics as raw SQL when CTEs, window functions, token-source selection, or bigint aggregate behavior are clearer and easier to audit in PostgreSQL syntax. Preserve token accounting semantics when changing those queries.
Start Gateway API:
npm run ai-gateway -- serve-apiBy default the Gateway API binds to 127.0.0.1, so it is reachable only from
the local machine. Keep this default for source checkout development. For an
intentional LAN or central deployment, set GATEWAY_HOST to the deployment bind
address and set GATEWAY_CORS_ALLOWED_ORIGINS to the exact dashboard origins
that may call the API. Do not use a LAN bind as an implicit CORS allowlist.
For a central deployment, set:
ENROLLMENT_TOKEN=<secret used only for first-time device registration>
ADMIN_TOKEN=<secret used to view dashboard data until SSO/admin auth exists>Token roles:
ENROLLMENT_TOKENis setup-only. It can register a device, but it cannot ingest events or view dashboard data.- The per-device token issued by
/api/v1/devices/registeris machine auth. It authorizes ingest from that registered device and device-scoped dashboard routes for that device only. DEVICE_TOKENis a legacy/dev fallback for local testing. Do not use one sharedDEVICE_TOKENas the company rollout credential.ADMIN_TOKENis a temporary global admin fallback until real admin auth exists. Admin endpoints fail closed whenADMIN_TOKENis not configured.
Dashboard UI:
/dashboardredirects to/dashboard/device./dashboard/deviceis the device-scoped dashboard. Its login is an in-page form that accepts thetokenfield from~/.ai-gateway/device.json. Theidfield starts withdev_and identifies the device; it is not a login token. Pasting adev_*value shows a targeted correction in the form./dashboard/adminis the admin dashboard. Its separate login form acceptsADMIN_TOKENfrom the Gateway API environment.- Device and admin tokens are route-specific. A device token cannot authorize the admin dashboard, and an admin token is not the device dashboard token.
The React/Vite dashboard is built into dist/dashboard. Fastify serves the
built app for the dashboard UI paths and serves its generated static assets
under /dashboard/assets/.
Dashboard API routes:
- Device-scoped routes use the registered device token:
/api/v1/dashboard/device/summaryand/api/v1/dashboard/device/sessions. - Admin routes use
ADMIN_TOKEN:/api/v1/admin/dashboard/summary,/api/v1/admin/dashboard/sessions,/api/v1/admin/dashboard/token-status,/api/v1/admin/devices, and/api/v1/admin/dashboard/mismatches. - Legacy
/api/v1/dashboard/summary,/api/v1/dashboard/sessions, and/api/v1/dashboard/token-statusremain admin-authenticated compatibility routes.
Dashboard development commands:
# Start the Vite frontend development server (frontend-only development).
npm run dashboard:dev
# Build only the dashboard into dist/dashboard.
npm run dashboard:build
# Preview the built dashboard with Vite.
npm run dashboard:previewFor the implemented same-origin dashboard and API behavior, build the dashboard
and run Fastify, then open /dashboard/device or /dashboard/admin on the
configured Gateway API port:
npm run dashboard:build
npm run dev:apiThe full package build runs the backend TypeScript compiler and then the Vite dashboard build:
npm run buildStart Local Collector:
npm run ai-gateway -- serve-collectorThe collector uses a generated local shared secret to protect local control
endpoints. The token is stored in ~/.ai-gateway/config.json and can be
overridden with AI_GATEWAY_COLLECTOR_TOKEN when an operator needs to inject a
known non-empty value. The generated hook bridge and collector launcher read the
current token at runtime instead of storing a token copy in generated scripts.
Protected collector endpoints are /hooks/:agent/:event, /policy/evaluate,
/sync, and /telemetry/health. /health stays open for local health checks,
and /v1/logs stays unauthenticated for OTLP HTTP compatibility.
Check local setup:
npm run ai-gateway -- doctorInstall machine-level hooks for Codex and Cursor so every project can send hook events without per-repo .codex/config.toml or .cursor/hooks.json setup:
npm run ai-gateway -- init-machine --agents codex,cursor
npm run ai-gateway -- machine-hook-statusThe generated bridge preserves the original project working directory before entering this gateway runtime, so hook events from other repositories can still be attributed to the correct project.
For Codex, init-machine also installs an OpenTelemetry exporter used only to
verify the signed-in Codex account email against ALLOWED_EMAIL_DOMAINS. Codex
OTel payloads are not stored as usage events.
Clean up machine-level gateway config and remove the global package:
ai-gateway uninstall-packageCurrent npm versions do not run package uninstall lifecycle scripts during
npm uninstall, so raw npm uninstall -g @tryhand/ai-gateway only removes the
package files. Use ai-gateway uninstall-package when you want gateway cleanup
and npm package removal in one step.
Project identity prefers the normalized Git remote when it can be detected.
HTTPS, SSH, and git-protocol clones of the same repository normalize to the same
project identity, for example https://github.com/org/repo.git and
[email protected]:org/repo.git. If no usable remote is available, the collector
falls back to workspace path and project name identity.
Send an end-to-end smoke event through the collector:
npm run ai-gateway -- test-eventSync Codex session JSONL usage into the gateway:
npm run ai-gateway -- sync-codex-jsonlThis reads $CODEX_HOME/sessions, $CODEX_HOME/archived_sessions, or the default ~/.codex paths. Parser state is stored at ~/.ai-gateway/codex-jsonl-state.json. The sync only emits events when token usage fields are present; metadata-only session lines are skipped so the dashboard does not show fake token usage.
If an earlier parser run created unknown Codex JSONL rows, clean them up with:
npm run ai-gateway -- cleanup-codex-jsonl-unknownThe .env file should not contain per-user identity or access rules. Configure company AI access policy in config/access-policy.json:
{
"version": 1,
"allowedEmailDomains": ["tryhand.tech"],
"defaultPolicy": "allow_domain"
}The checked-in default policy allows verified tryhand.tech accounts with
defaultPolicy: "allow_domain". Copy config/access-policy.example.json to
config/access-policy.json for local changes, and set ACCESS_POLICY_PATH in
.env only when the policy file lives outside the default path. With
defaultPolicy: "allow_domain", any verified account from
allowedEmailDomains is allowed. Use defaultPolicy: "block" to fail closed
until the domain policy is explicitly changed.
Hook Bridge
Agent hooks should call the bridge command and pass their hook JSON on stdin:
npm run ai-gateway-hook-bridge -- codex user-promptThe bridge posts to:
http://127.0.0.1:17654/hooks/<agent>/<event>Exit behavior:
0: collector allowed/warned and accepted the event.2: collector blocked the event or could not accept it.
Optional hook debug logging is off by default. When enabled, it only records
user-prompt and session-stop hook flows, not tool/edit/shell events. This keeps
debug output focused on the retained lifecycle events. AI_GATEWAY_HOOK_DEBUG=1
writes redacted records with a payload summary by default.
AI_GATEWAY_HOOK_DEBUG_RAW=1 is a sensitive opt-in for raw payload capture and
should only be used during tightly scoped local troubleshooting. Prefer writing
per-agent debug files with AI_GATEWAY_HOOK_DEBUG_DIR; if only
AI_GATEWAY_HOOK_DEBUG_PATH is configured, all debug records are written to the
legacy single JSONL file.
AI_GATEWAY_HOOK_DEBUG=1
AI_GATEWAY_HOOK_DEBUG_RAW=0
AI_GATEWAY_HOOK_DEBUG_DIR=.data/hook-debug
AI_GATEWAY_HOOK_DEBUG_PATH=.data/hook-debug.jsonlWith AI_GATEWAY_HOOK_DEBUG_DIR=.data/hook-debug, records are written to
.data/hook-debug/codex.jsonl, .data/hook-debug/cursor.jsonl, or
.data/hook-debug/claude.jsonl.
Do not write debug logs to stdout; agent hook stdout is reserved for the hook protocol response.
If the offline collector queue contains invalid records, they are moved to the
quarantine file next to the queue. With the default COLLECTOR_QUEUE_PATH, that
location is .data/collector-queue.jsonl.quarantine.jsonl; in general it is
<collectorQueuePath>.quarantine.jsonl.
Cursor Quick Start
Cursor Core Mode tracks lifecycle, prompt, tool, and edit events through hooks. It does not proxy Cursor model traffic. Policy handling for Cursor Core Mode is audit/reporting oriented in this MVP. The collector records policy decisions from Cursor hook events, but Cursor runtime fail-closed blocking is not claimed unless Cursor-specific hook output and exit-code behavior is verified against the Cursor runtime.
Install Cursor hooks on this workstation:
npm run ai-gateway -- install-hooks --agents cursorCheck installed hook state:
npm run ai-gateway -- hook-statusRun Cursor-specific preflight checks:
npm run ai-gateway -- preflight --agent cursorSend a Cursor smoke event to the local collector:
npm run ai-gateway -- test-cursor-eventCursor token usage is not billing-grade in the MVP. Cursor sessions remain estimated or unknown unless the hook payload includes token fields that can be normalized.
Codex OTel
Install project-local Codex OTel config:
npm run ai-gateway -- install-otel --agent codexCheck OTel config and latest ingested OTel event:
npm run ai-gateway -- otel-statusSend a sample OTLP HTTP JSON log through the local collector:
npm run ai-gateway -- test-otelSuccessful setup should make /api/v1/dashboard/summary show usage_quality = otel_reported with non-zero tokens.
Example Hook Event
Invoke-RestMethod `
-Method Post `
-Uri http://127.0.0.1:17654/hooks/codex/user-prompt `
-ContentType application/json `
-Body '{"prompt":"Review this repository","session_id":"ses_demo","model":"gpt-5","cwd":"D:\\tryhand-project\\ai-tracking-gateway"}'Architecture
The request path to AI providers stays unchanged. The collector receives telemetry and hooks locally, redacts sensitive content, evaluates local policy, queues events if offline, then syncs to the Gateway API.
Cursor MITM/protobuf decoding is intentionally not part of the MVP. It should remain a future opt-in experimental adapter after Core Mode has stable telemetry health and policy reporting.
