Skip to content

Commit 5a46c35

Browse files
committed
feat: shared API_TOKEN, qwen oauth in container, bot /getToken
- Accept API_TOKEN for MCP and tunnel (apiToken query param) - Wrap qwen-search image with oauth creds decode and qwen CLI invocation - Pass PROD_QWEN_OAUTH_CREDS into container runs; deploy reads from env - Add Telegram /getToken command for standalone package auth Made-with: Cursor
1 parent ff43c70 commit 5a46c35

19 files changed

Lines changed: 173 additions & 22 deletions

docker/qwen-search/Dockerfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ RUN npm install -g @qwen-code/qwen-code@latest
1212

1313
WORKDIR /workspace
1414

15+
# Runtime wrapper creates ~/.qwen/oauth_creds.json from env and runs qwen.
16+
COPY docker/qwen-search/entrypoint.sh /usr/local/bin/qwen-entrypoint
17+
COPY docker/qwen-search/qwen-search.sh /usr/local/bin/qwen-search
18+
RUN chmod +x /usr/local/bin/qwen-entrypoint /usr/local/bin/qwen-search
19+
1520
# Default corpus mount point (host maps repo/api/knowledge here with :ro)
1621
VOLUME ["/corpus"]
1722

18-
ENTRYPOINT ["qwen"]
23+
ENTRYPOINT ["qwen-entrypoint"]

docker/qwen-search/README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Qwen search container (SpawnDock API / MCP)
22

3-
OCI image that installs **@qwen-code/qwen-code** and runs `qwen` as `ENTRYPOINT`. The API server (`repo/api`) invokes this image for `QWEN_MODE=container` MCP search.
3+
OCI image that installs **@qwen-code/qwen-code** and runs `qwen-search` as `ENTRYPOINT`. The API server (`repo/api`) invokes this image for `QWEN_MODE=container` MCP search.
44

55
## Build
66

