Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
6fbadb7
docs(wiki): update wiki
zzjc1234 May 11, 2026
b660de0
feat(llm-client): scaffold project
zzjc1234 May 11, 2026
465e4f1
feat(llm-client): add capture pipeline
zzjc1234 May 11, 2026
1695d95
feat(llm-client): wire end-to-end handler + main entry
zzjc1234 May 11, 2026
7c9c33c
feat(llm-client): backend ingest client with persistent retry queue
zzjc1234 May 11, 2026
d0396eb
feat(llm-client): client side pipeline
zzjc1234 May 11, 2026
b767ab7
feat: token-hash auth mechanism + client-side secret redaction
zzjc1234 May 12, 2026
45c9c97
feat(llm-client): cache eviction (TTL) + interruptible worker sleeps
zzjc1234 May 12, 2026
8bb42a9
feat(llm-client): cache eviction (TTL) + interruptible worker sleeps
zzjc1234 May 12, 2026
3dad184
feat(backend): scaffold + ingest API + token-hash auth
zzjc1234 May 12, 2026
0d48cc7
chore(e2e): real-backend smoke + relax ingest body optional fields
zzjc1234 May 12, 2026
661a85c
feat: admin payload-fetch endpoint + inspectable blob format
zzjc1234 May 12, 2026
dfd44c0
feat(llm-client): atomic claim + busy_timeout for multi-worker safety
zzjc1234 May 12, 2026
8ba5fef
feat: integrate real Tbox WebDAV (basic-auth + auto-MKCOL) e2e
zzjc1234 May 12, 2026
572c531
feat(llm-client): multi-target compile + example config
zzjc1234 May 12, 2026
ac8a6d3
feat(backend): TT-side admin CLI (aimdware-admin)
zzjc1234 May 12, 2026
302b5cf
feat: session-keyed blobs (O(N) storage for agents)
zzjc1234 May 12, 2026
4bb1aef
chore: rename scripts -> test-scripts
zzjc1234 May 12, 2026
1fba751
fix(review): address 7 issues from Design A code review
zzjc1234 May 13, 2026
e99f29c
fix(review2): close 2 Important + 1 Minor from second-round review
zzjc1234 May 13, 2026
bb09bf4
fix(review2): close 4 deferred Minor items
zzjc1234 May 13, 2026
a6403c8
style: clean up
zzjc1234 May 13, 2026
cbb76c3
refactor(llm-client): update prj hierachy
zzjc1234 May 14, 2026
78c1f5b
chore: update prj ci
zzjc1234 May 14, 2026
86aa3be
fix(test-scripts): smoke_e2e fake WebDAV needs MKCOL support
zzjc1234 May 14, 2026
bb0d450
fix(http): preserve base_url path prefix when forwarding to upstream
zzjc1234 May 14, 2026
5d950ff
feat(recording): persist tools + tool_choice
zzjc1234 May 14, 2026
f6ac656
test(functional): findings from SJTU-upstream run, 5 tests
zzjc1234 May 14, 2026
811f774
refactor(recording): blob carries the request verbatim
zzjc1234 May 14, 2026
4cb0f6f
test(recording): pin behaviour under 1-5 MB payloads
zzjc1234 May 14, 2026
0c10889
feat(backend): add assignment dimension
zzjc1234 May 14, 2026
bff73b5
docs(wiki): align with current implementation + add design-notes
zzjc1234 May 16, 2026
661e294
fix(admin_cli): register --assignment on `record list` parser
zzjc1234 May 16, 2026
88f9439
fix(aimdware): harden ingest validation and cache cleanup
zzjc1234 May 16, 2026
156458b
fix(client): reclaim cache after successful upload
zzjc1234 May 16, 2026
0fb0396
chore: ignore agent.md
zzjc1234 May 16, 2026
106a43e
fix(client): guard cache cleanup during pending writes
zzjc1234 May 16, 2026
76537da
feat(client): add subscription provider plugins
zzjc1234 May 16, 2026
093b077
feat(client): support responses protocol
zzjc1234 May 16, 2026
cec9813
fix(client): align subscription providers with opencode
zzjc1234 May 16, 2026
e3a71e6
feat(client): harden auth store with del, atomic 0600 writes, seriali…
zzjc1234 Jun 2, 2026
bd34703
fix(client): make codex token refresh single-flight, validated, self-…
zzjc1234 Jun 2, 2026
ceae7e5
fix(client): bound codex device-login polling and validate the token …
zzjc1234 Jun 2, 2026
4a22130
fix(client): close codex refresh stale-read race in the single-flight…
zzjc1234 Jun 2, 2026
98da0e3
fix(client): close auth.json permission gaps (temp-file window, exist…
zzjc1234 Jun 2, 2026
08c1eb8
fix(client): isolate credential dir so chmod can't clobber the shared…
zzjc1234 Jun 2, 2026
94bc090
fix(client): recover instead of deleting when a concurrent refresh ro…
zzjc1234 Jun 2, 2026
84d3f08
fix(client): serialize codex refresh across processes with a credenti…
zzjc1234 Jun 2, 2026
7c50f1d
ci(client): add [lock]-triggered cross-process refresh concurrency test
zzjc1234 Jun 2, 2026
5c27a85
chore(backend): load .env
zzjc1234 Jun 8, 2026
04c8ec1
docs: update tt and student side docs
zzjc1234 Jun 21, 2026
6f938a1
docs(spec): aimdware-admin CSV batch design
zzjc1234 Jun 21, 2026
64072c4
feat(admin_cli): CSV roster batch for user create / enroll / token is…
zzjc1234 Jun 21, 2026
1a52499
docs(wiki): student_id schema + codex ChatGPT-account specifics
zzjc1234 Jun 22, 2026
468848a
docs: accuracy pass — CSV batch + subscriptions shipped, WebDAV PUT, …
zzjc1234 Jun 22, 2026
19e23e7
chore: rm outdated docs
zzjc1234 Jun 22, 2026
a1dfd02
chore: ignore env
zzjc1234 Jul 6, 2026
10d487d
Add GitHub client build workflow
zzjc1234 Jul 6, 2026
9d45500
Update GitHub Actions versions
zzjc1234 Jul 6, 2026
e8a9b5d
Pin macOS Actions runner
zzjc1234 Jul 6, 2026
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
32 changes: 32 additions & 0 deletions .gitea/workflows/concurrency.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: codex-auth-concurrency

# Heavy: spawns multiple real processes that contend on one credential file.
# Runs only when a pushed commit's message contains "[lock]", or on manual
# dispatch — not on every push.
on:
push:
workflow_dispatch:

jobs:
concurrency:
if: ${{ contains(github.event.head_commit.message, '[lock]') || github.event_name == 'workflow_dispatch' }}
# Adjust to a label your Gitea Act runner advertises if it isn't ubuntu-latest.
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.11"

- name: Install llm-client dependencies
working-directory: llm-client
run: bun install --frozen-lockfile

- name: Cross-process codex refresh concurrency test
env:
CONC_PROCESSES: "8"
CONC_RUNS: "3"
run: bun run test-scripts/concurrency/run.ts
54 changes: 54 additions & 0 deletions .github/workflows/client-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: client-build

on:
push:
pull_request:
workflow_dispatch:

jobs:
build:
name: ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-15
target: macos-arm64
script: build:macos-arm64
artifact: aimdware-router-macos-arm64
- os: macos-15
target: macos-x64
script: build:macos-x64
artifact: aimdware-router-macos-x64
- os: ubuntu-latest
target: linux-arm64
script: build:linux-arm64
artifact: aimdware-router-linux-arm64
- os: ubuntu-latest
target: linux-x64
script: build:linux-x64
artifact: aimdware-router-linux-x64

defaults:
run:
working-directory: llm-client

steps:
- uses: actions/checkout@v7

- uses: oven-sh/setup-bun@v2

- run: bun install --frozen-lockfile

- run: bun run typecheck

- run: bun test

- run: bun run ${{ matrix.script }}

- uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact }}
path: llm-client/dist/${{ matrix.artifact }}
if-no-files-found: error
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
third_party/
node_modules/
dist/
*.log
.DS_Store

