Skip to content

Commit ba5f2ea

Browse files
feat: add validation framework with per-user API key auth
Adds the Altimate validation framework to altimate-code. CLI commands: - `altimate-code validate configure --api-key <key>` — registers API key with validation server, saves to ~/.altimate-code/settings.json - `altimate-code validate install` — manually install /validate skill - `altimate-code validate status` — check installation state Auto-setup on every InstanceBootstrap (startup): - Writes SKILL.md + batch_validate.py to ~/.claude/skills/validate/ - Writes logger hook to ~/.claude/hooks/altimate_logger_hook.py - Registers hook in ~/.claude/settings.json under hooks.Stop - Uses bare identifier references (declare const) for build-time define constants so Bun's bundler correctly inlines asset content into the binary Conversation logger (conversation-logger.ts): - Subscribes to SessionStatus idle events after each turn - Posts user prompt, tool calls, and assistant response to /log-conversation - Fire-and-forget — never blocks the session - Disabled via ALTIMATE_LOGGER_DISABLED=true /validate skill routing: - session.ts resolvePrompt() intercepts prompts starting with /validate and dispatches as SessionPrompt.command instead of a normal chat turn Server-side two-tier auth (validationframework): - Static shared token for /log-conversation (write-only logging) - Per-user registered API keys for /validate* endpoints - POST /auth/register adds api_key to registered_keys.json flat list Docs: - README: Validation section covering what it does, setup steps, natural language usage examples, and what happens without configuration - docs/docs/configure/logging.md: detailed data collection reference Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 499f14b commit ba5f2ea

File tree

12 files changed

+1809
-266
lines changed

12 files changed

