Skip to content

feat(skills): add Kiro local account importer#17

Merged
acking-you merged 1 commit into
masterfrom
fix/kiro-local-import-bridge
May 31, 2026
Merged

feat(skills): add Kiro local account importer#17
acking-you merged 1 commit into
masterfrom
fix/kiro-local-import-bridge

Conversation

@acking-you
Copy link
Copy Markdown
Owner

Summary

Add a local Kiro account import skill that reads Kiro CLI auth SQLite files on the operator machine and calls the locally mapped llm-access admin API directly, without involving the StaticFlow backend.

What changed

  • Added kiro-local-account-importer skill docs and agent metadata.
  • Added scripts/import_kiro_accounts.py to discover SQLite files, parse Kiro auth_kv records, create accounts through /admin/kiro-gateway/accounts, assign fixed United States proxies, and refresh balances before accepting imports.
  • Enforced the standard import policy: max concurrency 3, randomized start interval in 200..1000ms, minimum remaining credits >= 10, no direct/no-proxy imports, and duplicate upstream user_id deletion.
  • Added focused Python unit tests for US proxy filtering, minimum credit policy, balance validation deletion, and duplicate account deletion.

Test plan

  • python3 -m unittest skills/kiro-local-account-importer/tests/test_import_kiro_accounts.py
  • python3 -m py_compile skills/kiro-local-account-importer/scripts/import_kiro_accounts.py
  • python3 /home/ts_user/.codex/skills/.system/skill-creator/scripts/quick_validate.py skills/kiro-local-account-importer

Notes

No Rust crates are affected by this PR, so no Cargo build or clippy run was needed.

@acking-you acking-you merged commit d832605 into master May 31, 2026
3 checks passed
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the kiro-local-account-importer skill, which enables safe local importing of Kiro CLI accounts into StaticFlow. It includes a Python script to discover and parse local SQLite databases, apply scheduling and proxy configurations, and validate accounts against active US proxies. The feedback highlights several critical areas to improve the script's robustness and correctness, such as fixing a loop-control bug during proxy validation, handling potential database and network exceptions, removing dead code, and preventing crashes from null database values.

Comment on lines +443 to +447
if not isinstance(balance, dict) or not balance_has_minimum_remaining(
balance, args.minimum_remaining_credits
):
attempt["error"] = "remaining credits below minimum"
break
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If balance is not a dictionary (for example, if the API returns an empty response or None), the current check will set the error to 'remaining credits below minimum' and break out of the proxy validation loop entirely. This prevents the script from trying other active proxies. Instead, an invalid/non-dict response should be treated as a proxy/network failure (continuing to the next proxy), and the loop should only be broken if we successfully retrieved a valid balance dictionary and confirmed that the credits are below the minimum.

Suggested change
if not isinstance(balance, dict) or not balance_has_minimum_remaining(
balance, args.minimum_remaining_credits
):
attempt["error"] = "remaining credits below minimum"
break
if not isinstance(balance, dict):
attempt["error"] = "invalid balance response"
continue
if not balance_has_minimum_remaining(
balance, args.minimum_remaining_credits
):
attempt["error"] = "remaining credits below minimum"
break

Comment on lines +422 to +425
created = request_json(
"POST", args.admin_base_url, "/admin/kiro-gateway/accounts", args.admin_token, body
)
result["created_name"] = created.get("name") if isinstance(created, dict) else auth.name
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure robustness against any server-side normalization or modification of the account name during creation, we should use the created_name returned by the server for all subsequent operations (such as PATCH, POST /balance, and DELETE) instead of assuming it is exactly auth.name.

Suggested change
created = request_json(
"POST", args.admin_base_url, "/admin/kiro-gateway/accounts", args.admin_token, body
)
result["created_name"] = created.get("name") if isinstance(created, dict) else auth.name
created = request_json(
"POST", args.admin_base_url, "/admin/kiro-gateway/accounts", args.admin_token, body
)
created_name = created.get("name") if isinstance(created, dict) else auth.name
result["created_name"] = created_name

Comment on lines +271 to +282
def choose_proxy(
proxies: list[dict[str, Any]],
account_counts: dict[str, int],
latencies: dict[str, float],
balance_penalty_ms: float,
) -> dict[str, Any] | None:
ranked = rank_proxies(proxies, account_counts, latencies, balance_penalty_ms)
if not ranked:
return None
selected = ranked[0]
account_counts[str(selected["id"])] = account_counts.get(str(selected["id"]), 0) + 1
return selected
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The function choose_proxy is defined but never called anywhere in the script or tests. If it is dead code, it should be removed to keep the codebase clean and maintainable.

Comment on lines +93 to +95
conn = sqlite3.connect(str(path))
try:
profile_arn = load_profile_arn(conn)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the SQLite file is corrupted, locked, or not a valid database, sqlite3.connect or subsequent queries can raise sqlite3.Error (such as sqlite3.DatabaseError or sqlite3.OperationalError). Since these are not caught, they will crash the entire import script. Wrapping these database operations in a try...except sqlite3.Error block and raising a descriptive ValueError will make the script much more robust.

    try:
        conn = sqlite3.connect(str(path))
    except sqlite3.Error as exc:
        raise ValueError(f"Failed to open SQLite database {path}: {exc}") from exc
    try:
        profile_arn = load_profile_arn(conn)