.*.bun-build
test-functional/runs/
test-functional/opencode.json

# Personal runbook — do not commit
AGENT.md
.claude/
.env
47 changes: 47 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Run with: uv run --project backend prek run --all-files
# Install : uv run --project backend prek install (one-time, hooks .git/hooks)
#
# Tooling lives where the language lives:
# - oxfmt + oxlint: llm-client/ (bun devDeps + .oxfmtrc.json + .oxlintrc.json)
# - ruff: backend/ (uv dev dep + [tool.ruff] in pyproject)
# - prek itself: backend/ (uv dev dep)
exclude: '^(third_party/|.*/dist/|.*/node_modules/|.*/\.venv/|backend/alembic/versions/)'

repos:
# --- generic ---
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files

# --- Python: backend/ ---
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.5
hooks:
- id: ruff-check
types_or: [python, pyi]
args: ["--fix"]
- id: ruff-format
types_or: [python, pyi]

# --- TypeScript: llm-client/ ---
- repo: https://github.com/oxc-project/mirrors-oxlint
rev: v1.41.0
hooks:
- id: oxlint
types_or: [javascript, jsx, ts, tsx]
verbose: true

- repo: local
hooks:
- id: oxfmt
name: oxfmt
# pre-commit runs from repo root; cd into the workspace where
# oxfmt is installed as a Bun devDep + .oxfmtrc.json lives.
entry: bash -c 'cd llm-client && bun run format'
language: system
files: '^llm-client/.*\.(ts|tsx|js|jsx)$'
pass_filenames: false
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ A teaching-team toolkit for monitoring student AI usage in coursework.
Three components:

- **Backend** (Python/FastAPI/Postgres) — stores per-record metadata + sha256 + jbox URI. Holds no prompt/response content.
- **LLM client** (Bun single binary) — runs on the student's machine. OpenAI-compatible local API; forwards to a student-configured upstream with the student's own LLM key; uploads the response JSON to the student's jbox via `rclone` over the local Tbox WebDAV endpoint.
- **LLM client** (Bun single binary) — runs on the student's machine. OpenAI-compatible local API; forwards to a student-configured upstream (the student's own LLM key, or a ChatGPT/Codex or GitHub Copilot subscription); uploads the response JSON to the student's jbox via a WebDAV PUT to the local Tbox endpoint.
- **Admin script** (`aimdware-admin`, Python CLI) — TT-side tool. Manages users / courses / enrollments / tokens via direct Postgres; fetches blobs from jbox for inspection.

## Architecture
Expand All @@ -15,7 +15,7 @@ Three components:
│ Student's own laptop │
│ │
│ coding agent ──▶ aimdware router ──▶ OpenAI / DeepSeek / │
│ (Cline / Aider / (course token + upstream LLM │
│ (Cline / Aider / (student token + upstream LLM │
│ OpenCode...) student's LLM key) │
│ │ │ │
└──────────────────────────┼─────────────┼─────────────────────────┘
Expand Down
12 changes: 12 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.venv/
.env
__pycache__/
*.egg-info/
.pytest_cache/
*.pyc
.mypy_cache/
.ruff_cache/
*.db
*.db-journal
*.db-wal
*.db-shm
149 changes: 149 additions & 0 deletions backend/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# A generic, single database configuration.

[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/alembic

# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s

# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .


# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the tzdata library which can be installed by adding
# `alembic[tz]` to the pip requirements.
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =

# max length of characters to apply to the "slug" field
# truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions

# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os

# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = driver://user:pass@localhost/dbname


[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples

# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME

# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME

# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME

# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARNING
handlers = console
qualname =

[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
1 change: 1 addition & 0 deletions backend/alembic/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Generic single-database configuration.
55 changes: 55 additions & 0 deletions backend/alembic/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Alembic env. Reads DB URL from $AIMDWARE_DATABASE_URL via our settings,
and targets SQLModel.metadata so `alembic revision --autogenerate` works."""

from logging.config import fileConfig

from sqlalchemy import engine_from_config, pool
from sqlmodel import SQLModel

# Importing the models registers every table with SQLModel.metadata.
from aimdware_backend import models # noqa: F401
from aimdware_backend.settings import settings
from alembic import context

config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)

# Inject the runtime DB URL so the same env vars work for migrations and the app.
config.set_main_option("sqlalchemy.url", settings.database_url)

target_metadata = SQLModel.metadata


def run_migrations_offline() -> None:
context.configure(
url=config.get_main_option("sqlalchemy.url"),
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
render_as_batch=True, # required for sqlite ALTER COLUMN ops
)
with context.begin_transaction():
context.run_migrations()


def run_migrations_online() -> None:
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=True,
)
with context.begin_transaction():
context.run_migrations()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
Loading