Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
689fecf
cocalc-api: fix docstring style, check with pyright as well, fix/igno…
haraldschilly Sep 27, 2025
ef6b05d
Merge remote-tracking branch 'origin/master' into cocalc-api-20250927
haraldschilly Sep 27, 2025
4cf7197
cocalc-api: add tests, using localhost instance and a given key
haraldschilly Sep 29, 2025
0772810
cocalc-api: implement project deletion in conat and support a full pr…
haraldschilly Sep 29, 2025
c652d60
cocalc-api: testing organization management
haraldschilly Sep 29, 2025
3eb7997
cocalc-api: coverage report, makefile help
haraldschilly Sep 29, 2025
b824e3f
cocalc-api: attempt to integrate into CI
haraldschilly Sep 30, 2025
d24ec9a
server/organizations logic: only allow site-admins to promote users t…
haraldschilly Sep 30, 2025
5faae38
cocalc-api: adjust org tests after changes to organization logic in s…
haraldschilly Sep 30, 2025
200810a
cocalc-api: fix the CI test
haraldschilly Sep 30, 2025
29dc582
cocalc-api: fix tests, creating accounts started many project servers…
haraldschilly Sep 30, 2025
7390989
Merge remote-tracking branch 'origin/master' into cocalc-api-20250927
haraldschilly Oct 1, 2025
1aed72c
cocalc-api: introduce a cleanup logic, of tracked accounts, orgs and …
haraldschilly Oct 1, 2025
95c7274
cocalc-api: jupyter tests, add endpoints to list/stop kernels
haraldschilly Oct 1, 2025
3f27741
Merge remote-tracking branch 'origin/master' into cocalc-api-20250927
haraldschilly Oct 2, 2025
fc0d26a
cocalc-api/tests: jupyterExecute increase timeout + retry logic
haraldschilly Oct 2, 2025
a31cedb
Merge branch 'master' into cocalc-api-20250927
haraldschilly Oct 3, 2025
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
109 changes: 108 additions & 1 deletion .github/workflows/make-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ jobs:
pip install ipykernel
python -m ipykernel install --prefix=./jupyter-local --name python3-local --display-name "Python 3 (Local)"


- name: install pnpm
uses: pnpm/action-setup@v4
with:
Expand All @@ -128,6 +127,114 @@ jobs:
name: "test-results-node-${{ matrix.node-version }}-pg-${{ matrix.pg-version }}"
path: 'src/packages/*/junit.xml'

- name: Start CoCalc Hub
run: |
# Create conat password for hub internal authentication
mkdir -p src/data/secrets
echo "test-conat-password-$(date +%s)" > src/data/secrets/conat-password
chmod 600 src/data/secrets/conat-password

cd src/packages/hub
pnpm run hub-project-dev-nobuild > hub.log 2>&1 &
HUB_PID=$!
echo $HUB_PID > hub.pid
echo "Hub started with PID $HUB_PID"
# Check if process is still running after a moment
sleep 2
if ! kill -0 $HUB_PID 2>/dev/null; then
echo "Error: Hub process died immediately after starting"
echo "Hub log:"
cat hub.log
exit 1
fi
env:
PGDATABASE: smc
PGUSER: smc
PGHOST: localhost
COCALC_MODE: single-user
COCALC_TEST_MODE: yes
DEBUG: 'cocalc:*,-cocalc:silly:*,hub:*,project:*'

- name: Wait for hub readiness
run: |
MAX_ATTEMPTS=30
READY=false
for i in $(seq 1 $MAX_ATTEMPTS); do
if curl -sf --max-time 3 http://localhost:5000/healthcheck > /dev/null; then
echo "Hub is ready"
READY=true
break
fi
echo "Waiting for hub... ($i/$MAX_ATTEMPTS)"
sleep 3
done
if [ "$READY" = "false" ]; then
echo "Hub failed to become ready after $MAX_ATTEMPTS attempts"
echo "Hub log:"
cat src/packages/hub/hub.log || echo "No log file found"
exit 1
fi

