Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion docs/system-specs/modules/learn-cron-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ Bounds (`validation.py`, single source of truth) still gate the tool's arguments
**Agents**: GET `/api/agents` (KiroCrew agent roster ordered **most-used-first** — reorders config agents + discovered project agents by `ConversationLog.agent_usage()` (turn count, then recency), falling back gracefully to config-insertion order on any failure so the dropdown never breaks or drops agents), GET `/api/agents/installed` (list all kiro-cli agents from `~/.kiro/agents/`, with `package` field extracted from filename), GET/DELETE `/api/agents/detail/{name}` (full agent config JSON; DELETE removes the config file, protected for kirocrew/kirocrew-lite)
**Capability Integration** (edition-supplied operations-based `CapabilityManager` seam — the edition owns its CLI grammar, output parsing, and error translation; on a vanilla OSS install `CapabilityManager.available()` is `False` so every endpoint returns HTTP 503 `"capability manager not available"`): GET `/api/capability/mcp` (list installed MCP servers), POST `/api/capability/mcp/install` (install an MCP server, pushes `refresh("agents")`), POST `/api/capability/mcp/uninstall` (pushes `refresh("agents")`), GET `/api/capability/mcp/registry` (browse available MCP servers — the manager returns already-parsed entries, which the core passes through as `{"servers": [...]}`), GET `/api/capability/skills` (list installed skill packages), POST `/api/capability/skills/install` (install by `package` only — **no `version_set`**; the manager owns version/source resolution; regenerates agent config, pushes `refresh("agents")`; the manager returns human-friendly errors), POST `/api/capability/skills/uninstall` (pushes `refresh("agents")`), GET `/api/capability/agents` (list installed agent packages), POST `/api/capability/agents/install` + POST `/api/capability/agents/uninstall` (by `package` only, same no-`version_set` rule as skills; both rebuild the agent config off the event loop, clear the `list_agents()` cache — it keys on a `(count, newest-mtime-ns)` signature a same-tick mutation would not bump — and push `refresh("agents")`), GET `/api/capability/plugins` (installed client-plugin packages **plus** `out_of_sync`, the drift set of packages installed as agents but missing their plugin counterpart; both reads are `asyncio.gather`ed so the endpoint stays inside ONE `CAPABILITY_READ_TIMEOUT` — it is polled), POST `/api/capability/plugins/sync` (reconcile that drift). Package names are allowlisted (`_is_valid_capability_package`: length cap, `..` rejection, charset) before crossing into the edition manager, manager messages are `_redact_external`-scrubbed + length-bounded on BOTH success and failure paths, and each mutation emits an explicit SEL line naming the package (the audit middleware logs only the request path). The routes were renamed from the former `/api/aim/*` to neutral `/api/capability/*` vocab so no Amazon-internal name fossilizes in the fork's public API.
**Removed cross-provider bridge** (fully **deleted**): the kiro→removed-provider mirror (`/api/cc/mirror/preview` + `/api/cc/mirror/run`, `mirror_kiro_to_cc`, the `ProviderPanel.tsx` migrate-from-kiro UI, the `kirocrew mirror kiro-to-cc` CLI) was removed when KiroCrew collapsed to a single KiroACP / `kiro-cli` backend. The later cross-provider AIM plugin bridge (`GET /api/cc/aim/missing`, `POST /api/cc/aim/sync`, plus `install_cc_plugin`, `installed_kiro_packages_missing_from_cc`, and the `_VALID_PACKAGE_RE`/`~/.aim/cc-plugins` machinery) was **also removed** with the pure-AIM code — there are no `/api/cc/aim/*` routes. Provider-specific MCP config is now handled uniformly through the `extra_mcp_scopes()` seam (see `platform-context.md`): the core manages the Kiro global only, and a companion re-adds its provider scope for both apply/uninstall AND discovery. On `/api/mcp/apply` the omitted-field default differs by scope family — the core `kiroGlobal` key is `omit → delete` (the bundled SPA always sends it) while every seam `f"{id}Global"` key is `omit → preserve` (defaults to current on-disk presence), so an OSS apply that omits a companion scope never deletes that provider's server; see the `extra_mcp_scopes()` contract note in `platform-context.md`.
**Sessions**: GET `/api/sessions` (paginated list), GET `/api/sessions/{key}` (detail), DELETE `/api/sessions/{key}` (permanent delete), GET `/api/sessions/context` (context usage), GET `/api/sessions/usage` (kiro credit usage, cached 10 min; the background refresh tries the real CodeWhisperer RTS `GetUsageLimits` API first — the true used/limit/overage — and falls back to scraping `kiro-cli chat --no-interactive --agent kirocrew-lite /usage` stdout when the API path is unavailable), POST `/api/sessions/summarize` (one-line LLM summaries for a list of session keys — bounded to 8, generated on an ephemeral background session with the cheap Haiku model, best-effort; backs the `list_sessions` MCP tool's opt-in `summarize=true`), GET `/api/sessions/memory` (per-session and per-task memory footprint; returns `{sessions, tasks, totals, history, unattributed}` — each session row carries: `key`, `title`, `slot_key`, `untitled`, `agent`, `channel` (the grouping dimension, resolved by `telemetry_channel_of(key)` from `kiro_crew.messaging.link` — bounded cardinality, same taxonomy as telemetry metrics), `pid`, `owns_runtime`, `rss_mb`, `procs`, `mcp`, `cpu_cores`, `prompts`, `uptime_s`, `credits` (cumulative kiro credits consumed over the spend window — `SPEND_WINDOW_DAYS` in `dashboard/handlers/usage.py`, which the Telemetry spend tab's `cost_breakdown` shares so the two surfaces cannot report different totals for one session; `null` when no measured turn exists in the window, which is semantically distinct from zero; the join is by session key, and because a slot bound to a channel or cron conversation runs its turns under `linked_session_key` while its usage rows are still filed under the dashboard `slot.key`, the lookup falls back to `DashboardState.spend_slot_by_session()` — without that reverse index those sessions report `null` despite having spent), `turns` (count of completed turns in the same window — `null` same semantics as `credits`); `totals` carries `rss_mb`, `host_mb`, `host_pct`, `runtimes`, `rss_is_upper_bound`; `history` is a ring of `{t, mb}` snapshots; `tasks` is the subagent task list from `SubagentManager.task_memory_rows()`; `unattributed` is `{procs, rss_mb, oldest_uptime_s}` summarizing agent runtimes alive on the machine that this gateway does not own — `null` (not a zero record) on platforms where processes cannot be enumerated, so the UI can distinguish "none found" from "cannot look")
**Sessions**: GET `/api/sessions` (paginated list), GET `/api/sessions/{key}` (detail), DELETE `/api/sessions/{key}` (permanent delete), GET `/api/sessions/context` (context usage), GET `/api/sessions/usage` (kiro credit usage, cached 10 min; the background refresh tries the real CodeWhisperer RTS `GetUsageLimits` API first — the true used/limit/overage — reading bearer tokens from `kiro-cli`'s own SQLite auth store under keys `kirocli:odic:token` (OIDC/Builder ID), `codewhisperer:odic:token` (legacy), `kirocli:social:token` (GitHub/Google social login), and `kirocli:external-idp:token` (Identity Center/org SSO), then falls back to scraping `kiro-cli chat --no-interactive --agent kirocrew-lite /usage` stdout when the API path is unavailable and `dashboard.usage_text_scrape_enabled` is true), POST `/api/sessions/summarize` (one-line LLM summaries for a list of session keys — bounded to 8, generated on an ephemeral background session with the cheap Haiku model, best-effort; backs the `list_sessions` MCP tool's opt-in `summarize=true`), GET `/api/sessions/memory` (per-session and per-task memory footprint; returns `{sessions, tasks, totals, history, unattributed}` — each session row carries: `key`, `title`, `slot_key`, `untitled`, `agent`, `channel` (the grouping dimension, resolved by `telemetry_channel_of(key)` from `kiro_crew.messaging.link` — bounded cardinality, same taxonomy as telemetry metrics), `pid`, `owns_runtime`, `rss_mb`, `procs`, `mcp`, `cpu_cores`, `prompts`, `uptime_s`, `credits` (cumulative kiro credits consumed over the spend window — `SPEND_WINDOW_DAYS` in `dashboard/handlers/usage.py`, which the Telemetry spend tab's `cost_breakdown` shares so the two surfaces cannot report different totals for one session; `null` when no measured turn exists in the window, which is semantically distinct from zero; the join is by session key, and because a slot bound to a channel or cron conversation runs its turns under `linked_session_key` while its usage rows are still filed under the dashboard `slot.key`, the lookup falls back to `DashboardState.spend_slot_by_session()` — without that reverse index those sessions report `null` despite having spent), `turns` (count of completed turns in the same window — `null` same semantics as `credits`); `totals` carries `rss_mb`, `host_mb`, `host_pct`, `runtimes`, `rss_is_upper_bound`; `history` is a ring of `{t, mb}` snapshots; `tasks` is the subagent task list from `SubagentManager.task_memory_rows()`; `unattributed` is `{procs, rss_mb, oldest_uptime_s}` summarizing agent runtimes alive on the machine that this gateway does not own — `null` (not a zero record) on platforms where processes cannot be enumerated, so the UI can distinguish "none found" from "cannot look")
**Logs**: GET `/api/logs` (SSE), GET/POST `/api/logs/level` (runtime log level control)
**Task Runner**: GET `/api/taskrunner` (status with runs[], includes `agent`), POST `/api/taskrunner` (start, optional `agent` field), POST `/api/taskrunner/cancel` (per-task or all), DELETE `/api/taskrunner/{task_id}` (remove finished run), POST `/api/taskrunner/refine` (dynamic multi-turn with tool access), GET `/api/taskrunner/refine` (status with `waiting` field), POST `/api/taskrunner/refine/cancel`, POST `/api/taskrunner/refine/answer` (answer clarifying question)
**Approvals**: GET `/api/approvals` (pending list), POST `/api/approvals/{id}/approve`, POST `/api/approvals/{id}/reject`
Expand Down
2 changes: 1 addition & 1 deletion src/kiro_crew/dashboard/handlers/kiro_usage_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@
Path.home() / ".local" / "share" / "amazon-q" / "data.sqlite3",
Path.home() / "Library" / "Application Support" / "amazon-q" / "data.sqlite3",
)
_SQLITE_TOKEN_KEYS = ("kirocli:odic:token", "codewhisperer:odic:token")
_SQLITE_TOKEN_KEYS = ("kirocli:odic:token", "codewhisperer:odic:token", "kirocli:social:token", "kirocli:external-idp:token")

# SEL audit label for the SQLite live-token read. Not an allowlist entry (that
# gate is for sensitive-path reads via safe_read_file_internal); this is only
Expand Down
42 changes: 42 additions & 0 deletions test/test_kiro_usage_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,48 @@ def test_sqlite_opened_but_no_token_still_audits(self, tmp_path):
assert api._token_from_sqlite(db, datetime.now(timezone.utc)) is None
audit.assert_any_call(api._SQLITE_AUDIT_READ_ID, "no_token")

def test_social_login_token_key_is_recognized(self, tmp_path):
# GitHub social login stores its bearer token under a different key
# (kirocli:social:token) than the OIDC flow (kirocli:odic:token).
# The API module must read it from kiro-cli's own store.
db = tmp_path / "data.sqlite3"
con = sqlite3.connect(str(db))
con.execute("CREATE TABLE auth_kv (key TEXT PRIMARY KEY, value TEXT)")
future = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()
con.execute(
"INSERT INTO auth_kv VALUES (?, ?)",
("kirocli:social:token",
json.dumps({"access_token": "social-tok", "expires_at": future})),
)
con.commit()
con.close()
with patch("kiro_crew.hooks.safe_read_file_internal", return_value=None), \
patch("kiro_crew.hooks.emit_internal_read_audit", return_value=True), \
patch.object(api, "_CLI_SQLITE_DBS", (db,)), \
patch.object(api, "_OTHER_SQLITE_DBS", ()):
assert api._load_bearer_token() == "social-tok"

def test_external_idp_token_key_is_recognized(self, tmp_path):
# Identity Center (org/enterprise SSO) stores its bearer token under
# kirocli:external-idp:token. On Linux the JSON SSO cache is not
# refreshed, so this SQLite key is the only live source.
db = tmp_path / "data.sqlite3"
con = sqlite3.connect(str(db))
con.execute("CREATE TABLE auth_kv (key TEXT PRIMARY KEY, value TEXT)")
future = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()
con.execute(
"INSERT INTO auth_kv VALUES (?, ?)",
("kirocli:external-idp:token",
json.dumps({"access_token": "idp-tok", "expires_at": future})),
)
con.commit()
con.close()
with patch("kiro_crew.hooks.safe_read_file_internal", return_value=None), \
patch("kiro_crew.hooks.emit_internal_read_audit", return_value=True), \
patch.object(api, "_CLI_SQLITE_DBS", (db,)), \
patch.object(api, "_OTHER_SQLITE_DBS", ()):
assert api._load_bearer_token() == "idp-tok"


class TestPostSecurityControls:
def test_post_sets_headers_method_and_url(self):
Expand Down
Loading