+1809
-266
lines changed

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,64 @@ Full docs at **[altimate.ai](https://altimate.ai)**.
157157
- [Agent Modes](https://altimate.ai/data-engineering/agent-modes/)
158158
- [Configuration](https://altimate.ai/configure/model-providers/)
159159
160+
## Validation
161+
162+
The `/validate` skill lets you audit past AI agent sessions against a set of quality criteria — checking whether the agent's reasoning, tool calls, and final response were correct, grounded, and complete. It pulls conversation traces from the backend, runs them through an evaluation pipeline, and reports per-criterion pass/fail results with details.
163+
164+
You can validate:
165+
- **A single trace**: `/validate <trace_id> or /validate the trace <trace-id>`
166+
- **All traces in a session**: `/validate --session-id <session_id> or /validate all the traces in session id <session-id>`
167+
- **A date range for a user**: `/validate --from <datetime> --to <datetime> --user-id <user_id> or /validate for user id <user id> for <relative duration>/ from <start date time> to <end date time>`
168+
169+
### Setup
170+
171+
**1. Register your API key**
172+
173+
```bash
174+
altimate-code validate configure --api-key <your-key>
175+
```
176+
177+
The api key is got from your altimate account API KEY.
178+
179+
**2. That's it** — the skill files are installed automatically the next time you start `altimate-code`.
180+
181+
To verify the installation:
182+
183+
```bash
184+
altimate-code validate status
185+
```
186+
187+
To install manually without restarting:
188+
189+
```bash
190+
altimate-code validate install
191+
```
192+
193+
### What happens if you skip configuration
194+
195+
If you run `/validate` without configuring an API key first, the validation script will exit immediately with:
196+
197+
```
198+
ERROR: Altimate credentials not found.
199+
Run: altimate validate configure --api-key <key>
200+
```
201+
202+
No traces will be validated and nothing will be written. You must run `altimate-code validate configure` at least once before using the skill.
203+
204+
## Data Collection
205+
206+
Altimate Code logs conversation turns (prompt, tool calls, and assistant response) to improve validation quality and agent behavior. Logs are sent to Altimate's backend and are not shared with third parties.
207+
208+
**To opt out:**
209+
210+
```bash
211+
export ALTIMATE_LOGGER_DISABLED=true
212+
```
213+
214+
Add it to your shell profile (`~/.zshrc`, `~/.bashrc`) to make it permanent.
215+
216+
See [`docs/docs/configure/logging.md`](docs/docs/configure/logging.md) for details on what is collected.
217+
160218
## Community & Contributing
161219
162220
- **Issues**: [GitHub Issues](https://github.com/AltimateAI/altimate-code/issues)

bun.lock

Lines changed: 122 additions & 263 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/docs/configure/logging.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Conversation Logging
2+
3+
Altimate Code automatically logs each conversation turn to the Altimate backend. This powers validation, audit, and quality analysis features. Logging is **enabled by default** — no configuration is required to activate it.
4+
5+
## What Is Logged
6+
7+
Each turn (one user prompt + all assistant responses) sends the following to the Altimate backend:
8+
9+
| Field | Description |
10+
|-------|-------------|
11+
| `session_id` | The current session identifier |
12+
| `conversation_id` | The assistant message ID for this turn |
13+
| `user_id` | Your email or username (from your Altimate account) |
14+
| `user_prompt` | The text of your message |
15+
| `parts` | All reasoning, text, and tool call/response parts from the assistant |
16+
| `final_response` | The last text response from the assistant |
17+
| `metadata` | Model ID, token counts, and cost for the turn |
18+
19+
Logging fires after the session becomes idle (i.e., after the assistant finishes responding). Up to 500 messages are captured per turn to ensure complete coverage of multi-step agentic sessions.
20+
21+
## Why We Log
22+
23+
Conversation logs are used to:
24+
25+
- **Validate AI responses** — power the `/validate` skill that audits factual claims against source data
26+
- **Quality analysis** — identify recurring failure patterns across sessions
27+
- **Audit trails** — provide a record of what the assistant did and why
28+
29+
## Disabling Logging
30+
31+
Logging is on by default. To disable it, set the following environment variable before starting Altimate Code:
32+
33+
```bash
34+
export ALTIMATE_LOGGER_DISABLED=true
35+
```
36+
37+
To make this permanent, add it to your shell profile (`~/.zshrc`, `~/.bashrc`, etc.):
38+
39+
```bash
40+
echo 'export ALTIMATE_LOGGER_DISABLED=true' >> ~/.zshrc
41+
source ~/.zshrc
42+
```
43+
44+
To re-enable logging, unset the variable:
45+
46+
```bash
47+
unset ALTIMATE_LOGGER_DISABLED
48+
```
49+
50+
Setting `ALTIMATE_LOGGER_DISABLED=false` is equivalent to not setting it — logging will be active.
51+
52+
## Network
53+
54+
Conversation logs are sent to:
55+
56+
| Endpoint | Purpose |
57+
|----------|---------|
58+
| `apimi.tryaltimate.com` | Conversation log ingestion |
59+
60+
Requests are fire-and-forget — a failed log request does not affect your session in any way.

packages/opencode/script/build.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ const migrations = await Promise.all(
7171
)
7272
console.log(`Loaded ${migrations.length} migrations`)
7373

74+
// Load validate skill assets for embedding
75+
const validateSkillMd = await Bun.file(path.join(dir, "src/skill/validate/SKILL.md")).text()
76+
const validateBatchPy = await Bun.file(path.join(dir, "src/skill/validate/batch_validate.py")).text()
77+
console.log("Loaded validate skill assets")
78+
79+
// Load logger hook for embedding
80+
const loggerHookPy = await Bun.file(path.join(dir, "src/skill/validate/logger_hook.py")).text()
81+
console.log("Loaded logger hook")
82+
7483
const singleFlag = process.argv.includes("--single")
7584
const baselineFlag = process.argv.includes("--baseline")
7685
const skipInstall = process.argv.includes("--skip-install")
@@ -224,6 +233,9 @@ for (const item of targets) {
224233
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
225234
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
226235
OPENCODE_CHANGELOG: JSON.stringify(changelog),
236+
ALTIMATE_VALIDATE_SKILL_MD: JSON.stringify(validateSkillMd),
237+
ALTIMATE_VALIDATE_BATCH_PY: JSON.stringify(validateBatchPy),
238+
ALTIMATE_LOGGER_HOOK_PY: JSON.stringify(loggerHookPy),
227239
OPENCODE_WORKER_PATH: workerPath,
228240
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
229241
},
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import type { Argv } from "yargs"
2+
import { cmd } from "./cmd"
3+
import * as prompts from "@clack/prompts"
4+
import fs from "fs/promises"
5+
import path from "path"
6+
import os from "os"
7+
8+
const BASE_URL = "https://apimi.tryaltimate.com"
9+
10+
function getAltimateDotDir(): string {
11+
return path.join(os.homedir(), ".altimate-code")
12+
}
13+
14+
async function readSettings(): Promise<Record<string, unknown>> {
15+
const settingsPath = path.join(getAltimateDotDir(), "settings.json")
16+
try {
17+
return JSON.parse(await fs.readFile(settingsPath, "utf-8"))
18+
} catch {
19+
return {}
20+
}
21+
}
22+
23+
async function writeSettings(settings: Record<string, unknown>): Promise<void> {
24+
const dir = getAltimateDotDir()
25+
await fs.mkdir(dir, { recursive: true })
26+
await fs.writeFile(path.join(dir, "settings.json"), JSON.stringify(settings, null, 2))
27+
}
28+
29+
// Injected at build time by build.ts (same pattern as ALTIMATE_CLI_MIGRATIONS).
30+
// In development these fall back to reading from disk via getAssets().
31+
declare const ALTIMATE_VALIDATE_SKILL_MD: string
32+
declare const ALTIMATE_VALIDATE_BATCH_PY: string
33+
34+
interface ValidateAssets {
35+
skillMd: string
36+
batchPy: string
37+
}
38+
39+
async function getAssets(): Promise<ValidateAssets> {
40+
if (
41+
typeof ALTIMATE_VALIDATE_SKILL_MD !== "undefined" &&
42+
typeof ALTIMATE_VALIDATE_BATCH_PY !== "undefined"
43+
) {
44+
return {
45+
skillMd: ALTIMATE_VALIDATE_SKILL_MD,
46+
batchPy: ALTIMATE_VALIDATE_BATCH_PY,
47+
}
48+
}
49+
// Development fallback: read from disk relative to this source file
50+
const skillsDir = path.join(import.meta.dir, "../../skill/validate")
51+
const [skillMd, batchPy] = await Promise.all([
52+
fs.readFile(path.join(skillsDir, "SKILL.md"), "utf-8"),
53+
fs.readFile(path.join(skillsDir, "batch_validate.py"), "utf-8"),
54+
])
55+
return { skillMd, batchPy }
56+
}
57+
58+
59+
60+
const InstallSubcommand = cmd({
61+
command: "install",
62+
describe: "install the /validate skill into ~/.altimate-code",
63+
handler: async () => {
64+
prompts.intro("Altimate Validate — Installer")
65+
66+
const { skillMd, batchPy } = await getAssets()
67+
68+
const spinner = prompts.spinner()
69+
spinner.start("Installing /validate skill...")
70+
const skillTargetDir = path.join(os.homedir(), ".altimate-code", "skills", "validate")
71+
await fs.mkdir(skillTargetDir, { recursive: true })
72+
await fs.writeFile(path.join(skillTargetDir, "SKILL.md"), skillMd)
73+
await fs.writeFile(path.join(skillTargetDir, "batch_validate.py"), batchPy)
74+
spinner.stop(`Installed /validate skill → ${skillTargetDir}`)
75+
76+
prompts.outro("Altimate validation skill installed successfully!")
77+
},
78+
})
79+
80+
const StatusSubcommand = cmd({
81+
command: "status",
82+
describe: "check whether the /validate skill is installed",
83+
handler: async () => {
84+
const skillDir = path.join(os.homedir(), ".altimate-code", "skills", "validate")
85+
86+
prompts.intro("Altimate Validate — Installation Status")
87+
88+
const check = (exists: boolean, label: string, detail: string) =>
89+
prompts.log.info(`${exists ? "✓" : "✗"} ${label}${exists ? "" : " (not found)"}: ${detail}`)
90+
91+
const skillMdExists = await fs.access(path.join(skillDir, "SKILL.md")).then(() => true).catch(() => false)
92+
const batchPyExists = await fs.access(path.join(skillDir, "batch_validate.py")).then(() => true).catch(() => false)
93+
check(skillMdExists && batchPyExists, "/validate skill", skillDir)
94+
95+
prompts.outro("Done")
96+
},
97+
})
98+
99+
const ConfigureSubcommand = cmd({
100+
command: "configure",
101+
describe: "register your Altimate API key to enable /validate",
102+
builder: (yargs: Argv) =>
103+
yargs.option("api-key", { type: "string", description: "Your Altimate API key" }),
104+
handler: async (args) => {
105+
prompts.intro("Altimate Validate — Configure")
106+
107+
const apiKey =
108+
(args["api-key"] as string | undefined) ||
109+
((await prompts.text({
110+
message: "Enter your Altimate API key:",
111+
placeholder: "8a5b279d...",
112+
validate: (v) => ((v ?? "").trim().length > 0 ? undefined : "API key is required"),
113+
})) as string)
114+
115+
if (prompts.isCancel(apiKey)) {
116+
prompts.cancel("Cancelled.")
117+
process.exit(0)
118+
}
119+
120+
const spinner = prompts.spinner()
121+
spinner.start("Registering with validation server...")
122+
123+
try {
124+
const res = await fetch(`${BASE_URL}/auth/register`, {
125+
method: "POST",
126+
headers: { "Content-Type": "application/json" },
127+
body: JSON.stringify({ api_key: apiKey }),
128+
})
129+
130+
if (!res.ok) {
131+
const body = await res.text()
132+
spinner.stop("Registration failed.")
133+
prompts.log.error(`Server returned ${res.status}: ${body}`)
134+
process.exit(1)
135+
}
136+
137+
spinner.stop("Registered with validation server.")
138+
} catch (err) {
139+
spinner.stop("Could not reach validation server.")
140+
prompts.log.warn(`Warning: ${err}. Credentials saved locally anyway.`)
141+
}
142+
143+
// Save credentials to ~/.altimate-code/settings.json
144+
const settings = await readSettings()
145+
settings.altimate_api_key = apiKey
146+
await writeSettings(settings)
147+
148+
prompts.log.success(`Credentials saved to ${path.join(getAltimateDotDir(), "settings.json")}`)
149+
prompts.outro("Configuration complete. You can now run /validate.")
150+
},
151+
})
152+
153+
export const ValidateCommand = cmd({
154+
command: "validate",
155+
describe: "manage the Altimate validation framework (/validate skill)",
156+
builder: (yargs: Argv) =>
157+
yargs
158+
.command(InstallSubcommand)
159+
.command(StatusSubcommand)
160+
.command(ConfigureSubcommand)
161+
.demandCommand(),
162+
handler: () => {},
163+
})

packages/opencode/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { WebCommand } from "./cli/cmd/web"
3030
import { PrCommand } from "./cli/cmd/pr"
3131
import { SessionCommand } from "./cli/cmd/session"
3232
import { DbCommand } from "./cli/cmd/db"
33+
import { ValidateCommand } from "./cli/cmd/validate"
3334
import path from "path"
3435
import { Global } from "./global"
3536
import { JsonMigration } from "./storage/json-migration"
@@ -175,6 +176,7 @@ let cli = yargs(hideBin(process.argv))
175176
.command(PrCommand)
176177
.command(SessionCommand)
177178
.command(DbCommand)
179+
.command(ValidateCommand)
178180

179181
if (Installation.isLocal()) {
180182
cli = cli.command(WorkspaceServeCommand)

0 commit comments

Comments
 (0)