- name: Create CI admin user and API key
run: |
cd src/packages/hub
node dist/run/test-create-admin.js > ../../api_key.txt
# Validate API key was created
if [ ! -s ../../api_key.txt ]; then
echo "Error: API key file is empty or missing"
exit 1
fi
API_KEY=$(cat ../../api_key.txt)
if ! echo "$API_KEY" | grep -qE '^sk-[A-Za-z0-9]+$'; then
echo "Error: Invalid API key format: $API_KEY"
exit 1
fi
echo "API key created successfully"
env:
PGDATABASE: smc
PGUSER: smc
PGHOST: localhost

- name: Install uv for cocalc-api tests
run: curl -LsSf https://astral.sh/uv/install.sh | sh && echo "$HOME/.local/bin" >> $GITHUB_PATH

- name: Run cocalc-api tests
run: |
export COCALC_API_KEY=$(cat src/api_key.txt)
export COCALC_HOST=http://localhost:5000
cd src/python/cocalc-api && make ci
env:
PGDATABASE: smc
PGUSER: smc
PGHOST: localhost

- name: Stop CoCalc Hub
if: always()
run: |
if [ -f src/packages/hub/hub.pid ]; then
HUB_PID=$(cat src/packages/hub/hub.pid)
echo "Stopping hub with PID $HUB_PID"
kill $HUB_PID || true
# Wait a bit for graceful shutdown
sleep 5
# Force kill if still running
kill -9 $HUB_PID 2>/dev/null || true
fi

- name: Upload hub logs
uses: actions/upload-artifact@v4
if: always()
with:
name: "hub-logs-node-${{ matrix.node-version }}-pg-${{ matrix.pg-version }}"
path: 'src/packages/hub/hub.log'

- name: Upload cocalc-api test results
uses: actions/upload-artifact@v4
if: always()
with:
name: "cocalc-api-test-results-node-${{ matrix.node-version }}-pg-${{ matrix.pg-version }}"
path: 'src/python/cocalc-api/test-results.xml'

report:
runs-on: ubuntu-latest

Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ src/conf/tinc_hosts/
# mocha: banket coverage report
src/coverage
src/*/coverage

# Python coverage files
.coverage
.coverage.*
htmlcov/
**/htmlcov/
# comes up when testing in that directory
src/rethinkdb_data/
src/dev/project/rethinkdb_data/
Expand Down
6 changes: 6 additions & 0 deletions src/.claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"Bash(git commit:*)",
"Bash(git push:*)",
"Bash(grep:*)",
"Bash(make:*)",
"Bash(node:*)",
"Bash(npm show:*)",
"Bash(npm view:*)",
Expand All @@ -29,6 +30,10 @@
"Bash(pnpm build:*)",
"Bash(pnpm exec tsc:*)",
"Bash(pnpm i18n:*)",
"Bash(pnpm i18n:compile:*)",
"Bash(pnpm i18n:download:*)",
"Bash(pnpm i18n:extract:*)",
"Bash(pnpm i18n:upload:*)",
"Bash(pnpm i18n:*:*)",
"Bash(pnpm info:*)",
"Bash(pnpm list:*)",
Expand All @@ -43,6 +48,7 @@
"Bash(prettier -w:*)",
"Bash(psql:*)",
"Bash(python3:*)",
"Bash(uv:*)",
"Bash(timeout:*)",
"WebFetch",
"WebSearch",
Expand Down
83 changes: 83 additions & 0 deletions src/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ CoCalc is organized as a monorepo with key packages:
5. **Authentication**: Each conat request includes account_id and is subject to permission checks at the hub level
6. **Subjects**: Messages are routed using hierarchical subjects like `hub.account.{uuid}.{service}` or `project.{uuid}.{compute_server_id}.{service}`

#### CoCalc Conat Hub API Architecture

**API Method Registration Pattern:**
- **Registry**: `packages/conat/hub/api/projects.ts` contains `export const projects = { methodName: authFirstRequireAccount }`
- **Implementation**: `packages/server/conat/api/projects.ts` contains `export async function methodName() { ... }`
- **Flow**: Python client `@api_method("projects.methodName")` → POST `/api/conat/hub` → `hubBridge()` → conat subject `hub.account.{account_id}.api` → registry lookup → implementation

**Example - projects.createProject:**
1. **Python**: `@api_method("projects.createProject")` decorator
2. **HTTP**: `POST /api/conat/hub {"name": "projects.createProject", "args": [...]}`
3. **Bridge**: `hubBridge()` routes to conat subject
4. **Registry**: `packages/conat/hub/api/projects.ts: createProject: authFirstRequireAccount`
5. **Implementation**: `packages/server/conat/api/projects.ts: export { createProject }` → `@cocalc/server/projects/create`

### Key Technologies

- **TypeScript**: Primary language for all new code
Expand Down Expand Up @@ -216,10 +230,79 @@ Same flow as above, but **before 3. i18n:upload**, delete the key. Only new keys
- Ignore everything in `node_modules` or `dist` directories
- Ignore all files not tracked by Git, unless they are newly created files

# CoCalc Python API Client Investigation

## Overview

The `python/cocalc-api/` directory contains a Python client library for the CoCalc API, published as the `cocalc-api` package on PyPI.

## Client-Server Architecture Investigation

### API Call Flow

1. **cocalc-api Client** (Python) → HTTP POST requests
2. **Next.js API Routes** (`/api/conat/{hub,project}`) → Bridge to conat messaging
3. **ConatClient** (server-side) → NATS-like messaging protocol
4. **Hub API Implementation** (`packages/conat/hub/api/`) → Actual business logic

### Endpoints Discovered

#### Hub API: `POST /api/conat/hub`
- **Bridge**: `packages/next/pages/api/conat/hub.ts` → `hubBridge()` → conat subject `hub.account.{account_id}.api`
- **Implementation**: `packages/conat/hub/api/projects.ts`
- **Available Methods**: `createProject`, `start`, `stop`, `setQuotas`, `addCollaborator`, `removeCollaborator`, etc.
- **Missing**: ❌ **No `delete` method implemented in conat hub API**

#### Project API: `POST /api/conat/project`
- **Bridge**: `packages/next/pages/api/conat/project.ts` → `projectBridge()` → conat project subjects
- **Implementation**: `packages/conat/project/api/` (system.ping, system.exec, system.jupyterExecute)

### Project Deletion Investigation

#### ✅ Next.js v2 API Route Available
- **Endpoint**: `packages/next/pages/api/v2/projects/delete.ts`
- **Functionality**: Sets deleted=true, removes licenses, stops project
- **Authentication**: Requires collaborator access or admin

#### ❌ Missing Conat Hub API Method
- **Current Methods**: Only CRUD operations (create, start, stop, quotas, collaborators)
- **Gap**: No `delete` method exposed through conat hub API used by cocalc-api

#### Frontend Implementation
- **Location**: `packages/frontend/projects/actions.ts:delete_project()`
- **Method**: Direct database table update via `projects_table_set({deleted: true})`

## Implementation

### Solution Implemented: Direct v2 API Call
- **Added**: `hub.projects.delete(project_id)` method to cocalc-api Python client
- **Implementation**: Direct HTTP POST to `/api/v2/projects/delete` endpoint
- **Reasoning**: Fastest path to complete project lifecycle without requiring conat hub API changes
- **Consistency**: Uses same authentication and error handling patterns as other methods

### Code Changes
1. **`src/cocalc_api/hub.py`**: Added `delete()` method to Projects class
2. **`tests/conftest.py`**: Updated cleanup to use new delete method
3. **`tests/test_hub.py`**: Added test for delete method availability

