# GroutCode Coding Exams

> The code_spec format, starter and test files, and encrypted hidden tests with key custody.

Canonical: https://grout.app/developer/documentation/exams/groutcode/

A coding exam is an exam with a `code_spec`. GroutCode installs the toolchain locally, opens the brief, runs sample tests on demand and, when the student asks, fetches the hidden-test key **once per attempt** so hidden tests can run offline without ever shipping the key inside the exam.

## `code_spec`

```json
{
  "v": 1,
  "brief_md": "# Implement a stack\n…",
  "toolchain": "python",
  "starter": { "kind": "zip", "file": { "id": "file_…" } },
  "sample_tests": { "kind": "inline", "files": [{ "path": "tests/sample/test_basic.py", "content": "…" }] },
  "hidden_tests": { "kind": "zip-encrypted", "file": { "id": "file_…" }, "iv_b64": "…", "sha256": "…" },
  "commands": { "setup": "", "run": "python main.py", "test_sample": "pytest {{workspace}}/tests/sample", "test_hidden": "pytest {{hidden}}", "timeout_ms": 60000 },
  "ai_policy": { "allowed": false, "model_tier": "any" },
  "rubric_md": "…",
  "exclude_globs": ["node_modules/**", ".venv/**"]
}
```

| Field | Notes |
|---|---|
| `toolchain` | `c`, `cpp`, `java`, `python`, `node`, `sqlite`. Missing toolchains are installed on the student's machine. |
| `starter` | `{kind:"zip", file}` uploaded via `POST /exams/{id}/files`, or `{kind:"empty"}`. |
| `sample_tests` | Inline files (relative paths, no `..`) or a zip. Visible to the student. |
| `hidden_tests` | AES-256-GCM-encrypted zip. `iv_b64` and the ciphertext `sha256` live in the spec; the **key does not**. |
| `commands` | `{{workspace}}`, `{{hidden}}`, `{{results}}` are substituted. `timeout_ms` 1 000–600 000. |
| `ai_policy` | Enforced locally; usage is reported at submit in `ai_usage_data`. |

## Hidden-test workflow

:::steps
### Encrypt and upload
Zip the hidden tests, encrypt with AES-256-GCM using a random 12-byte IV, and upload the ciphertext:
```bash
curl -X POST $API/exams/ex_…/files -H "Authorization: Bearer $KEY" -F "file=@hidden-tests.zip.enc"
```
Put the returned `ref` in `code_spec.hidden_tests.file`, plus `iv_b64` and the ciphertext `sha256`.

### Store the key
```bash
curl -X PUT $API/exams/ex_…/code-secret -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{ "hidden_key_b64": "<base64 of 32 random bytes>" }'
```
The key is write-only for students; GroutCode obtains it through a release call that stamps the attempt.

### Watch releases
```bash
curl $API/exams/ex_…/code-secret/status -H "Authorization: Bearer $KEY"
curl $API/exams/ex_…/attempts -H "Authorization: Bearer $KEY"        # hidden_released_at per student
```

### Re-grade offline (optional)
`POST /exams/{id}/code-secret/release` returns the key to an institution integration so you can decrypt and re-run hidden tests yourself.
:::

## Encrypting in practice

```js
import { randomBytes, createCipheriv, createHash } from 'node:crypto';
import { readFileSync, writeFileSync } from 'node:fs';

const key = randomBytes(32), iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, iv);
const ct = Buffer.concat([cipher.update(readFileSync('hidden-tests.zip')), cipher.final(), cipher.getAuthTag()]);
writeFileSync('hidden-tests.zip.enc', ct);
console.log({ hidden_key_b64: key.toString('base64'), iv_b64: iv.toString('base64'), sha256: createHash('sha256').update(ct).digest('hex') });
```

## What comes back at submit

GroutCode uploads `workspace.zip`, `transcript.json` (AI transcript) and `test-results.json`. They appear as signed attachment URLs on `GET /submissions/{id}`, and `ai_usage_data` carries the local test results, timing, focus-loss and models used:

```json
{ "kind": "groutcode", "v": 1,
  "tests": { "sample": { "passed": 8, "failed": 0 }, "hidden": { "passed": 11, "failed": 1, "hidden_zip_sha256": "…", "workspace_sha256": "…" } },
  "timing": { "started_at": "…", "hidden_released_at": "…", "submitted_at": "…" },
  "focus": { "lost_count": 2, "lost_seconds": 41 },
  "toolchain": "python", "app": { "name": "groutcode", "version": "2.4.0", "platform": "win32" },
  "total_interactions": 0, "models_used": [] }
```
