Skip to content

Commit a4427f2

Browse files
authored
Merge pull request #381 from Extra-Chill/fix/380-kimaki-credential-seeding
Seed external Kimaki credentials through the managed adapter
2 parents 1d7650e + 3a21f8f commit a4427f2

7 files changed

Lines changed: 211 additions & 2 deletions

File tree

.github/workflows/shell.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ jobs:
230230
- homeboy-components
231231
- homeboy-dmc-provider
232232
- homeboy-project-id
233+
- kimaki-credential-seeding
233234
- kimaki-install-existing
234235
- kimaki-launchd-start
235236
- kimaki-managed-plugin-rig

bridges/kimaki.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ bridge_install() {
169169
fi
170170

171171
if [ "${EXTERNAL_WORDPRESS:-false}" = true ]; then
172+
_kimaki_seed_external_credential
172173
log "External WordPress profile: Kimaki installed. Start it from the runtime environment with:"
173174
log " WP_CONTROL_TRANSPORT_JSON='<argv-json>' $(external_wordpress_kimaki_command)"
174175
elif [ "$LOCAL_MODE" = true ] && [ "$PLATFORM" = "mac" ]; then
@@ -186,6 +187,17 @@ bridge_install() {
186187
_kimaki_register_runtime_signature
187188
}
188189

190+
_kimaki_seed_external_credential() {
191+
[ -n "${KIMAKI_BOT_TOKEN:-}" ] || return 0
192+
[ -n "${KIMAKI_DATA_DIR:-}" ] || error "KIMAKI_DATA_DIR is required to seed an external Kimaki credential"
193+
194+
local helper
195+
helper="$(external_wordpress_kimaki_credential_command)"
196+
[ "${DRY_RUN:-false}" = true ] || [ -x "$helper" ] || error "Managed Kimaki credential helper is unavailable: $helper"
197+
run_cmd node "$helper"
198+
log "External Kimaki credential configured"
199+
}
200+
189201
# _kimaki_register_cli_channel
190202
#
191203
# Register kimaki with the wp-coding-agents CLI transport runtime so that

lib/external-wordpress.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ external_wordpress_kimaki_command() {
1313
printf '%s' "$(runtime_project_root)/.wp-coding-agents/bin/kimaki"
1414
}
1515

16+
external_wordpress_kimaki_credential_command() {
17+
printf '%s' "$(runtime_project_root)/.wp-coding-agents/bin/kimaki-seed-credential"
18+
}
19+
1620
external_wordpress_prepare_transport() {
1721
[ "${EXTERNAL_WORDPRESS:-false}" = true ] || return 0
1822
[ -n "${RUNTIME_PROJECT_ROOT:-}" ] || error "--external-wordpress requires --runtime-project-root or RUNTIME_PROJECT_ROOT"
@@ -37,9 +41,10 @@ PY
3741
if [ "${DRY_RUN:-false}" != true ]; then
3842
mkdir -p "$RUNTIME_PROJECT_ROOT"
3943
RUNTIME_PROJECT_ROOT=$(cd "$RUNTIME_PROJECT_ROOT" && pwd)
40-
local control_dir control_command kimaki_command profile_file
44+
local control_dir control_command kimaki_command kimaki_credential_command profile_file
4145
control_command="$(external_wordpress_control_command)"
4246
kimaki_command="$(external_wordpress_kimaki_command)"
47+
kimaki_credential_command="$(external_wordpress_kimaki_credential_command)"
4348
control_dir="${control_command%/*}"
4449
profile_file="$(runtime_project_root)/.wp-coding-agents/wordpress.json"
4550
if [ -L "$(runtime_project_root)/.wp-coding-agents" ]; then
@@ -48,8 +53,10 @@ PY
4853
mkdir -p "$control_dir"
4954
cp "$SCRIPT_DIR/scripts/wp-control-transport.py" "$control_command"
5055
cp "$SCRIPT_DIR/scripts/external-wordpress-kimaki.py" "$kimaki_command"
56+
cp "$SCRIPT_DIR/scripts/seed-kimaki-credential.mjs" "$kimaki_credential_command"
5157
chmod 0755 "$control_command"
5258
chmod 0755 "$kimaki_command"
59+
chmod 0755 "$kimaki_credential_command"
5360
python3 - "$profile_file" "$WORDPRESS_PATH" "${WORDPRESS_USER:-}" <<'PY'
5461
import json, sys
5562
path, wordpress_path, wordpress_user = sys.argv[1:]

scripts/seed-kimaki-credential.mjs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env node
2+
3+
import { execFileSync } from "node:child_process";
4+
import path from "node:path";
5+
import { pathToFileURL } from "node:url";
6+
7+
const token = process.env.KIMAKI_BOT_TOKEN ?? "";
8+
if (!token) {
9+
throw new Error("KIMAKI_BOT_TOKEN is required");
10+
}
11+
if (!process.env.KIMAKI_DATA_DIR) {
12+
throw new Error("KIMAKI_DATA_DIR is required");
13+
}
14+
15+
let packageRoot = process.env.KIMAKI_PACKAGE_ROOT;
16+
if (!packageRoot) {
17+
const npmRoot = execFileSync("npm", ["root", "-g"], {
18+
encoding: "utf8",
19+
stdio: ["ignore", "pipe", "ignore"],
20+
}).trim();
21+
packageRoot = path.join(npmRoot, "kimaki", "dist");
22+
}
23+
24+
const { setDataDir } = await import(
25+
pathToFileURL(path.join(packageRoot, "config.js"))
26+
);
27+
const { initDatabase, setBotMode, setBotToken } = await import(
28+
pathToFileURL(path.join(packageRoot, "database.js"))
29+
);
30+
31+
setDataDir(process.env.KIMAKI_DATA_DIR);
32+
await initDatabase();
33+
34+
const separator = token.indexOf(":");
35+
if (separator > 0 && separator < token.length - 1) {
36+
await setBotMode({
37+
appId: process.env.KIMAKI_GATEWAY_APP_ID ?? "1477605701202481173",
38+
clientId: token.slice(0, separator),
39+
clientSecret: token.slice(separator + 1),
40+
mode: "gateway",
41+
proxyUrl:
42+
process.env.KIMAKI_GATEWAY_PROXY_REST_URL ??
43+
"https://slack-gateway.kimaki.dev",
44+
});
45+
} else {
46+
const [encodedAppId] = token.split(".", 1);
47+
const appId =
48+
process.env.KIMAKI_BOT_APP_ID ??
49+
Buffer.from(encodedAppId, "base64").toString("utf8");
50+
if (!/^\d{17,20}$/.test(appId)) {
51+
throw new Error("KIMAKI_BOT_TOKEN does not contain a valid application ID");
52+
}
53+
await setBotToken(appId, token);
54+
}

setup.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,10 +420,15 @@ ENVIRONMENT VARIABLES:
420420
AI_GATEWAY_API_MODEL_ID Model ID sent to WP AI Gateway
421421
AI_GATEWAY_SITE_URL Public site URL for OPENAI_BASE_URL override
422422
WITH_CLAUDE_CODE_AUTH false to skip direct OpenCode Claude Pro/Max auth
423-
KIMAKI_BOT_TOKEN Discord bot token (skip interactive setup)
423+
KIMAKI_BOT_TOKEN Bot token or gateway clientId:clientSecret
424+
(skip interactive setup)
424425
KIMAKI_UNIT Kimaki systemd unit (default: kimaki.service)
425426
KIMAKI_DATA_DIR Kimaki state directory
426427
KIMAKI_LOCK_PORT Kimaki lock port
428+
KIMAKI_PACKAGE_ROOT Kimaki dist directory used for external credential setup
429+
KIMAKI_GATEWAY_APP_ID Gateway application ID override
430+
KIMAKI_GATEWAY_PROXY_REST_URL
431+
Gateway REST URL override
427432
TELEGRAM_BOT_TOKEN Telegram bot token from @BotFather (--chat telegram)
428433
TELEGRAM_ALLOWED_USER_ID Numeric Telegram user ID (--chat telegram)
429434
OPENCODE_MODEL_PROVIDER Default model provider for Telegram bot (default: opencode)

tests/external-wordpress-runtime.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ opencode_project_subagents
144144
[ "$(cat "$RUNTIME_PROJECT_ROOT/.wp-coding-agents/context/agent/SOUL.md")" = "agent context" ] || { echo "FAIL: agent context not projected"; exit 1; }
145145
[ -f "$RUNTIME_PROJECT_ROOT/.opencode/skills/upgrade-wp-coding-agents/SKILL.md" ] || { echo "FAIL: skills were not installed below runtime root"; exit 1; }
146146
[ -f "$RUNTIME_PROJECT_ROOT/.kimaki/kimaki-config/plugins/dm-context-filter.ts" ] || { echo "FAIL: Kimaki config was not installed below runtime root"; exit 1; }
147+
[ -x "$RUNTIME_PROJECT_ROOT/.wp-coding-agents/bin/kimaki-seed-credential" ] || { echo "FAIL: managed Kimaki credential helper was not installed"; exit 1; }
147148
[ ! -e "$RUNTIME_PROJECT_ROOT/wp-content" ] || { echo "FAIL: WordPress-side files were written below runtime root"; exit 1; }
148149
[ -L "$RUNTIME_PROJECT_ROOT/.wp-coding-agents/context" ] || { echo "FAIL: projected context is not atomically activated"; exit 1; }
149150
[ -f "$RUNTIME_PROJECT_ROOT/.opencode/agents/writer.md" ] || { echo "FAIL: embedded writer was not projected"; exit 1; }

tests/kimaki-credential-seeding.sh

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/bin/bash
2+
set -eu
3+
4+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
5+
TMP="$(mktemp -d)"
6+
trap 'rm -rf "$TMP"' EXIT
7+
PACKAGE_ROOT="$TMP/kimaki/dist"
8+
mkdir -p "$PACKAGE_ROOT"
9+
10+
cat > "$TMP/kimaki/package.json" <<'JSON'
11+
{"type":"module"}
12+
JSON
13+
cat > "$PACKAGE_ROOT/config.js" <<'JS'
14+
export function setDataDir(value) {
15+
globalThis.dataDir = value;
16+
}
17+
JS
18+
cat > "$PACKAGE_ROOT/database.js" <<'JS'
19+
import { writeFile } from "node:fs/promises";
20+
21+
export async function initDatabase() {
22+
globalThis.initialized = true;
23+
}
24+
export async function setBotMode(value) {
25+
await writeFile(process.env.TEST_RESULT, JSON.stringify({
26+
operation: "gateway",
27+
dataDir: globalThis.dataDir,
28+
initialized: globalThis.initialized,
29+
value,
30+
}));
31+
}
32+
export async function setBotToken(appId, token) {
33+
await writeFile(process.env.TEST_RESULT, JSON.stringify({
34+
operation: "bot",
35+
dataDir: globalThis.dataDir,
36+
initialized: globalThis.initialized,
37+
appId,
38+
token,
39+
}));
40+
}
41+
JS
42+
43+
export KIMAKI_PACKAGE_ROOT="$PACKAGE_ROOT"
44+
export KIMAKI_DATA_DIR="$TMP/state"
45+
export TEST_RESULT="$TMP/result.json"
46+
47+
gateway_token='gateway-client:gateway-secret'
48+
export KIMAKI_BOT_TOKEN="$gateway_token"
49+
output="$(node "$ROOT/scripts/seed-kimaki-credential.mjs" 2>&1)"
50+
[ -z "$output" ] || { echo "FAIL: credential helper produced output"; exit 1; }
51+
python3 - "$TEST_RESULT" "$KIMAKI_DATA_DIR" <<'PY'
52+
import json, sys
53+
data = json.load(open(sys.argv[1]))
54+
assert data == {
55+
"operation": "gateway",
56+
"dataDir": sys.argv[2],
57+
"initialized": True,
58+
"value": {
59+
"appId": "1477605701202481173",
60+
"clientId": "gateway-client",
61+
"clientSecret": "gateway-secret",
62+
"mode": "gateway",
63+
"proxyUrl": "https://slack-gateway.kimaki.dev",
64+
},
65+
}
66+
PY
67+
68+
bot_token="$(printf '123456789012345678' | base64 | tr -d '\n').fixture.signature"
69+
export KIMAKI_BOT_TOKEN="$bot_token"
70+
output="$(node "$ROOT/scripts/seed-kimaki-credential.mjs" 2>&1)"
71+
[ -z "$output" ] || { echo "FAIL: credential helper produced output"; exit 1; }
72+
python3 - "$TEST_RESULT" "$KIMAKI_DATA_DIR" "$bot_token" <<'PY'
73+
import json, sys
74+
data = json.load(open(sys.argv[1]))
75+
assert data == {
76+
"operation": "bot",
77+
"dataDir": sys.argv[2],
78+
"initialized": True,
79+
"appId": "123456789012345678",
80+
"token": sys.argv[3],
81+
}
82+
PY
83+
84+
unset KIMAKI_BOT_TOKEN
85+
if node "$ROOT/scripts/seed-kimaki-credential.mjs" >"$TMP/missing.out" 2>"$TMP/missing.err"; then
86+
echo "FAIL: missing credential was accepted"
87+
exit 1
88+
fi
89+
grep -F 'KIMAKI_BOT_TOKEN is required' "$TMP/missing.err" >/dev/null
90+
91+
# The external bridge owns invocation; callers only supply the credential and
92+
# selected Kimaki data directory.
93+
mkdir -p "$TMP/runtime/.wp-coding-agents/bin" "$TMP/bin"
94+
cp "$ROOT/scripts/seed-kimaki-credential.mjs" \
95+
"$TMP/runtime/.wp-coding-agents/bin/kimaki-seed-credential"
96+
chmod +x "$TMP/runtime/.wp-coding-agents/bin/kimaki-seed-credential"
97+
cat > "$TMP/bin/kimaki" <<'SH'
98+
#!/bin/sh
99+
exit 0
100+
SH
101+
chmod +x "$TMP/bin/kimaki"
102+
103+
export PATH="$TMP/bin:$PATH"
104+
export RUNTIME_PROJECT_ROOT="$TMP/runtime"
105+
export KIMAKI_BOT_TOKEN="$gateway_token"
106+
export EXTERNAL_WORDPRESS=true LOCAL_MODE=true DRY_RUN=false
107+
export SERVICE_HOME="$TMP/home"
108+
UPDATED_ITEMS=()
109+
log() { printf '%s\n' "$*" >> "$TMP/bridge.log"; }
110+
run_cmd() { "$@"; }
111+
error() { printf '%s\n' "$*" >&2; return 1; }
112+
external_wordpress_kimaki_command() { printf '%s' "$TMP/runtime/.wp-coding-agents/bin/kimaki"; }
113+
external_wordpress_kimaki_credential_command() { printf '%s' "$TMP/runtime/.wp-coding-agents/bin/kimaki-seed-credential"; }
114+
115+
# shellcheck disable=SC1091
116+
source "$ROOT/bridges/kimaki.sh"
117+
_kimaki_sync_bin_helpers() { :; }
118+
rm -f "$TEST_RESULT"
119+
bridge_install
120+
python3 - "$TEST_RESULT" <<'PY'
121+
import json, sys
122+
assert json.load(open(sys.argv[1]))["operation"] == "gateway"
123+
PY
124+
if grep -F "$gateway_token" "$TMP/bridge.log" >/dev/null; then
125+
echo "FAIL: bridge logged the gateway credential"
126+
exit 1
127+
fi
128+
129+
echo "PASS: tests/kimaki-credential-seeding.sh"

0 commit comments

Comments
 (0)