Skip to content

Commit c15a107

Browse files
committed
cocalc-api: improve api key type discovery
1 parent efdadad commit c15a107

File tree

6 files changed

+36
-41
lines changed

6 files changed

+36
-41
lines changed

src/packages/conat/hub/api/system.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ export interface System {
3232
getCustomize: (fields?: string[]) => Promise<Customize>;
3333
// ping server and get back the current time
3434
ping: () => { now: number };
35-
// test API key and return scope information (account_id or project_id) and server time
36-
test: () => Promise<{ account_id?: string; project_id?: string; server_time: number }>;
35+
// test API key and return scope information (account_id) and server time
36+
test: () => Promise<{ account_id: string; server_time: number }>;
3737
// terminate a service:
3838
// - only admin can do this.
3939
// - useful for development

src/packages/next/pages/api/conat/project.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,6 @@ export default async function handle(req, res) {
5858
args,
5959
timeout,
6060
});
61-
// For project-scoped API keys, include the project_id in the response
62-
// so the client can discover it
63-
if (project_id0 && !resp.project_id) {
64-
resp.project_id = project_id0;
65-
}
6661
res.json(resp);
6762
} catch (err) {
6863
res.json({ error: err.message });

src/packages/server/api/project-bridge.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { projectSubject } from "@cocalc/conat/names";
44
import { conat } from "@cocalc/backend/conat";
55
import { type Client as ConatClient } from "@cocalc/conat/core/client";
66
import { getProject } from "@cocalc/server/projects/control";
7+
78
const DEFAULT_TIMEOUT = 15000;
89

910
let client: ConatClient | null = null;
@@ -58,10 +59,17 @@ async function callProject({
5859
await project.start();
5960
}
6061

61-
// For system.test(), inject project_id into args[0] if not already present
62+
// For discovery-style calls, inject identifiers so the project can report scope
6263
let finalArgs = args;
63-
if (name === "system.test" && (!args || args.length === 0)) {
64-
finalArgs = [{ project_id }];
64+
if (name === "system.test") {
65+
if (!args || args.length === 0 || typeof args[0] !== "object") {
66+
finalArgs = [{}];
67+
}
68+
if (finalArgs[0] == null || typeof finalArgs[0] !== "object") {
69+
finalArgs = [{ project_id }];
70+
} else {
71+
finalArgs = [{ ...finalArgs[0], project_id }];
72+
}
6573
}
6674
const data = { name, args: finalArgs };
6775
// we use waitForInterest because often the project hasn't

src/packages/server/conat/api/system.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,13 @@ export function ping() {
2020

2121
export async function test({
2222
account_id,
23-
project_id,
24-
}: { account_id?: string; project_id?: string } = {}) {
23+
}: { account_id?: string } = {}) {
2524
// Return API key scope information and server time
26-
// The authFirst decorator determines the scope from the API key and injects
27-
// either account_id (for account-scoped keys) or project_id (for project-scoped keys)
28-
// into this parameter object.
29-
const response: { account_id?: string; project_id?: string; server_time: number } = {
25+
// The authFirst decorator determines the scope from the API key and injects account_id.
26+
const response: { account_id: string; server_time: number } = {
27+
account_id: account_id ?? "",
3028
server_time: Date.now(),
3129
};
32-
if (account_id) {
33-
response.account_id = account_id;
34-
}
35-
if (project_id) {
36-
response.project_id = project_id;
37-
}
3830
return response;
3931
}
4032

src/packages/server/projects/control/single-user.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,9 @@ class Project extends BaseProject {
156156

157157
// First attempt: graceful shutdown with SIGTERM
158158
// This allows the process to clean up child processes (e.g., Jupyter kernels)
159-
let usedSigterm = false;
159+
const stopStartedAt = Date.now();
160+
const SIGKILL_GRACE_MS = 5000;
161+
let sigkillSent = false;
160162
const killProject = (signal: NodeJS.Signals = "SIGTERM") => {
161163
try {
162164
logger.debug(`stop: sending kill -${pid} with ${signal}`);
@@ -169,16 +171,15 @@ class Project extends BaseProject {
169171

170172
// Try SIGTERM first for graceful shutdown
171173
killProject("SIGTERM");
172-
usedSigterm = true;
173174

174175
await this.wait({
175176
until: async () => {
176177
if (await isProjectRunning(this.HOME)) {
177-
// After 5 seconds, escalate to SIGKILL
178-
if (usedSigterm) {
178+
// After a grace period, escalate to SIGKILL
179+
if (!sigkillSent && Date.now() - stopStartedAt >= SIGKILL_GRACE_MS) {
179180
logger.debug("stop: escalating to SIGKILL");
180181
killProject("SIGKILL");
181-
usedSigterm = false;
182+
sigkillSent = true;
182183
}
183184
return false;
184185
} else {

src/python/cocalc-api/src/cocalc_api/mcp/mcp_server.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,29 +107,28 @@ def check_api_key_scope(api_key: str, host: str) -> dict[str, str]:
107107
Raises:
108108
RuntimeError: If the API key is invalid or scope cannot be determined
109109
"""
110+
# First, try hub.system.test (account-scoped keys will succeed here)
110111
try:
111112
hub = Hub(api_key=api_key, host=host)
112-
113-
# Try the hub.system.test() method (only works for account-scoped keys)
114113
result = hub.system.test()
115-
116-
# Check which scope is returned
117-
if "account_id" in result and result["account_id"]:
114+
if result.get("account_id"):
118115
return {"account_id": result["account_id"]}
119-
elif "project_id" in result and result["project_id"]:
116+
if result.get("project_id"):
120117
return {"project_id": result["project_id"]}
121-
else:
122-
raise RuntimeError("API key test returned neither account_id nor project_id")
118+
except Exception:
119+
# Fall back to project-side system.test for project-scoped keys
120+
pass
123121

122+
try:
123+
project = Project(api_key=api_key, host=host)
124+
result = project.system.test()
125+
if result.get("project_id"):
126+
return {"project_id": result["project_id"]}
124127
except Exception as e:
125-
# Check if this looks like a project-scoped key error
126-
error_msg = str(e)
127-
if "must be signed in and MUST provide an api key" in error_msg:
128-
raise RuntimeError("API key appears to be project-scoped. "
129-
"Project-scoped keys require the project_id to be specified at the OS level. "
130-
"Please set the COCALC_PROJECT_ID environment variable and try again.") from e
131128
raise RuntimeError(f"API key validation failed: {e}") from e
132129

130+
raise RuntimeError("API key test returned neither account_id nor project_id")
131+
133132

134133
# Initialize FastMCP server with instructions and documentation
135134
mcp = FastMCP(

0 commit comments

Comments
 (0)