## Current Status
- ✅ pytest test framework established with automatic project lifecycle
- ✅ Project creation/start/stop working via conat hub API
- ✅ Project deletion implemented by calling v2 API route directly
- ✅ Complete project lifecycle management: create → start → test → stop → delete
- ✅ All 14 tests passing with proper resource cleanup

# Important Instruction Reminders

- Do what has been asked; nothing more, nothing less
- NEVER create files unless they're absolutely necessary for achieving your goal
- ALWAYS prefer editing an existing file to creating a new one
- REFUSE to modify files when the git repository is on the `master` or `main` branch
- NEVER proactively create documentation files (`*.md`) or README files. Only create documentation files if explicitly requested by the User

# important-instruction-reminders
Do what has been asked; nothing more, nothing less.
NEVER create files unless they're absolutely necessary for achieving your goal.
ALWAYS prefer editing an existing file to creating a new one.
NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
ALWAYS ask questions if something is unclear. Only proceed to the implementation step if you have no questions left.
2 changes: 2 additions & 0 deletions src/packages/conat/hub/api/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const projects = {
setQuotas: authFirstRequireAccount,
start: authFirstRequireAccount,
stop: authFirstRequireAccount,
deleteProject: authFirstRequireAccount,
};

export type AddCollaborator =
Expand Down Expand Up @@ -103,4 +104,5 @@ export interface Projects {

start: (opts: { account_id: string; project_id: string }) => Promise<void>;
stop: (opts: { account_id: string; project_id: string }) => Promise<void>;
deleteProject: (opts: { account_id: string; project_id: string }) => Promise<void>;
}
9 changes: 9 additions & 0 deletions src/packages/conat/project/api/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export const system = {

// jupyter stateless API
jupyterExecute: true,

// jupyter kernel management
listJupyterKernels: true,
stopJupyterKernel: true,
};

export interface System {
Expand Down Expand Up @@ -74,4 +78,9 @@ export interface System {
}) => Promise<void>;

jupyterExecute: (opts: ProjectJupyterApiOptions) => Promise<object[]>;

listJupyterKernels: () => Promise<
{ pid: number; connectionFile: string; kernel_name?: string }[]
>;
stopJupyterKernel: (opts: { pid: number }) => Promise<{ success: boolean }>;
}
69 changes: 69 additions & 0 deletions src/packages/hub/run/test-create-admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env node

/*
* Script to create a test admin account and API key for CI testing.
* This is used in GitHub Actions to set up cocalc-api tests.
*/

import { v4 as uuidv4 } from "uuid";

import createAccount from "@cocalc/server/accounts/create-account";
import manageApiKeys from "@cocalc/server/api/manage";
import getPool from "@cocalc/database/pool";

async function main() {
const account_id = uuidv4();
const email = "[email protected]";
const password = "testpassword"; // dummy password
const firstName = "CI";
const lastName = "Admin";

console.error(`Creating admin account ${account_id}...`);

// Create the account
await createAccount({
email,
password,
firstName,
lastName,
account_id,
tags: [],
signupReason: "CI testing",
noFirstProject: true,
});

// Set as admin
const pool = getPool();
await pool.query("UPDATE accounts SET groups=$1 WHERE account_id=$2", [
["admin"],
account_id,
]);

console.error("Creating API key...");

// Create API key
const keys = await manageApiKeys({
account_id,
action: "create",
name: "ci-testing",
});

if (!keys || keys.length === 0) {
throw new Error("Failed to create API key");
}

const apiKey = keys[0];
if (!apiKey.secret) {
throw new Error("API key secret is missing");
}
console.error(`API key created with id=${apiKey.id}: ${apiKey.secret}`);
console.error(`Last 6 chars: ${apiKey.secret.slice(-6)}`);

// Output the key for CI
process.stdout.write(apiKey.secret);
}

main().catch((err) => {
console.error("Error:", err);
process.exit(1);
});
Loading
Loading