Comment on lines +64 to +69
def query_auth_kv(conn: sqlite3.Connection, keys: tuple[str, ...]) -> str | None:
for key in keys:
row = conn.execute("SELECT value FROM auth_kv WHERE key = ? LIMIT 1", (key,)).fetchone()
if row:
return str(row[0])
return None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If row[0] is None (which represents a NULL value in the database), str(row[0]) will return the string "None". Then, load_json("None", ...) will be called, which raises a ValueError because "None" is not a valid JSON object. To prevent this, we should check if row[0] is not None before converting it to a string.

Suggested change
def query_auth_kv(conn: sqlite3.Connection, keys: tuple[str, ...]) -> str | None:
for key in keys:
row = conn.execute("SELECT value FROM auth_kv WHERE key = ? LIMIT 1", (key,)).fetchone()
if row:
return str(row[0])
return None
def query_auth_kv(conn: sqlite3.Connection, keys: tuple[str, ...]) -> str | None:
for key in keys:
row = conn.execute("SELECT value FROM auth_kv WHERE key = ? LIMIT 1", (key,)).fetchone()
if row and row[0] is not None:
return str(row[0])
return None

Comment on lines +169 to +173
for item in dirs:
root = Path(item).expanduser()
for path in root.rglob("*"):
if path.is_file() and path.suffix.lower() in {".sqlite", ".sqlite3", ".db"}:
discovered.append(path)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Iterating over root.rglob("*") can raise OSError (such as PermissionError) if the script encounters directories with restricted permissions. This will crash the entire discovery process. Wrapping the iteration in a try...except OSError block ensures that permission or other filesystem errors in one directory do not crash the script.

Suggested change
for item in dirs:
root = Path(item).expanduser()
for path in root.rglob("*"):
if path.is_file() and path.suffix.lower() in {".sqlite", ".sqlite3", ".db"}:
discovered.append(path)
for item in dirs:
root = Path(item).expanduser()
try:
for path in root.rglob("*"):
try:
if path.is_file() and path.suffix.lower() in {".sqlite", ".sqlite3", ".db"}:
discovered.append(path)
except OSError:
continue
except OSError:
pass

Comment on lines +210 to +212
except urllib.error.HTTPError as exc:
text = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"{method} {url} failed: HTTP {exc.code}: {text[:500]}") from exc
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current exception handling only catches urllib.error.HTTPError. However, general network failures (like connection refused, DNS resolution failure, or timeouts) raise urllib.error.URLError, which is not caught here and will result in a raw traceback. Catching urllib.error.URLError as well and raising a clean RuntimeError will improve the CLI user experience.

Suggested change
except urllib.error.HTTPError as exc:
text = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"{method} {url} failed: HTTP {exc.code}: {text[:500]}") from exc
except urllib.error.HTTPError as exc:
text = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"{method} {url} failed: HTTP {exc.code}: {text[:500]}") from exc
except urllib.error.URLError as exc:
raise RuntimeError(f"{method} {url} failed: {exc.reason}") from exc

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 77e76f172d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

except Exception as exc:
attempt["error"] = str(exc)

result["deleted"] = delete_account(args, auth.name)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid deleting a pre-existing account on failed validation

When the generated/imported name already exists (for example reusing --account-name, --name-prefix, or the default default name), the earlier POST /admin/kiro-gateway/accounts replaces the existing row via create_or_replace_kiro_account/upsert_kiro_account; if the newly imported credentials then fail balance validation or duplicate-user checks, this unconditional delete removes the account that existed before the import. Please preflight for name collisions or create under a temporary unique name before deleting failures.

Useful? React with 👍 / 👎.

Comment on lines +422 to +424
created = request_json(
"POST", args.admin_base_url, "/admin/kiro-gateway/accounts", args.admin_token, body
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Create disabled until the fixed proxy is assigned

In environments where the Kiro provider binding is unset or not the chosen US proxy, creating the account as active here lets create_or_replace_kiro_account immediately run sync_kiro_status_after_account_update before the later PATCH assigns the fixed proxy. That first status refresh can go through inherited/direct routing, which violates this importer’s no-direct/no-proxy policy and can cache a failure before validation. Create the account disabled (then patch proxy and enable it) or otherwise avoid the pre-patch refresh.

Useful? React with 👍 / 👎.


def fetch_kiro_accounts(base_url: str, token: str | None) -> list[dict[str, Any]]:
payload = request_json(
"GET", base_url, "/admin/kiro-gateway/accounts?limit=10000&offset=0", token
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Page through all Kiro accounts before importing

When there are more than 200 Kiro accounts, this request still only sees the first page because the admin handler clamps limit to MAX_ADMIN_LIST_LIMIT (200) and returns has_more. That means proxy counts and existing_user_ids ignore later accounts, so the importer can place new accounts on overloaded proxies and miss duplicate upstream user_ids already present beyond the first page. Please follow pagination until has_more is false.

Useful? React with 👍 / 👎.

Comment on lines +502 to +503
if args.min_interval_min_ms < 0 or args.min_interval_max_ms < args.min_interval_min_ms:
raise SystemExit("invalid min interval range")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the 200–1000ms interval policy

If an operator passes --min-interval-min-ms 0 --min-interval-max-ms 0 or a value above 1000, this validation accepts it and the real import writes an interval outside the stated standard policy, even though the script hard-enforces the other policy knobs such as concurrency and minimum credits. Please reject interval bounds outside 200..1000 so applied accounts cannot accidentally route with no pacing or excessive pacing.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant