Changeset 0.28.2.2 (#185)

This commit is contained in:
2026-03-13 16:09:16 +00:00
parent 7803ba8adf
commit 8c4cb9bbeb
9 changed files with 1199 additions and 91 deletions

View File

@@ -1,55 +1,205 @@
# User Profile & Settings
### Profile
User identity, preferences, and credentials.
**All endpoints require:** `Auth()` middleware (JWT bearer token).
---
### Get Profile
```
GET /profile → profileResponse
PUT /profile ← { "display_name", "email" }
GET /profile
```
**Auth:** Authenticated user
Returns the current user's profile.
```json
{
"id": "uuid",
"username": "jdoe",
"email": "jdoe@example.com",
"display_name": "Jane Doe",
"role": "user|admin",
"avatar_url": "...",
"created_at": "...",
"last_login": "..."
"role": "user",
"avatar": "data:image/png;base64,...",
"settings": { "theme": "dark" },
"created_at": "2025-06-15T14:30:00Z",
"last_login_at": "2025-06-15T14:30:00Z"
}
```
### Avatar
| Field | Type | Notes |
|-------|------|-------|
| `id` | string | UUIDv4 |
| `username` | string | Unique login name |
| `email` | string | Unique, lowercased |
| `display_name` | string \| null | Optional friendly name |
| `role` | `"user"` \| `"admin"` | Platform role |
| `avatar` | string \| null | Data URI (PNG, 128×128). Omitted if unset. |
| `settings` | object | User preferences (theme, keybindings, etc.) |
| `created_at` | string | ISO 8601 timestamp |
| `last_login_at` | string \| null | ISO 8601 timestamp. Null if never logged in. |
> **Note:** The profile endpoint returns `avatar` (not `avatar_url`) as a
> curated response shape. The `User` model uses `avatar_url` internally.
---
### Update Profile
```
PUT /profile
```
**Auth:** Authenticated user
```json
{
"display_name": "New Name",
"email": "new@example.com"
}
```
Both fields are optional (partial update). Returns the full profile
(same shape as `GET /profile`).
**Errors:**
- `409` — email already taken
---
### Upload Avatar
```
POST /profile/avatar
```
**Auth:** Authenticated user
**Content-Type:** `application/json`
```json
{
"image": "data:image/png;base64,..."
}
```
Accepts a base64 data URI or raw base64 string. The server decodes,
resizes to 128×128 PNG, and stores as a data URI. Max input: 2 MB.
**Response:**
```json
{
"avatar": "data:image/png;base64,..."
}
```
**Errors:**
- `400` — missing `image` field, invalid base64, unsupported format, too large
---
### Delete Avatar
```
POST /profile/avatar ← multipart/form-data (field: "avatar")
DELETE /profile/avatar
```
### Password
**Auth:** Authenticated user
**Response:**
```json
{
"message": "avatar removed"
}
```
---
### Change Password
```
POST /profile/password
```
**Auth:** Authenticated user (builtin auth only)
```json
{
"current_password": "...",
"new_password": "..."
"current_password": "old-pass",
"new_password": "new-pass-min-8"
}
```
On password change, the UEK is re-wrapped with the new password
(BYOK keys remain accessible without re-encryption).
Validates `current_password` against stored bcrypt hash. `new_password`
must be 8128 characters.
### App Settings
On success, the UEK (User Encryption Key) is re-wrapped with the new
password so BYOK-encrypted provider keys remain accessible.
```
GET /settings → { "settings": {...} }
PUT /settings ← { "theme", "editor_keybindings", ... }
**Response:**
```json
{
"message": "password updated"
}
```
User-level preferences (theme, keybindings, default model, etc.).
**Errors:**
- `400` — missing fields, password too short/long
- `401` — current password incorrect
---
### Get Settings
```
GET /settings
```
**Auth:** Authenticated user
Returns user-level preferences wrapped in a `settings` key.
```json
{
"settings": {
"theme": "dark",
"editor_keybindings": "vim",
"default_model": "claude-sonnet-4-5"
}
}
```
This is a **composite response** (named key wrapping a single object),
not a bare object. The `settings` key is always present; its value is
`{}` if the user has never saved preferences.
---
### Update Settings
```
PUT /settings
```
**Auth:** Authenticated user
```json
{
"theme": "light",
"new_key": "new_value"
}
```
Shallow merge: incoming keys overwrite existing, unmentioned keys are
preserved. Handles edge cases: SQL NULL, JSON `null`, and corrupted
array values are all normalized to `{}` before merge.
Returns the updated settings (same shape as `GET /settings`).
---

View File

@@ -4,6 +4,13 @@ The **Workspace** is a virtual filesystem for the editor surface.
Each workspace has a root directory, file operations, optional Git
integration, and full-text indexing for code search.
**All endpoints require:** `Auth()` middleware (JWT bearer token).
Workspace-scoped endpoints additionally verify ownership via
`loadAndAuthorize` (user owns workspace, or is member of the
owning team/project/channel).
---
### Workspace CRUD
```
@@ -14,62 +21,175 @@ PATCH /workspaces/:id ← partial update
DELETE /workspaces/:id
```
**Auth:** Authenticated user. `GET /workspaces` returns user-owned
workspaces plus those owned by the user's teams. All `:id` endpoints
verify the caller can access the workspace's owner entity.
Workspace object:
```json
{
"id": "uuid",
"name": "my-project",
"owner_type": "user|project",
"owner_type": "user",
"owner_id": "uuid",
"status": "active|archived",
"storage_bytes": 1048576,
"status": "active",
"max_bytes": 52428800,
"indexing_enabled": false,
"git_remote_url": "https://github.com/...",
"git_branch": "main",
"git_credential_id": "uuid",
"git_last_sync": "2025-06-15T14:30:00Z",
"file_count": 42,
"created_at": "...",
"updated_at": "..."
"total_bytes": 1048576,
"created_at": "2025-06-15T14:30:00Z",
"updated_at": "2025-06-15T14:30:00Z"
}
```
### File Operations
| Field | Type | Notes |
|-------|------|-------|
| `id` | string | UUIDv4 |
| `name` | string | Max 200 chars |
| `owner_type` | `"user"` \| `"project"` \| `"channel"` \| `"team"` | Owner entity type |
| `owner_id` | string | UUID of owner entity |
| `status` | `"active"` \| `"archived"` \| `"deleting"` | Lifecycle state |
| `max_bytes` | int \| null | Quota ceiling. Omitted if unset. |
| `indexing_enabled` | bool | Full-text search indexing |
| `git_remote_url` | string \| null | Git remote. Omitted if unset. |
| `git_branch` | string \| null | Tracked branch. Omitted if unset. |
| `git_credential_id` | string \| null | FK to git credential. Omitted if unset. |
| `git_last_sync` | string \| null | ISO 8601. Omitted if never synced. |
| `file_count` | int | Computed. Enriched on `GET /:id` only. |
| `total_bytes` | int | Computed. Enriched on `GET /:id` only. |
| `created_at` | string | ISO 8601 |
| `updated_at` | string | ISO 8601 |
All paths are relative to the workspace root.
> **Note:** `root_path` is a server-internal filesystem path and is
> never exposed in API responses (json tag `"-"`).
```
GET /workspaces/:id/files?path=/ → { "files": [entries] }
GET /workspaces/:id/files/read?path=/a.md → { "content": "...", "size": 123 }
PUT /workspaces/:id/files/write ← { "path": "/a.md", "content": "..." }
DELETE /workspaces/:id/files/delete ← { "path": "/a.md" }
POST /workspaces/:id/files/mkdir ← { "path": "/src" }
```
File entry:
**Create request:**
```json
{
"name": "main.go",
"path": "/src/main.go",
"is_dir": false,
"size": 4096,
"modified": "..."
"name": "my-project",
"owner_type": "user",
"owner_id": "uuid",
"max_bytes": 52428800
}
```
Returns `201` with the workspace object.
**PATCH request** (all fields optional):
```json
{
"name": "new-name",
"status": "archived",
"max_bytes": 104857600,
"indexing_enabled": true,
"git_remote_url": "https://...",
"git_branch": "develop",
"git_credential_id": "uuid"
}
```
Returns updated workspace object.
**DELETE** returns `{"ok": true}`. Deletion is async (status set to
`"deleting"`, filesystem cleanup runs in background).
---
### File Operations
All paths are relative to the workspace root. File path is passed
as `?path=` query parameter.
```
GET /workspaces/:id/files?path=/ → { "data": [entries] }
GET /workspaces/:id/files/read?path=/a.md → raw file content (binary stream)
PUT /workspaces/:id/files/write?path=/a.md ← raw body (Content-Length required)
DELETE /workspaces/:id/files/delete?path=/a.md
POST /workspaces/:id/files/mkdir?path=/src
```
**Auth:** Authenticated user with workspace access.
**`GET .../files`** accepts `?recursive=true` for deep listing.
Returns `{"data": [...]}` (never bare array). Nil slice guarded.
File entry (from `WorkspaceFile` model):
```json
{
"id": "uuid",
"workspace_id": "uuid",
"path": "/src/main.go",
"is_directory": false,
"content_type": "text/x-go",
"size_bytes": 4096,
"sha256": "abc123...",
"index_status": "ready",
"chunk_count": 3,
"created_at": "2025-06-15T14:30:00Z",
"updated_at": "2025-06-15T14:30:00Z"
}
```
**`GET .../files/read`** returns raw file content with appropriate
`Content-Type` header and `Content-Length`. This is a binary stream,
not JSON.
**`PUT .../files/write`** reads the request body as raw content.
Returns `{"ok": true, "path": "/a.md"}`. Returns `413` if the
write would exceed the workspace quota.
**`DELETE .../files/delete`** accepts `?recursive=true` for
directory removal. Returns `{"ok": true}`.
**`POST .../files/mkdir`** returns `201 {"ok": true, "path": "/src"}`.
---
### Archive
**Download** (tar.gz of entire workspace):
**Download** (full workspace as archive):
```
GET /workspaces/:id/archive/download
GET /workspaces/:id/archive/download?format=zip
```
**Upload** (restore from tar.gz):
**Auth:** Authenticated user with workspace access.
`format` query param: `zip` (default), `tar.gz`, `tgz`.
Returns binary stream with `Content-Disposition: attachment`.
**Upload** (restore from archive):
```
POST /workspaces/:id/archive/upload
Content-Type: multipart/form-data
Content-Type: multipart/form-data (field: "file")
```
### Reconcile, Stats, Index
**Auth:** Authenticated user with workspace access.
Accepts `.zip`, `.tar.gz`, `.tgz` archives. Format detected from
filename extension.
Returns:
```json
{
"ok": true,
"files_extracted": 42
}
```
---
### Reconcile, Stats, Index Status
**Reconcile** (sync filesystem state with DB metadata):
@@ -77,13 +197,37 @@ Content-Type: multipart/form-data
POST /workspaces/:id/reconcile
```
**Auth:** Authenticated user with workspace access.
Returns:
```json
{
"added": 5,
"removed": 2,
"updated": 3
}
```
**Stats:**
```
GET /workspaces/:id/stats
```
Returns `{ "file_count", "storage_bytes", "indexed_files", ... }`.
**Auth:** Authenticated user with workspace access.
Returns a `WorkspaceStats` object:
```json
{
"workspace_id": "uuid",
"file_count": 42,
"dir_count": 8,
"total_bytes": 1048576,
"max_bytes": 52428800
}
```
**Index status:**
@@ -91,47 +235,166 @@ Returns `{ "file_count", "storage_bytes", "indexed_files", ... }`.
GET /workspaces/:id/index-status
```
Returns indexing progress for full-text search.
**Auth:** Authenticated user with workspace access.
Returns indexing progress for full-text search:
```json
{
"workspace_id": "uuid",
"indexing_enabled": true,
"status_counts": {
"pending": 3,
"indexing": 1,
"ready": 38,
"error": 0,
"skipped": 2
},
"total_chunks": 156
}
```
---
### Git Integration
All Git operations are workspace-scoped.
All Git operations are workspace-scoped. Require the workspace to
have a Git remote configured.
```
POST /workspaces/:id/git/clone ← { "url", "branch", "credential_id" }
POST /workspaces/:id/git/pull
POST /workspaces/:id/git/push
GET /workspaces/:id/git/status → { "files": [{ "path", "status" }] }
GET /workspaces/:id/git/status → GitStatus object
GET /workspaces/:id/git/diff → { "diff": "..." }
POST /workspaces/:id/git/commit ← { "message", "files": [...] }
GET /workspaces/:id/git/log → { "commits": [...] }
POST /workspaces/:id/git/commit ← { "message", "paths": [...] }
GET /workspaces/:id/git/log → { "data": [GitLogEntry] }
GET /workspaces/:id/git/branches → { "branches": [...], "current": "main" }
POST /workspaces/:id/git/checkout ← { "branch": "feature-x" }
```
**Auth:** Authenticated user with workspace access.
**`POST .../git/clone`** request:
```json
{
"url": "https://github.com/user/repo.git",
"branch": "main",
"credential_id": "uuid"
}
```
`branch` and `credential_id` are optional. Returns `{"status": "cloned"}`.
**`POST .../git/pull`** returns `{"status": "pulled"}`.
**`POST .../git/push`** returns `{"status": "pushed"}`.
**`GET .../git/status`** returns the full `GitStatus` object:
```json
{
"branch": "main",
"clean": false,
"ahead": 2,
"behind": 0,
"staged": [{ "path": "file.go", "status": "M" }],
"modified": [{ "path": "other.go", "status": "M" }],
"untracked": ["new-file.txt"]
}
```
**`GET .../git/diff`** accepts optional `?path=file.go` for single-file diff.
Returns `{"diff": "..."}` (unified diff string).
**`POST .../git/commit`** request:
```json
{
"message": "fix: resolve null pointer",
"paths": ["/src/main.go", "/src/util.go"]
}
```
`paths` is optional (commits all staged if omitted). Returns
`{"status": "committed"}`.
**`GET .../git/log`** accepts optional `?n=20` (default 20, max 100).
Returns `{"data": [GitLogEntry]}`:
```json
{
"data": [
{
"hash": "abc123...",
"short_hash": "abc123",
"author": "Jane Doe",
"date": "2025-06-15T14:30:00Z",
"message": "Initial commit"
}
]
}
```
**`GET .../git/branches`** returns:
```json
{
"current": "main",
"branches": ["main", "develop", "feature-x"]
}
```
**`POST .../git/checkout`** returns
`{"status": "checked out", "branch": "feature-x"}`.
---
### Git Credentials
User-owned, encrypted credential storage for Git operations.
Encrypted with the user's UEK (same as BYOK keys — admins cannot
recover).
Encrypted with the user's UEK via the platform vault (same pattern
as BYOK keys — admins cannot recover).
Independent of workspace scope — credentials are user-level.
```
POST /git-credentials ← { "name", "auth_type", "token"|"username"+"password"|"ssh_key" }
POST /git-credentials ← { "name", "auth_type", ... }
GET /git-credentials → { "data": [GitCredentialSummary] }
DELETE /git-credentials/:id
```
`auth_type`: `https_pat`, `https_basic`, or `ssh_key`.
**Auth:** Authenticated user. Credentials are scoped to the requesting user.
Summary response (never exposes encrypted data):
**Create request:**
`auth_type` determines which fields are required:
| `auth_type` | Required fields |
|-------------|----------------|
| `https_pat` | `token` |
| `https_basic` | `username`, `password` |
| `ssh_key` | `private_key` (+ optional `passphrase`) |
Returns `201` with the credential summary (never exposes encrypted data).
**List response:**
```json
{
"id": "uuid",
"name": "GitHub PAT",
"auth_type": "https_pat",
"created_at": "..."
"data": [
{
"id": "uuid",
"name": "GitHub PAT",
"auth_type": "https_pat",
"created_at": "2025-06-15T14:30:00Z"
}
]
}
```
**DELETE** returns `{"deleted": true}`. Returns `404` if credential
not found or not owned by the requesting user.
---

View File

@@ -24,13 +24,14 @@ v0.9.xv0.27.5 Foundation → Extensions → Surfaces → Auth ✅
v0.28.0 Platform Polish
├─ v0.28.1 Surfaces ICD audit ✅
├─ v0.28.2 ICD audit round 2
├─ v0.28.3 Security tier (red team)
├─ v0.28.4 Infrastructure
├─ v0.28.2 ICD audit — all domains
├─ v0.28.3 FE decomp + SDK prep
├─ v0.28.4 Security tier (red team)
├─ v0.28.5 Infrastructure
│ (virtual scroll, KB auto-inject,
│ Helm chart, provider model prefs,
│ git credentials UI)
└─ v0.28.5 Frontend SDK
└─ v0.28.6 Frontend SDK
v0.29.0 Starlark Sandbox
(eval loop, permissions, admin UI)
@@ -57,7 +58,7 @@ v0.9.xv0.27.5 Foundation → Extensions → Surfaces → Auth ✅
## v0.28.0 — Platform Polish
Audit arc, frontend SDK, and infrastructure improvements.
Audit arc, frontend decomposition, security, and infrastructure improvements.
### v0.28.1 — Surfaces ICD Audit ✅
- [x] ICD `surfaces.md` corrected (6 discrepancies: field name, archive format, response shape)
@@ -66,15 +67,19 @@ Audit arc, frontend SDK, and infrastructure improvements.
- [x] 22 handler-level tests + 14 store-level tests (PG + SQLite)
- [x] CI timeout 8m → 12m for PG integration tests
### v0.28.2 — ICD Audit: Notifications + Profile + Knowledge + Notes + Providers
Known ICD report failures to investigate (likely envelope shape mismatches):
- [ ] `GET /settings``missing key "settings"` (profile)
- [x] `GET /notifications/preferences``missing key "preferences"` (envelope fix)
- [ ] `GET /workspaces``missing key "data"`
- [ ] `GET /git-credentials``missing key "data"`
- [ ] Notes, providers — passed clean but need full trace (route → handler → store → tests)
- [ ] WebSocket event contract audit: document `message.created`, `workflow.advanced`,
`presence.changed`, `typing`, `workflow.assigned` event shapes in ICD
### v0.28.2 — ICD Audit: All Domains
Full audit of every ICD document against code. Goal: ICD becomes the single
source of truth. After this version, the workflow is ICD-first — update the
contract before changing code.
**Methodology:** Read ICD → grep all source → trace route → handler → store
(PG + SQLite) → tests → enforce `{"data": [...]}` envelope convention → fix
ICD → fix code → fix runner → CI green. Final pass picks up straggling
failures across all domains.
**Completed audits:**
*Notifications (cs0):*
- [x] Notifications ICD audit: object shape, query params, response envelopes, WS events
- [x] Notification type enum sync: remove aspirational types, add implemented types
(`kb.ready`, `kb.error`, `grant.changed`, `task.budget_exceeded`)
@@ -82,9 +87,9 @@ Known ICD report failures to investigate (likely envelope shape mismatches):
- [x] Implement `user.mentioned` persisted notification (was WS-only)
- [x] Implement `workflow.claimed` persisted notification (was WS-only)
- [x] Remove dead `NotifTypeProjectInvite` constant
- [ ] Notification handler + store tests (PG + SQLite)
- [x] `GET /notifications/preferences` — envelope fix (`missing key "preferences"`)
**Knowledge ICD audit (cs1cs4):**
*Knowledge (cs1cs4):*
- [x] `knowledge.md` corrected: KB object shape (6 field mismatches), search envelope
(`data` not `results`), search result fields, status progression (`extracting`
step), file type support (text-only, not binary), auth annotations
@@ -103,7 +108,54 @@ Known ICD report failures to investigate (likely envelope shape mismatches):
`stores.Teams.IsTeamAdmin`; remove `database` import (cs4)
- [x] Team-scoped KB creation test: member denied, team admin succeeds (cs4)
### v0.28.3 — Security Tier (ICD Runner Red Team)
*Profile (cs5):*
- [x] `profile.md` corrected: avatar API (was `multipart/form-data`, actually JSON
base64), response shapes for all 7 endpoints, auth annotations, field table
- [x] P0 fix: `GET /settings` returns bare object → wrapped in `{"settings": {...}}`
- [x] `profileResponse` shape: add `last_login_at` field + dialect-safe time scan
(`database.ST()` / `database.SNT()`), COALESCE guard on settings column
- [x] Integration test harness: register all 7 profile/settings routes (was only
`GET /profile`), remove stale `/avatar` route aliases
- [x] New `profile_test.go`: GET profile shape, PUT profile (display_name, email,
duplicate email 409), GET/PUT settings envelope, password change (success,
wrong current 401, too short 400), avatar delete
*Workspaces (cs6):*
- [x] `workspaces.md` corrected: workspace object shape (added `indexing_enabled`,
`git_*` fields, `total_bytes` not `storage_bytes`, `owner_type` full enum),
file entry shape (`is_directory`/`size_bytes` not `is_dir`/`size`), git status
full shape, git log envelope (`{"data": [...]}`), git commit request (`paths`
not `files`), archive format query param, auth annotations, all response shapes
- [x] P0 fix: `GET /workspaces` returns bare array → `{"data": [...]}`
- [x] P0 fix: `GET /git-credentials` returns bare array → `{"data": [...]}`
- [x] Fix: `GET .../git/log` returns bare array → `{"data": [...]}` + nil slice guard
- [x] New `workspace_test.go`: List empty envelope, list with data + shape, root_path
not exposed, user isolation, GET by ID shape, not found 404, forbidden 403,
git-credentials empty envelope, auth required (11 tests)
**Remaining audits:**
| Priority | ICD | Known failures | Notes |
|----------|-----|----------------|-------|
| 1 | `projects.md` | 0 | Passed clean, needs full trace |
| 2 | `websocket.md` | — | Document event shapes only (not testable in runner) |
- [ ] Notification handler + store tests (PG + SQLite)
- [ ] WebSocket event contract audit: document `message.created`, `workflow.advanced`,
`presence.changed`, `typing`, `workflow.assigned` event shapes in ICD
- [ ] Final straggler pass: run full ICD runner, fix any remaining failures
### v0.28.3 — Frontend Decomposition + ICD Audit SDK Prep
Frontend JS decomposition and ICD runner hardening. Prepares the codebase
for the SDK layer by untangling the current 15-file global soup.
- [ ] JS dependency audit: map every `window.*` global, cross-file call, init order
- [ ] Extract shared primitives: toast, confirm, theme, API client into importable modules
- [ ] ICD runner gains test tiers: `crud`, `envelope`, `security`, `sdk`
- [ ] Runner coverage target: 100% of ICD-documented endpoints have at least one test
- [ ] Document JS module graph and init sequence
### v0.28.4 — Security Tier (ICD Runner Red Team)
New `security` tier in ICD test runner. Multi-user fixtures already exist.
**Auth boundary:**
@@ -132,7 +184,7 @@ New `security` tier in ICD test runner. Multi-user fixtures already exist.
- [ ] Visitor → escalate to authenticated user via crafted headers
- [ ] Cross-visitor isolation: visitor A's cookie cannot read visitor B's messages
### v0.28.4 — Infrastructure
### v0.28.5 — Infrastructure
- [ ] Virtual scroll for long conversations (prerequisite for heavy task output channels)
- [ ] KB auto-injection: top-K chunk prepend, context budget aware, per-channel toggle
- [ ] Helm chart (replaces raw k8s manifests, `helm install switchboard ./chart`)
@@ -146,7 +198,7 @@ New `security` tier in ICD test runner. Multi-user fixtures already exist.
(`POST /admin/notifications/broadcast`), fan-out to all active users via
`NotifyMany`, admin UI for composing announcements
### v0.28.5 — Frontend SDK
### v0.28.6 — Frontend SDK
`switchboard-sdk.js` — composition layer over existing globals. Surface
authors consume a single coherent API instead of hunting through 15 JS files.