@@ -10,6 +10,12 @@ From `repo/api`:
1010
docker build -t spawndock/qwen-search:local -f docker/qwen-search/Dockerfile .
1111
```
1212

13+
## Runtime contract
14+
15+
- `ENTRYPOINT` is `qwen-search` (wrapper over `qwen --output-format json --prompt "<TERM>"`).
16+
- At container start it decodes `PROD_QWEN_OAUTH_CREDS` (or `QWEN_OAUTH_CREDS_B64`) into `~/.qwen/oauth_creds.json`.
17+
- Knowledge directory is mounted read-only.
18+
1319
## Volume contract (read-only)
1420

1521
| Host path | Container path | Mode |
@@ -25,9 +31,9 @@ Replace `/abs/path/to/repo/api/knowledge` with your checkout:
2531
```bash
2632
docker run --rm \
2733
-v /abs/path/to/repo/api/knowledge:/corpus:ro \
28-
-e QWEN_OAUTH=true \
34+
-e PROD_QWEN_OAUTH_CREDS="$(cat ~/.qwen/oauth_creds.json | base64 -w0)" \
2935
spawndock/qwen-search:local \
30-
--help
36+
--term "how to build TON mini app auth flow"
3137
```
3238

3339
Pass auth / API keys only via `-e` as required by your Qwen CLI setup; do not bake secrets into the image.

docker/qwen-search/entrypoint.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env sh
2+
set -eu
3+
4+
ensure_oauth_file() {
5+
creds_b64="${PROD_QWEN_OAUTH_CREDS:-${QWEN_OAUTH_CREDS_B64:-}}"
6+
if [ -z "${creds_b64}" ]; then
7+
return 0
8+
fi
9+
10+
mkdir -p "${HOME}/.qwen"
11+
printf "%s" "${creds_b64}" | base64 -d > "${HOME}/.qwen/oauth_creds.json"
12+
chmod 600 "${HOME}/.qwen/oauth_creds.json"
13+
}
14+
15+
ensure_oauth_file
16+
exec /usr/local/bin/qwen-search "$@"

docker/qwen-search/qwen-search.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env sh
2+
set -eu
3+
4+
prompt=""
5+
output_format="json"
6+
7+
while [ "$#" -gt 0 ]; do
8+
case "$1" in
9+
--term|--prompt|-p)
10+
if [ "$#" -lt 2 ]; then
11+
echo "Missing value for $1" >&2
12+
exit 2
13+
fi
14+
prompt="$2"
15+
shift 2
16+
;;
17+
--output-format)
18+
if [ "$#" -lt 2 ]; then
19+
echo "Missing value for --output-format" >&2
20+
exit 2
21+
fi
22+
output_format="$2"
23+
shift 2
24+
;;
25+
*)
26+
shift
27+
;;
28+
esac
29+
done
30+
31+
if [ -z "$prompt" ]; then
32+
echo "Missing --term/--prompt value" >&2
33+
exit 2
34+
fi
35+
36+
exec qwen --output-format "$output_format" --prompt "$prompt"

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/deploy-prod.sh

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
33

4-
if [[ $# -lt 3 ]]; then
5-
echo "Usage: $0 <ssh-password> <qwen-oauth-creds-b64> <telegram-bot-token> [ssh-host] [ssh-user] [public-origin]" >&2
4+
if [[ $# -lt 2 ]]; then
5+
echo "Usage: $0 <ssh-password> <telegram-bot-token> [ssh-host] [ssh-user] [public-origin]" >&2
6+
echo "Required env: PROD_QWEN_OAUTH_CREDS (base64 of ~/.qwen/oauth_creds.json)" >&2
67
exit 1
78
fi
89

910
SSH_PASSWORD="$1"
10-
QWEN_OAUTH_CREDS_B64="$2"
11-
TELEGRAM_BOT_TOKEN="$3"
12-
SSH_HOST="${4-spawn-dock.w3voice.net}"
13-
SSH_USER="${5:-ops}"
14-
PUBLIC_ORIGIN="${6:-https://spawn-dock.w3voice.net}"
11+
TELEGRAM_BOT_TOKEN="$2"
12+
SSH_HOST="${3-spawn-dock.w3voice.net}"
13+
SSH_USER="${4:-ops}"
14+
PUBLIC_ORIGIN="${5:-https://spawn-dock.w3voice.net}"
15+
QWEN_OAUTH_CREDS_B64="${PROD_QWEN_OAUTH_CREDS:-}"
16+
if [[ -z "$QWEN_OAUTH_CREDS_B64" ]]; then
17+
echo "PROD_QWEN_OAUTH_CREDS is required" >&2
18+
exit 1
19+
fi
1520
TARGET_DIR="/srv/spawndock-api"
1621
BOT_SECRET="$(openssl rand -hex 24)"
1722
BOT_CONTROL_PLANE_URL="http://mcp-server:3000"
@@ -69,6 +74,7 @@ QWEN_CODE_COMMAND=qwen
6974
QWEN_CODE_AUTH_TYPE=qwen-oauth
7075
QWEN_CONTAINER_IMAGE=spawndock/qwen-search:prod
7176
QWEN_KNOWLEDGE_HOST_PATH=${TARGET_DIR}/knowledge
77+
PROD_QWEN_OAUTH_CREDS=${QWEN_OAUTH_CREDS_B64}
7278
QWEN_OAUTH_CREDS_B64=${QWEN_OAUTH_CREDS_B64}
7379
HF_TOKEN=
7480
EOF

src/__tests__/bot-commands.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ describe("parseCommand", () => {
3333
it("parses unknown /lang code as lang-invalid", () => {
3434
expect(parseCommand("/lang de")).toEqual({ tag: "lang-invalid", arg: "de" });
3535
});
36+
it("parses /getToken", () => {
37+
expect(parseCommand("/getToken")).toEqual({ tag: "get-token" });
38+
});
3639
it("returns unknown for random text", () => {
3740
expect(parseCommand("hello").tag).toBe("unknown");
3841
});

src/__tests__/mcp-auth.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest";
22
import { authenticateMcpRequest, findDeviceCredentialByMcpApiKey, readMcpApiKey } from "../mcp-auth.js";
33
import type { StoreState } from "../types.js";
44

5+
delete process.env.API_TOKEN;
6+
57
const state: StoreState = {
68
projects: [],
79
pairingTokens: [],

src/__tests__/qwen-container.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ describe("runQwenSearchInContainer", () => {
8686
"-v",
8787
expect.stringMatching(/\/host\/knowledge:\/corpus:ro$/),
8888
"spawndock/qwen-search:local",
89-
"-p",
89+
"--term",
9090
"test prompt",
9191
"--output-format",
9292
"json",

src/bot/commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type BotCommand =
1111
| { tag: "lang-help" }
1212
| { tag: "lang-set"; locale: BotLocale }
1313
| { tag: "lang-invalid"; arg: string }
14+
| { tag: "get-token" }
1415
| { tag: "unknown"; input: string };
1516

1617
export function parseCommand(input: string): BotCommand {
@@ -31,6 +32,9 @@ export function parseCommand(input: string): BotCommand {
3132
const slug = text.slice(7).trim();
3233
return slug ? { tag: "launch", slug } : { tag: "launch-missing" };
3334
}
35+
if (text === "/getToken" || text === "/gettoken") {
36+
return { tag: "get-token" };
37+
}
3438
return { tag: "unknown", input: text };
3539
}
3640

0 commit comments

Comments
 (0)