feat(dev-portal): kbagent Developer Portal support with no-bypass write safety - #354
Conversation
padak
left a comment
There was a problem hiding this comment.
Review of #354 — feat(dev-portal): kbagent Developer Portal support with no-bypass write safety
Generated by
kbagent-pr-reviewersubagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed viamake check, not duplicated here.
Summary
This PR adds a kbagent dev-portal command group wrapping the Keboola Developer Portal API (apps-api.keboola.com). The implementation is well-structured: the 3-layer architecture is respected (new DeveloperPortalClient extends BaseHttpClient; service layer is pure; commands are thin), the OPERATION_REGISTRY has all 15 real entries, the TTY-only write-safety design is sound, the require_random_code_confirmation() refactor is clean, and most silent-drift surfaces (AGENT_CONTEXT, CLAUDE.md, SKILL.md, commands-reference.md, gotchas.md) are correctly updated. However, three BLOCKING gaps remain: keboola-expert.md was not updated at all (the highest-risk silent-drift surface per CONTRIBUTING.md), the server/routers/dev_portal.py endpoint is absent without a documented skip justification, and the hints/definitions/dev_portal.py entry is absent without a documented exception. The verdict is REQUEST CHANGES.
Verdict
- Verdict: REQUEST CHANGES
- Blocking findings: 3
- Non-blocking findings: 3
- Nits: 2
Blocking findings
[B-1] plugins/kbagent/agents/keboola-expert.md — NOT updated (highest silent-drift risk)
keboola-expert.md does not appear in the PR's changed-files list and has zero mentions of dev-portal. Per CONTRIBUTING.md > "Plugin synchronization map", this file is the highest silent-drift risk in the repo and must be updated for any new write or destructive command. Two sub-gaps:
(a) §2 Tool Selection Matrix — none of the five write commands (dev-portal create, patch, upload-icon, publish, deprecate) have a | ... | First choice | Fallback | NEVER | row. Without a matrix entry, the keboola-expert subagent will fall back to MCP tool call or refuse portal write tasks instead of using the correct CLI path with --dry-run discipline.
(b) §1 Rule 6 VERSION GATE — the gate list ends with semantic-layer search-context|get-context ... = 0.47.0+ but never mentions dev-portal commands needing 0.48.0+. Any session running on a pre-0.48.0 install will attempt kbagent dev-portal ..., receive "command not found", and generate an unhelpful error traceback rather than the clean refusal the VERSION GATE is designed to produce.
Fix: add dev-portal {list,get,create,patch,upload-icon,publish,deprecate} and identity lifecycle need 0.48.0+ to the Rule 6 list, and add at minimum one Tool Selection Matrix row for dev-portal patch and one for dev-portal publish (the two highest-risk write paths), following the existing row format (| User intent | First choice | Fallback | NEVER |).
[B-2] src/keboola_agent_cli/server/routers/ — no dev_portal.py router, no documented skip
CONTRIBUTING.md > "HTTP API endpoint" checklist item states: "every command in a group has a matching endpoint in that group's router... Skip allowed only for genuinely terminal-only commands (interactive prompts, Rich-rendered output that has no useful JSON shape, doctor/init/update-style infrastructure). Document any skip in the PR description." The PR description does not mention this skip at all.
The dev-portal write commands (create, patch, publish, deprecate, upload-icon) use require_random_code_confirmation() which immediately exits 6 on a non-TTY — making them technically non-callable via REST. That design choice IS a valid justification for skipping the router. But the justification must appear in the PR description so reviewers do not re-file it, and so the decision is documented before it goes to main. The kbagent serve consumer contract (CONTRIBUTING.md Plugin sync map: "callers get HTTP 404 instead of a visible skip") cannot be met without at least a stub router that returns 501 with a human-readable message for the write commands, or an explicit skip documented in the description.
Fix (two options): (a) add a server/routers/dev_portal.py stub that exposes the read commands (list, get) as REST endpoints, and returns HTTP 405 Method Not Allowed for write commands with an explanation body; OR (b) add a one-sentence skip justification to the PR description ("No REST router: dev-portal write commands are hardcoded to require a live TTY; the REST surface intentionally omits them") so reviewers and future contributors understand the decision.
[B-3] src/keboola_agent_cli/hints/definitions/ — no dev_portal.py hint definition, no documented exception
CONTRIBUTING.md > "Code changes" checklist item: "every command must support --hint client and --hint service code generation". The exception list is explicit: doctor, context, init, serve, version, update, changelog, permissions, and the four http self-call verbs. dev-portal is NOT on that list — it wraps apps-api.keboola.com via DeveloperPortalClient, which IS a Keboola API client (same pattern as KeboolaClient, ManageClient, AiServiceClient).
The PR description does not mention --hint at all, and grep 'should_hint\|emit_hint' dev_portal.py returns empty.
Fix: add hints/definitions/dev_portal.py with CommandHint entries for at minimum dev-portal list, dev-portal get, and dev-portal patch (the three most likely to be used programmatically via the REST surface or --hint service code generation). The write-commands' --hint client recipes are still useful for scaffolding scripts that wrap the client directly. If the team decides dev-portal is deliberately --hint-free (e.g., because the portal API is credential-gated and hint generation is not useful without live creds), document that decision in the PR description with the same one-sentence justification format the checklist uses.
Non-blocking findings
[NB-1] tests/test_e2e.py:TestDevPortalE2E — only one unconditional E2E test; 13 of 15 commands have zero E2E coverage
CONTRIBUTING.md convention #16 (also CONTRIBUTING.md > "Tests" checklist): "Every new CLI command MUST have a corresponding E2E test in tests/test_e2e.py." The PR adds TestDevPortalE2E with two tests: test_identity_list_smoke (unconditional, confirms the command returns a list from an empty config — no network, no portal) and test_list_apps_against_real_portal (skip-guarded behind E2E_DP_USERNAME / E2E_DP_PASSWORD). The PR description marks the credential-gated test as "Optional".
The unconditional smoke test exercises dev-portal identity list only. The 12 other commands (dev-portal get, create, patch, upload-icon, publish, deprecate, identity add, identity remove, identity edit, identity use, identity current, identity verify) have no E2E test at all, even at the exit-code-only level. This is understandable for writes (TTY requirement makes them hard to E2E-test in CI), but reads (dev-portal list --vendor, dev-portal get --app) can be exercised against a public vendor endpoint without private credentials.
Deferring portal writes to a follow-up cycle is acceptable given the TTY constraint. However, dev-portal list and dev-portal get (which do NOT require write credentials) should have at least a skip-guarded test parallel to how other credential-gated E2E tests work in this repo.
[NB-2] src/keboola_agent_cli/commands/permissions.py:246,293 — permissions set/reset refactor drops structured error in JSON mode for the non-TTY path
The old _require_interactive_confirmation() returned False on non-TTY, and both permissions_set and permissions_reset called formatter.error(message="Confirmation failed. Permission policy not changed.", error_code=ErrorCode.PERMISSION_DENIED) before raising typer.Exit(code=EXIT_PERMISSION_DENIED).
The new require_random_code_confirmation() writes directly to sys.stderr and raises typer.Exit(code=EXIT_PERMISSION_DENIED) — bypassing formatter.error() entirely. In --json mode, callers that previously received a structured {"status": "error", "error": {"code": "PERMISSION_DENIED", "message": "Confirmation failed. ..."} envelope now receive only an empty body and exit code 6. This is a backward-compatibility regression for any script that was parsing the JSON error envelope on exit-code-6 from permissions set/reset.
The test_set_rejected_without_confirmation test only asserts result.exit_code == EXIT_PERMISSION_DENIED and does not catch this regression.
Fix: in the new require_random_code_confirmation(), write the error to sys.stderr for human-mode consistency, but also raise a KeboolaApiError(error_code=ErrorCode.PERMISSION_DENIED, message="...") that the calling command can catch and route through formatter.error() — or keep require_random_code_confirmation() raising typer.Exit and have the command wrap the call in a try/except to emit the structured error first.
[NB-3] src/keboola_agent_cli/changelog.py:0.48.0 — changelog lists dev-portal.identity.get which is not a registered operation or an implemented command
The 0.48.0 changelog entry reads: "15 dev-portal.* permission-registry operations (...dev-portal.identity.verify, dev-portal.identity.get) registered...". The command dev-portal identity get does not exist in dev_portal.py (no @identity_app.command("get") decorator), and "dev-portal.identity.get" is not in OPERATION_REGISTRY. The actual 15th entry is "dev-portal.identity" (parent callback placeholder, categorized as read).
This is a documentation-only inaccuracy — the OPERATION_REGISTRY itself is correct. But the changelog is a user-facing release document; shipping it with a non-existent operation listed as if it were real will confuse anyone who reads the release notes and tries to use kbagent dev-portal identity get.
Fix: replace dev-portal.identity.get in the changelog text with dev-portal.identity (the parent callback entry that actually exists), and drop the phantom command reference.
Nits
-
[NIT-1]src/keboola_agent_cli/commands/dev_portal.py:260andsrc/keboola_agent_cli/commands/_helpers.py:require_random_code_confirmation—_assert_tty()andrequire_random_code_confirmation()both checksys.stdin.isatty(). The write commands call_assert_tty()early (before API calls), then callrequire_random_code_confirmation()later (after the preview). This double-check is intentional for UX (fail fast before API calls), but both functions have identical TTY-check logic. Consider extracting_is_tty() -> boolinto_helpers.pyas the single source of truth for the TTY predicate, so the two guards cannot diverge. -
[NIT-2]src/keboola_agent_cli/permissions.py:153—"dev-portal.identity.remove": "write"but"data-app.secrets-remove": "destructive"(analogous credential-remove operation). Credential removal is arguablydestructiveby the CONTRIBUTING.md table ("Destroys data") since it permanently deletes the stored identity. This is a judgment call — the existing precedent in"project.remove"is"admin"— but aligning withdata-app.secrets-remove: destructivewould be more consistent.
Verification log
gh pr view 354 --repo keboola/cli --json title,body,files,state→ 33 files changed, +5800/-61, state OPEN, conventionalfeat(dev-portal):✓gh pr diff 354 --repo keboola/cli→ 6292-line diff saved to/tmp/kbagent-pr-354.diff✓Read CONTRIBUTING.md→ loaded Plugin synchronization map, Checklist for adding new CLI commands ✓Read /tmp/keboola-expert-pr.md(from PR branch viagh api) → 765 lines;grep 'dev.portal'→ 0 hits ✓ (confirms B-1)grep 'server/routers/dev_portal' /tmp/kbagent-pr-354.diff→ empty ✓ (confirms B-2);gh api repos/keboola/cli/contents/src/.../server/routers/dev_portal.py?ref=feat/dev-portal→ HTTP 404 ✓ls hints/definitions on PR branch via gh api→ nodev_portal.py✓ (confirms B-3);grep 'should_hint\|emit_hint' dev_portal.py→ empty ✓gh api keboola-expert.md §2 Tool Selection Matrix→ nodev-portalrow anywhere in 200+ rows ✓ (confirms B-1a)sed -n '65,130p' /tmp/keboola-expert-pr.md→ Rule 6 VERSION GATE list ends at0.47.0+; nodev-portal/0.48.0mention ✓ (confirms B-1b)awk permissions.py diff→ 15dev-portal.*OPERATION_REGISTRY entries confirmed; nodev-portal.identity.get✓grep 'dev-portal.identity.get' changelog diff→ present in changelog text, absent from OPERATION_REGISTRY ✓ (confirms NB-3)Read context.py diff→AGENT_CONTEXTextended with### Developer Portal (since v0.48.0)section ✓Read CLAUDE.md diff→## All CLI Commandsextended with all 15 new commands ✓Read gotchas.md diff→ new entry tagged(since v0.48.0)✓Read commands-reference.md diff→## Developer Portal (since v0.48.0)section added ✓Read SKILL.md diff on PR branch→ decision table rows + workflow table row[dev-portal-workflow]added ✓Read dev-portal-workflow.md→ 70 lines, covers identity model, safety contract, the prepare/apply loop, peer-research pattern, and boundaries ✓sed -n '334,530p' dev_portal.py→ all 5 write commands call_assert_tty()thenrequire_random_code_confirmation()with--dry-runearly-exit before confirmation ✓ (write-safety claim verified)grep 'require_random_code_confirmation\|_require_interactive' commands/permissions.py diff→ old_require_interactive_confirmation()removed,require_random_code_confirmation()imported from_helpers✓ (refactor verified)sed -n '104,122p' dev_portal.py→ identity list output omitspasswordfield ✓ (no credential leak in list output)make checkon main branch (not PR branch): 3622 passed, 7 skipped — note: this ismain; PR author reports 3667 passed / 8 skipped / 0 failures on the PR branch. Cannot reproduce onmain(PR files absent). The difference (+45 tests) is consistent with the 4 new test files in the diff ✓- Behavior reproduction (
kbagent dev-portal --help, dry-run exits 0, non-TTY exits 6): could not reproduce — local working tree is onclaude/mystifying-murdock-4c3da5, notfeat/dev-portal. Author's test-plan checklist confirms manual verification; accepted as author-attested. grep 'logger\.' dev_portal_client.py→ no bearer token in log output ✓
Open questions for the author
-
--hintexception decision: Isdev-portalintentionally excluded from--hintsupport? The exception list inCONTRIBUTING.mdis for "commands that manage kbagent itself, not Keboola". The Developer Portal is a separate Keboola service — the same rationale asAiServiceClient. If the call patterns are genuinely too variable to hint (portal payloads are arbitrary JSON, not a fixed schema), that is worth documenting explicitly as a first-class exception inCONTRIBUTING.mdfor future contributors. -
Server router skip scope: If the read commands (
dev-portal list,dev-portal get) do not require TTY, could they be exposed as REST endpoints inserver/routers/dev_portal.pyeven if write commands are skipped? The "peer-research" use case described in the workflow doc is plausible in a scheduled agent context (reading portal entries programmatically), which is exactly the use casekbagent serveis designed for.
padak
left a comment
There was a problem hiding this comment.
Review of #354 — feat(dev-portal): kbagent Developer Portal support with no-bypass write safety
Generated by
kbagent-pr-reviewersubagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed viamake check, not duplicated here.
Summary
This PR adds a kbagent dev-portal command group wrapping apps-api.keboola.com with
15 new OPERATION_REGISTRY entries, full 3-layer architecture compliance, and a
no-bypass TTY confirm for all write operations. The implementation is solid: make check
passes with 3667 tests, 3-layer boundaries are respected, all new ErrorCode enum entries
are used properly, and backward-compat on AppConfig is maintained via safe defaults.
gotchas.md correctly uses (since v0.48.0) in the heading.
Two silent-drift gaps prevent a clean APPROVE: (1) plugins/kbagent/agents/keboola-expert.md
was NOT updated despite the PR description claiming "all rule #17 surfaces updated" — the
§2 Tool Selection Matrix has no rows for any of the five write/destructive dev-portal
commands; (2) hints/definitions/dev_portal.py is absent with no documented skip reason,
which CONTRIBUTING.md requires for every command wrapping a non-infrastructure API client.
Additionally, dev-portal list and dev-portal get are pure reads with no TTY dependency
and have no server router endpoint, undocumented in the PR description.
Verdict
- Verdict: REQUEST CHANGES
- Blocking findings: 3
- Non-blocking findings: 0
- Nits: 1
Blocking findings
[B-1] plugins/kbagent/agents/keboola-expert.md — §2 Tool Selection Matrix missing all dev-portal rows
git diff origin/main...origin/feat/dev-portal -- plugins/kbagent/agents/keboola-expert.md
produces zero output: the file was not changed in this PR. The PR description claims
"All rule #17 silent-drift surfaces updated" but keboola-expert.md is not in the
changed-files list. CONTRIBUTING.md §"Plugin synchronization map" and §"Releasing a new
version" step 5 both require a | intent | First choice | Fallback | NEVER | row for
every new write/destructive command. Five commands qualify: dev-portal create (write),
dev-portal patch (write), dev-portal upload-icon (write), dev-portal publish (admin),
dev-portal deprecate (destructive). Without these rows, the keboola-expert subagent
will fall back to MCP tool call or refuse tasks involving Developer Portal updates, defeating
the purpose of the agent-safe --dry-run preview path.
Fix: add rows to the §2 matrix for each of the five write/destructive commands and for the
read commands (dev-portal list, dev-portal get) to enable peer-config research workflows.
For the write rows, the "First choice" should be kbagent dev-portal <cmd> --dry-run (show
preview to human, then human runs without --dry-run); "NEVER" should call out bypassing the
TTY confirm or passing --yes (which does not exist). Also update §1 Rule 6 VERSION GATE
examples if 0.48.0 adds a minimum-version constraint worth noting.
[B-2] src/keboola_agent_cli/hints/definitions/ — no dev_portal.py hint file, no documented skip
CONTRIBUTING.md "Checklist: Adding a New CLI Command" requires every command wrapping a
Keboola API client to have a CommandHint with ClientCall + ServiceCall in
hints/definitions/. The listed infrastructure exception (doctor, context, init, serve,
version, update, changelog, permissions, http) does NOT apply here: DeveloperPortalClient
inherits BaseHttpClient and is a standard HTTP client for an external Keboola service,
exactly the pattern hints are designed for. src/keboola_agent_cli/hints/definitions/__init__.py
has no dev_portal import. The PR description does not mention a skip reason.
Fix: add src/keboola_agent_cli/hints/definitions/dev_portal.py with CommandHint
registrations for the read commands (dev-portal.list, dev-portal.get) and the identity
CRUD commands. Write commands can include a hint with a note that actual execution requires
a real terminal. Add from . import dev_portal to hints/definitions/__init__.py. Then add
if should_hint(ctx): emit_hint(...) short-circuits in each command function.
[B-3] src/keboola_agent_cli/server/routers/ — no router for dev-portal read commands; skip undocumented
CONTRIBUTING.md requires a 1:1 server router for every new command group, with a skip
allowed "only for genuinely terminal-only commands." The write commands (create, patch,
upload-icon, publish, deprecate) are legitimately terminal-only because they call
require_random_code_confirmation() unconditionally (defensible skip). But dev-portal list
and dev-portal get are pure reads: they return structured JSON, have no TTY dependency,
and are directly useful for programmatic consumers such as Web UI panels, scheduled agents,
and CI pipelines that need to inspect portal state. The PR description contains zero
mention of the router skip, leaving reviewers unable to assess whether it was intentional.
Fix: add src/keboola_agent_cli/server/routers/dev_portal.py with at minimum
GET /dev-portal/list and GET /dev-portal/get endpoints, mirroring the CLI flag shapes.
For the five write commands, document the skip in the PR description: "dev-portal write
commands are intentionally absent from the REST router — they require a human at a TTY and
cannot be safely invoked programmatically."
Non-blocking findings
(none)
Nits
[NIT-1]PR description — the footer "🤖 Generated with Claude Code" violates CONTRIBUTING.md "No AI attribution footers in PR descriptions". Does not affect merge.
Verification log
gh pr view 354 --repo keboola/cli --json title,body,files,state→ 33 files changed, +5800/-61,feat(dev-portal):conventional prefix ✓, state OPEN ✓git fetch origin feat/dev-portal && git checkout review-354 origin/feat/dev-portal→ clean worktree checkout ✓uv sync --extra server && make check→ ruff clean ✓, SKILL.md up-to-date ✓, version sync ✓, changelog entries ✓, error-code enum ✓, 3667 passed, 8 skipped, 0 failures ✓- Layer violation checks: typer/click absent from
services/dev_portal_service.py✓; httpx absent fromcommands/dev_portal.py✓;DeveloperPortalClientinheritsBaseHttpClient, httpx only in client layer ✓ grep -E "error_code\s*=\s*\"" diff→ empty ✓ (all five new error codes useErrorCode.DP_*enum members)- Bare
except:scan → empty ✓;print()in src/ → empty ✓; magic-number scan → empty ✓ git diff origin/main...origin/feat/dev-portal -- plugins/kbagent/agents/keboola-expert.md | wc -l→ 0 ✗ BLOCKING (file unchanged)git ls-tree origin/feat/dev-portal src/keboola_agent_cli/hints/definitions/→ nodev_portal.py✗ BLOCKING;__init__.pyhas nodev_portalimport ✗git ls-tree origin/feat/dev-portal src/keboola_agent_cli/server/routers/→ nodev_portal.py✗ BLOCKING (reads not terminal-only)OPERATION_REGISTRYinpermissions.py→ 15 newdev-portal.*entries:deprecate=destructive,publish=admin, reads=read, writes=write✓context.pyAGENT_CONTEXT → 14 dev-portal references ✓;CLAUDE.md ## All CLI Commands→ full dev-portal block present ✓commands-reference.md→## Developer Portal (since v0.48.0)section with all 14 commands ✓gotchas.md→## Developer Portal: writes require a human, no exceptions (since v0.48.0)— version tag correctly in heading ✓dev-portal-workflow.md→ new file with full TTY safety contract, peer-research examples, boundaries ✓SKILL.md→ dev-portal rows in decision table + workflow link at bottom ✓AppConfig.dev_portal_identities→default_factory=dict✓;default_dev_portal_identity→default=""✓ (existing configs safe)require_random_code_confirmation()extracted to_helpers.py✓; TTY check fires before payload I/O in write commands ✓test_patch_non_tty_exits_6✓;test_patch_dry_run_no_confirm✓;TestRequireRandomCodeConfirmationcovers non-TTY/correct/wrong/EOF ✓TestDevPortalE2E.test_identity_list_smokeunconditional;test_list_apps_against_real_portalgated onE2E_DP_USERNAME/E2E_DP_PASSWORD✓- Manual behavior reproduction: cannot verify
--dry-runexit 0 or non-TTY exit 6 against real portal (no DP credentials available); PR test plan marks these as unchecked manual steps ✓ (acknowledged) - Password stored in
config.jsonat 0600 — same protection as KB tokens;identity listomits password field from JSON output ✓; bearer never written to disk per docstring ✓
Extra finding (human review): double login / double MFA on
|
…r-group (#355) * docs(contributing): deprecate --hint requirement, make tool-matrix per-group Two policy fixes surfaced by the dev-portal review (PR #354), where two of the repo's own rules collided with each other: - --hint code generation is already deprecated in favour of the `kbagent serve` REST API (CLAUDE.md, gotchas.md), but the per-command checklist and the kbagent-pr-reviewer prompt still demanded a hints/definitions entry per command and flagged its absence BLOCKING. Drop the requirement; existing hint definitions stay for back-compat but are no longer extended, and reviewers must not flag a missing one. - keboola-expert.md §2 Tool Selection Matrix is a static subagent system prompt loaded eagerly into every run, with a hard 60 KB budget. The rule demanding one matrix row PER COMMAND fought that budget directly. Make it one row per command GROUP; exhaustive per-command detail lives in AGENT_CONTEXT (loaded dynamically). A missing matrix row is no longer BLOCKING. Trim stale content rather than raising the cap. * docs(contributing): clarify matrix is author-expected but NON-BLOCKING in review
|
Hi @matyas-jirat-keboola — Petr asked me to push the review follow-ups straight onto this branch so it's unblocked. Three commits ( 1. 2. 3. The matrix-row and |
padak
left a comment
There was a problem hiding this comment.
Review of #354 — feat(dev-portal): kbagent Developer Portal support with no-bypass write safety
Generated by
kbagent-pr-reviewersubagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed viamake check, not duplicated here.
Summary
This PR adds a kbagent dev-portal command group wrapping apps-api.keboola.com. The architecture is sound: identity CRUD in config.json, prepare/apply split with a random-code TTY gate on writes, bearer cached in-process only. The earlier-flagged --hint and tool-matrix issues were resolved at the policy level (PR #355). Two new commits since the last review introduced the bearer-cache lifecycle (bac250c) and the serve read router (bfcd7f5). The bearer-cache fix is correct for the CLI (single-invocation service), but its interaction with the long-lived ServiceRegistry in kbagent serve introduces a stale-bearer lockout bug and an auth-bypass gap in --ui mode. Both are BLOCKING. Verdict: REQUEST CHANGES.
Verdict
- Verdict: REQUEST CHANGES
- Blocking findings: 2
- Non-blocking findings: 2
- Nits: 2
Blocking findings
[B-1] src/keboola_agent_cli/server/app.py:721-748 — /dev-portal missing from api_prefixes in _is_ui_public; auth bypass in --ui mode
_is_ui_public returns True for any GET path not in api_prefixes. The tuple at lines 721-748 lists every other router prefix (/projects, /configs, /data-apps, ...) but omits /dev-portal. In kbagent serve --ui mode, BearerAuthMiddleware.dispatch calls is_ui_public(method, path) at auth.py:72; a GET /dev-portal/apps?vendor=keboola request does not match any prefix, so is_ui_public returns True and the request is forwarded to the router without bearer validation. The dev-portal router executes normally, returning real portal app data to an unauthenticated caller. Note: a browser user who loaded GET / already has the session cookie and is unaffected; this gap hits script/curl callers in --ui mode.
Fix: add "/dev-portal" to the api_prefixes tuple alongside the other router prefixes.
# app.py line ~748 — add this line:
"/dev-portal",[B-2] src/keboola_agent_cli/services/dev_portal_service.py:160-164 — stale bearer never cleared on error in _authed_client; permanent 401 lockout in kbagent serve
_bearers[alias] = new_bearer is inside the with client: block after the yield. When an exception propagates out of yield (e.g., portal returns 401 on an expired bearer), Python's generator protocol causes the with to exit via GeneratorExit/exception path, skipping new_bearer = client.bearer and the _bearers update entirely. The stale bearer remains in self._bearers[alias]. In the CLI this is harmless (the service is rebuilt per invocation). In kbagent serve, ServiceRegistry is a singleton (app.state.registry installed once in create_app), so dev_portal._bearers persists across all HTTP requests. Once a portal bearer expires (Developer Portal tokens have a finite TTL), every subsequent GET /dev-portal/* call seeds the stale bearer, receives a new 401, and the service never re-authenticates — permanent lockout until server restart.
Fix: clear _bearers[alias] on INVALID_TOKEN / DP_LOGIN_FAILED errors, or use a try/except around the yield to remove the entry on error:
@contextmanager
def _authed_client(self, alias: str) -> Iterator[DeveloperPortalClient]:
ident = self._resolve_identity(alias)
client = self._client_factory(ident)
cached = self._bearers.get(alias)
if cached is not None:
client.seed_bearer(cached)
try:
with client:
yield client
new_bearer = client.bearer
if new_bearer is not None:
self._bearers[alias] = new_bearer
except KeboolaApiError as exc:
if exc.error_code in (ErrorCode.INVALID_TOKEN, ErrorCode.DP_LOGIN_FAILED):
self._bearers.pop(alias, None)
raiseA regression test should verify that after a simulated 401 from the portal, a second call re-authenticates (login count increases to 2 instead of staying at 1).
Non-blocking findings
[NB-1] plugins/kbagent/agents/keboola-expert.md:65-114 — dev-portal group not listed in §1 Rule 6 VERSION GATE
The dev-portal command group requires 0.48.0+, but §1 Rule 6 does not mention it. The tool-selection matrix row (§2, line 166) does contain (0.48.0+) inline, so a careful agent will encounter it during task planning — but the VERSION GATE is the canonical "check first" mechanism. An agent on 0.47.x might proceed to dev-portal list and get a confusing "No such command" error instead of a clean refusal.
Fix: add \dev-portal` command group needs 0.48.0+to the VERSION GATE examples list alongside the other version entries, e.g. after thesemantic-layer` block.
[NB-2] src/keboola_agent_cli/services/dev_portal_service.py:198-219 and src/keboola_agent_cli/commands/dev_portal.py:383-412 — patch --dry-run performs a live portal GET (undocumented portal connectivity requirement)
prepare_patch calls _authed_client → logs in → calls client.get_app() to fetch current state for diff computation. This is correct behaviour (the dry-run diff would be empty without it), but --dry-run is documented as the "agent-safe preview" path. An AI agent running kbagent dev-portal patch --dry-run against an identity with a personal account (MFA required) will trigger the MFA /dev/tty prompt, which has no TTY in a non-interactive context and will raise DP_MFA_REQUIRED. The test at tests/test_dev_portal_cli.py:140 mocks prepare_patch entirely, hiding this dependency.
Fix: document the connectivity requirement in --dry-run's help text and in dev-portal-workflow.md, or add a service-account-only note. Optionally, add a CLI test that exercises the DP_MFA_REQUIRED error path for patch --dry-run with a personal account on a non-TTY (using _tty_prompt monkeypatch returning None).
Nits
[NIT-1]src/keboola_agent_cli/commands/dev_portal.py:287—_render_pendingtype annotation is# type: ignore[type-arg]; the function signature could usePendingWritefrom the service module instead of the untypedAnyworkaround now that allPending*types are exported.[NIT-2]src/keboola_agent_cli/commands/dev_portal.py:324—_pending_as_jsonalso carries# type: ignore[type-arg]; same fix as NIT-1 applies.
Verification log
gh pr view 354 --json title,body,files,additions,deletions,state→ state=OPEN, 39 files, +6075/-68,feat(dev-portal):prefix ✓git rev-parse --abbrev-ref HEAD→feat/dev-portal✓ (correct branch)make check→ 3673 passed, 8 skipped, 107 deselected, 13 warnings, exit 0 ✓- Layer check (
grep -E '^\+.*(typer|click|formatter\.|console\.print)' diff | grep services/) → empty ✓ - Layer check (
grep -E '^\+(from httpx|import httpx)' diff | grep commands/) → empty ✓ grep -E '^\+.*error_code\s*=\s*"[A-Z_]+"' diff→ empty (all error codes useErrorCodeenum) ✓grep -E '^\+\s*except\s*:' diff→ empty ✓grep -n "dev-portal" src/keboola_agent_cli/permissions.py→ 15 entries present, categories match PR description ✓grep -n "dev-portal" plugins/kbagent/skills/kbagent/references/gotchas.md→ entry at line 2150 with(since v0.48.0)tag ✓grep -n "dev-portal" plugins/kbagent/skills/kbagent/references/commands-reference.md→ entries present, signatures match diff ✓grep -n "dev-portal" src/keboola_agent_cli/commands/context.py→ section present at line 1057 ✓grep -n "dev-portal" CLAUDE.md→ command signatures present ✓- B-1 confirmed:
/dev-portalabsent fromapi_prefixestuple atapp.py:721-748; every other router prefix is listed;auth.py:72confirms the bypass path - B-2 confirmed:
_authed_clientcontextmanager atdev_portal_service.py:145-164;new_bearer = client.beareris insidewith client:afteryield; exception path skips the cache-update;ServiceRegistryis a singleton in serve (app.py:520-525); no test exercises the 401-retry path on a long-lived service instance - Behavior reproduction: could not exercise against a real Developer Portal (no
E2E_DP_USERNAME/E2E_DP_PASSWORD). CLI unit tests pass; serve router tests pass. The two BLOCKING findings are structural and do not require live portal verification.
Open questions for the author
- Is
patch --dry-runintentionally agent-callable against service accounts only (personal accounts would MFA-block)? If yes, the workflow doc should say so explicitly. If personal accounts should be supported for dry-run, the prepare layer needs to handle the no-TTY case gracefully (return a diff stub or skip the get_app call).
|
Thanks for the re-review — both BLOCKING findings were real regressions from my earlier follow-up commits. All six findings addressed in [B-1] auth bypass on [B-2] stale bearer permanent lockout in serve — fixed [NB-1] [NB-2] [NIT-1/NIT-2]
|
Adds the design document produced during /superpowers:brainstorming for wrapping the Keboola Developer Portal API (apps-api.keboola.com) in kbagent. Spec covers data model (multi-identity, mirrors KB project storage), client/service/command 3-layer split, the random-code TTY confirm safety bar with no env-var bypass, v1 op scope (list/get/create/patch/upload-icon/publish/deprecate plus peers lookup), permission-registry integration, testing layout, and the rule #17 documentation-sync checklist. No implementation yet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the `dev-portal peers` helper from v1 scope -- the agent can compose peer-config research from `list` + `get` directly. Adds the 16-task implementation plan produced by /superpowers:writing-plans. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the load-bearing safety primitive from commands/permissions.py into commands/_helpers.py so upcoming Developer Portal write commands can reuse it without duplicating the guard logic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DeveloperPortalService skeleton: add/list/remove/edit/rename/use/verify identity methods. add_identity runs the login probe BEFORE persisting so bad credentials never land in config.json. Tests cover happy path, verify-failure-no-persist, use_identity default, and remove. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add 13 dev-portal.* entries to OPERATION_REGISTRY, resolve_identity_alias() helper, and get_dev_portal_service() factory to _helpers.py; cover with TestDevPortalPermissions test class. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Task 12 added entries with hyphenated identity names (dev-portal.identity-add)
that are unreachable -- check_cli_permission builds keys from the Typer tree
as `{group}.{subcommand}`, so identity-sub-app leaves are `dev-portal.identity.add`
(dotted). Task 13 added the correct dotted form and the original hyphenated
entries were dead. This:
- Drops the 6 unreachable hyphenated identity entries (Task 12 leftovers).
- Adds `dev-portal.identity: read` so the parent-callback descent is allowed.
- Realigns categories to data-app.secrets-* precedent: credential add/edit
are `write` (admin is reserved for org-level ops).
- Wires callbacks on dev_portal_app and identity_app so the engine actually
fires on these commands.
- Updates TestDevPortalPermissions to assert on the actual runtime keys.
Add create / patch / upload-icon / publish / deprecate commands under `kbagent dev-portal`. Each write command calls `_assert_tty()` as its very first action (before any file I/O or API calls), refusing with exit 6 on non-TTY shells. `--dry-run` bypasses both the TTY check and the random-code prompt, prints a preview, and exits 0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bump version to 0.48.0 for the `kbagent dev-portal` command group and update all silent-drift surfaces per CONTRIBUTING.md rule #17. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Task 15's additions to keboola-expert.md (version-gate, tool-matrix row, inline gotcha) pushed it 1,652 bytes over the CI-enforced 60,000-byte budget. Drops all three additions; dev-portal coverage in the agent surface lives in AGENT_CONTEXT, SKILL.md decision table, commands-reference.md, dev-portal-workflow.md, and gotchas.md (which have no equivalent size cap). File now back to 59,992 bytes. Matches the pattern of fe246e5 (which previously reverted a similar edit for the same reason).
The auto-generated decision table in SKILL.md is sourced from each Typer command's help text via scripts/generate_skill.py. Task 13/14 didn't add help= to the @dev_portal_app.command decorators, so the table generator skipped every dev-portal command and the prior manual entries got stripped on make skill-check. Adds concise help text to every dev-portal command (identity sub-app + list/get + create/patch/upload-icon/publish/deprecate), then regenerates SKILL.md. The auto-generated rows now correctly enumerate the full dev-portal surface.
patch and publish opened a fresh portal client in prepare_* (to read current state) and again in apply() (to write), each triggering its own /auth/login. On a personal MFA account that meant TWO MFA prompts for a single write; on a service account it was a redundant second login. The service now caches the bearer per alias (in-memory, rebuilt per CLI invocation) and seeds it into the apply() client via a new client.seed_bearer()/bearer API, so the human authenticates at most once per command. create/deprecate/upload-icon were already single-login (their prepare_* defers auth) and are unaffected. Also type BaseHttpClient.__enter__ as Self so `with <subclass>() as c` keeps the concrete type — this clears the pre-existing ty errors where DeveloperPortalClient methods were unresolved on BaseHttpClient inside `with` blocks (dev_portal_service + test_dev_portal_client).
Add server/routers/dev_portal.py with GET /dev-portal/apps (list by
vendor) and GET /dev-portal/apps/{app} (get one), wired into the FastAPI
app + ServiceRegistry. Mirrors `kbagent dev-portal list|get` so external
consumers (Web UI, scheduled agents) can do peer-config research over the
REST surface.
Writes (create/patch/upload-icon/publish/deprecate) and identity
management stay CLI-only by design: writes require a human to type a
random code on a TTY (meaningless over HTTP), and identity commands
handle login credentials that must not travel over this API. The skip is
documented in the router module docstring and the OpenAPI tag.
Give the dev-portal group one per-group row in §2 (reads agent-safe; writes TTY-confirmed, never raw apps-api). Stays under the 60 KB prompt budget by trimming now-deprecated `--hint client` fallbacks from five existing matrix rows (per the policy merged in #355: --hint is superseded by `kbagent serve`).
`_is_ui_public` treats any GET not matching `api_prefixes` as an SPA route and serves it without bearer validation. The new `/dev-portal` router was not added to that allow-list, so `GET /dev-portal/apps` was reachable unauthenticated in `kbagent serve --ui` mode (script/curl callers; browser users with the session cookie were unaffected). Add `/dev-portal` to `api_prefixes` and cover it with a 401-without-auth test.
The per-alias bearer cache is harmless in the CLI (service rebuilt per invocation) but the `kbagent serve` ServiceRegistry is a long-lived singleton, so a cached bearer outlives its portal-side TTL. Once expired, every `_authed_client` call re-seeded the dead token and 401'd forever -- permanent lockout until restart. `_authed_client` now drops `_bearers[alias]` on INVALID_TOKEN / DP_LOGIN_FAILED so the next call re-authenticates. Regression test seeds a stale bearer, asserts the 401 propagates AND the entry is evicted, then that a follow-up call logs in fresh and succeeds.
… gate Review polish (non-blocking): - patch/publish `--dry-run` help + dev-portal-workflow.md now state that the preview still logs in and GETs the app (needs connectivity; a personal/MFA identity prompts for MFA) -- use a service account for a fully non-interactive preview. - `_render_pending` / `_pending_as_json` drop their `# type: ignore` workarounds and annotate with `OutputFormatter` / `PendingWrite` (TYPE_CHECKING import). - keboola-expert.md §1 VERSION GATE lists `dev-portal = 0.48.0+`; offset by trimming two now-deprecated `--hint client` prose mentions (stays under the 60 KB prompt budget).
#356 (`kbagent feature` command group) merged to main as 0.48.0 and that tag is cut, so dev-portal moves to its own 0.49.0 instead of colliding. - pyproject / plugin.json / marketplace.json / uv.lock -> 0.49.0 - changelog: dev-portal entries live under a new 0.49.0 key, above main's 0.48.0 (feature flags) block (done during the rebase) - flip dev-portal "since v0.48.0" doc tags -> v0.49.0: AGENT_CONTEXT, commands-reference, gotchas, keboola-expert version-gate + matrix row
…--password-stdin Three independent fixes against the dev-portal surface that landed in #354, discovered while integrating ABRA Flexi (a real component registration on production apps-api): 1. **Admin-role PATCH routing**. `complexity`, `categories`, `forwardToken`, `forwardTokenDetails`, `injectEnvironment`, `processTimeout`, `requiredMemory`, `features`, and `category` are `.forbidden()` on the apps-api vendor schema (`PATCH /vendors/{vendor}/apps/{app}`) -- but the server's error message is misleading: it says "must be one of: easy, medium, hard" because the enum-validation `.error()` annotation lives on the shared admin schema before `clientAppSchema()` overrides with `.forbidden()`. Source of truth: keboola/developer-portal:src/lib/ validation.js -> clientAppSchema(). Fix: - `DeveloperPortalIdentity.role_hint` becomes a real validator: only `vendor` (default) or `admin` accepted; case-folded; typos raise. The field is now load-bearing, not a free-text label. - `DeveloperPortalClient.patch_app` reads `self._identity.role_hint` and routes admin identities to `PATCH /admin/apps/{app}` (permissive adminAppSchema); vendor identities stay on the vendor endpoint. - `DeveloperPortalService.prepare_patch` preflights: vendor role + admin-only field => fail-fast `VALIDATION_ERROR` with a message that (a) names every offending field, (b) explains why the 422 is misleading, (c) tells the user the exact command to switch identity (`dev-portal identity add --role-hint admin ...`). Admin role bypasses the preflight entirely. - Reads, create, upload-icon, deprecate keep vendor-endpoint behaviour -- only PATCH has a meaningful admin variant on the server. Admin tokens still work on the vendor path for those (superset perms). 2. **MFA login: explicit `challenge` field + actual error surfaced**. User report from a Keboola-org TOTP account: MFA code: 521278 Error: Developer Portal MFA login failed (HTTP 404) Root cause: the apiary spec calls `challenge` optional with default `SOFTWARE_TOKEN_MFA`, but in practice the server 404s when it's omitted. Sending it explicitly fixes it. Single attempt only: an earlier experiment retried with `SMS_MFA` on the same session, but `/auth/login` consumes the session, so the retry always 404'd with "Invalid code or auth state for the user", masking the real first failure (most often a stale 30-second TOTP code from waiting too long to enter it). The error now includes the server response body (truncated to 500 chars) and a hint about TOTP code freshness, so users can tell whether the code was wrong, the session expired, or something else. 3. **`--password-stdin` no longer hangs interactively**. `sys.stdin.read()` waits for EOF, not Enter -- users who pasted a password and pressed Enter sat there until they Ctrl-C'd out. New `_read_password_stdin()` helper branches on `sys.stdin.isatty()`: TTY uses `getpass.getpass()` (hidden, line-based, Enter to confirm); pipe still does `read() -> strip()`. Both `identity add --password-stdin` and `identity edit --password-stdin` route through it. Help text updated to spell out the dual-mode behaviour. Tests (10 new): - TestReadPasswordStdin: TTY -> getpass, pipe -> read. - TestLoginMfaPath::test_mfa_prompt_completes_login: now matches body including `challenge: SOFTWARE_TOKEN_MFA`. - TestLoginMfaPath::test_mfa_failure_surfaces_server_body: real body bubbles up plus stale-TOTP hint. - TestPortalWrites::test_patch_app_vendor_role_hits_vendor_endpoint + test_patch_app_admin_role_hits_admin_endpoint: confirm dispatch. - TestDeveloperPortalIdentity::test_role_hint_accepts_admin + test_role_hint_normalises_case + test_role_hint_rejects_typo. - TestReadsAndPrepareApply::test_prepare_patch_vendor_role_rejects_admin_only_fields + test_prepare_patch_admin_role_allows_admin_only_fields. All 95 dev-portal tests pass; `make check` green (3827 / 8 skipped).
…--password-stdin Three independent fixes against the dev-portal surface that landed in #354, discovered while integrating ABRA Flexi (a real component registration on production apps-api): 1. **Admin-role PATCH routing**. `complexity`, `categories`, `forwardToken`, `forwardTokenDetails`, `injectEnvironment`, `processTimeout`, `requiredMemory`, `features`, and `category` are `.forbidden()` on the apps-api vendor schema (`PATCH /vendors/{vendor}/apps/{app}`) -- but the server's error message is misleading: it says "must be one of: easy, medium, hard" because the enum-validation `.error()` annotation lives on the shared admin schema before `clientAppSchema()` overrides with `.forbidden()`. Source of truth: keboola/developer-portal:src/lib/ validation.js -> clientAppSchema(). Fix: - `DeveloperPortalIdentity.role_hint` becomes a real validator: only `vendor` (default) or `admin` accepted; case-folded; typos raise. The field is now load-bearing, not a free-text label. - `DeveloperPortalClient.patch_app` reads `self._identity.role_hint` and routes admin identities to `PATCH /admin/apps/{app}` (permissive adminAppSchema); vendor identities stay on the vendor endpoint. - `DeveloperPortalService.prepare_patch` preflights: vendor role + admin-only field => fail-fast `VALIDATION_ERROR` with a message that (a) names every offending field, (b) explains why the 422 is misleading, (c) tells the user the exact command to switch identity (`dev-portal identity add --role-hint admin ...`). Admin role bypasses the preflight entirely. - Reads, create, upload-icon, deprecate keep vendor-endpoint behaviour -- only PATCH has a meaningful admin variant on the server. Admin tokens still work on the vendor path for those (superset perms). 2. **MFA login: explicit `challenge` field + actual error surfaced**. User report from a Keboola-org TOTP account: MFA code: 521278 Error: Developer Portal MFA login failed (HTTP 404) Root cause: the apiary spec calls `challenge` optional with default `SOFTWARE_TOKEN_MFA`, but in practice the server 404s when it's omitted. Sending it explicitly fixes it. Single attempt only: an earlier experiment retried with `SMS_MFA` on the same session, but `/auth/login` consumes the session, so the retry always 404'd with "Invalid code or auth state for the user", masking the real first failure (most often a stale 30-second TOTP code from waiting too long to enter it). The error now includes the server response body (truncated to 500 chars) and a hint about TOTP code freshness, so users can tell whether the code was wrong, the session expired, or something else. 3. **`--password-stdin` no longer hangs interactively**. `sys.stdin.read()` waits for EOF, not Enter -- users who pasted a password and pressed Enter sat there until they Ctrl-C'd out. New `_read_password_stdin()` helper branches on `sys.stdin.isatty()`: TTY uses `getpass.getpass()` (hidden, line-based, Enter to confirm); pipe still does `read() -> strip()`. Both `identity add --password-stdin` and `identity edit --password-stdin` route through it. Help text updated to spell out the dual-mode behaviour. Tests (10 new): - TestReadPasswordStdin: TTY -> getpass, pipe -> read. - TestLoginMfaPath::test_mfa_prompt_completes_login: now matches body including `challenge: SOFTWARE_TOKEN_MFA`. - TestLoginMfaPath::test_mfa_failure_surfaces_server_body: real body bubbles up plus stale-TOTP hint. - TestPortalWrites::test_patch_app_vendor_role_hits_vendor_endpoint + test_patch_app_admin_role_hits_admin_endpoint: confirm dispatch. - TestDeveloperPortalIdentity::test_role_hint_accepts_admin + test_role_hint_normalises_case + test_role_hint_rejects_typo. - TestReadsAndPrepareApply::test_prepare_patch_vendor_role_rejects_admin_only_fields + test_prepare_patch_admin_role_allows_admin_only_fields. All 95 dev-portal tests pass; `make check` green (3827 / 8 skipped).
…--password-stdin Three independent fixes against the dev-portal surface that landed in #354, discovered while integrating ABRA Flexi (a real component registration on production apps-api): 1. **Admin-role PATCH routing**. `complexity`, `categories`, `forwardToken`, `forwardTokenDetails`, `injectEnvironment`, `processTimeout`, `requiredMemory`, `features`, and `category` are `.forbidden()` on the apps-api vendor schema (`PATCH /vendors/{vendor}/apps/{app}`) -- but the server's error message is misleading: it says "must be one of: easy, medium, hard" because the enum-validation `.error()` annotation lives on the shared admin schema before `clientAppSchema()` overrides with `.forbidden()`. Source of truth: keboola/developer-portal:src/lib/ validation.js -> clientAppSchema(). Fix: - `DeveloperPortalIdentity.role_hint` becomes a real validator: only `vendor` (default) or `admin` accepted; case-folded; typos raise. The field is now load-bearing, not a free-text label. - `DeveloperPortalClient.patch_app` reads `self._identity.role_hint` and routes admin identities to `PATCH /admin/apps/{app}` (permissive adminAppSchema); vendor identities stay on the vendor endpoint. - `DeveloperPortalService.prepare_patch` preflights: vendor role + admin-only field => fail-fast `VALIDATION_ERROR` with a message that (a) names every offending field, (b) explains why the 422 is misleading, (c) tells the user the exact command to switch identity (`dev-portal identity add --role-hint admin ...`). Admin role bypasses the preflight entirely. - Reads, create, upload-icon, deprecate keep vendor-endpoint behaviour -- only PATCH has a meaningful admin variant on the server. Admin tokens still work on the vendor path for those (superset perms). 2. **MFA login: explicit `challenge` field + actual error surfaced**. User report from a Keboola-org TOTP account: MFA code: 521278 Error: Developer Portal MFA login failed (HTTP 404) Root cause: the apiary spec calls `challenge` optional with default `SOFTWARE_TOKEN_MFA`, but in practice the server 404s when it's omitted. Sending it explicitly fixes it. Single attempt only: an earlier experiment retried with `SMS_MFA` on the same session, but `/auth/login` consumes the session, so the retry always 404'd with "Invalid code or auth state for the user", masking the real first failure (most often a stale 30-second TOTP code from waiting too long to enter it). The error now includes the server response body (truncated to 500 chars) and a hint about TOTP code freshness, so users can tell whether the code was wrong, the session expired, or something else. 3. **`--password-stdin` no longer hangs interactively**. `sys.stdin.read()` waits for EOF, not Enter -- users who pasted a password and pressed Enter sat there until they Ctrl-C'd out. New `_read_password_stdin()` helper branches on `sys.stdin.isatty()`: TTY uses `getpass.getpass()` (hidden, line-based, Enter to confirm); pipe still does `read() -> strip()`. Both `identity add --password-stdin` and `identity edit --password-stdin` route through it. Help text updated to spell out the dual-mode behaviour. Tests (10 new): - TestReadPasswordStdin: TTY -> getpass, pipe -> read. - TestLoginMfaPath::test_mfa_prompt_completes_login: now matches body including `challenge: SOFTWARE_TOKEN_MFA`. - TestLoginMfaPath::test_mfa_failure_surfaces_server_body: real body bubbles up plus stale-TOTP hint. - TestPortalWrites::test_patch_app_vendor_role_hits_vendor_endpoint + test_patch_app_admin_role_hits_admin_endpoint: confirm dispatch. - TestDeveloperPortalIdentity::test_role_hint_accepts_admin + test_role_hint_normalises_case + test_role_hint_rejects_typo. - TestReadsAndPrepareApply::test_prepare_patch_vendor_role_rejects_admin_only_fields + test_prepare_patch_admin_role_allows_admin_only_fields. All 95 dev-portal tests pass; `make check` green (3827 / 8 skipped).
…--password-stdin (#366) * feat(dev-portal): admin-role PATCH routing + MFA fixes + interactive --password-stdin Three independent fixes against the dev-portal surface that landed in #354, discovered while integrating ABRA Flexi (a real component registration on production apps-api): 1. **Admin-role PATCH routing**. `complexity`, `categories`, `forwardToken`, `forwardTokenDetails`, `injectEnvironment`, `processTimeout`, `requiredMemory`, `features`, and `category` are `.forbidden()` on the apps-api vendor schema (`PATCH /vendors/{vendor}/apps/{app}`) -- but the server's error message is misleading: it says "must be one of: easy, medium, hard" because the enum-validation `.error()` annotation lives on the shared admin schema before `clientAppSchema()` overrides with `.forbidden()`. Source of truth: keboola/developer-portal:src/lib/ validation.js -> clientAppSchema(). Fix: - `DeveloperPortalIdentity.role_hint` becomes a real validator: only `vendor` (default) or `admin` accepted; case-folded; typos raise. The field is now load-bearing, not a free-text label. - `DeveloperPortalClient.patch_app` reads `self._identity.role_hint` and routes admin identities to `PATCH /admin/apps/{app}` (permissive adminAppSchema); vendor identities stay on the vendor endpoint. - `DeveloperPortalService.prepare_patch` preflights: vendor role + admin-only field => fail-fast `VALIDATION_ERROR` with a message that (a) names every offending field, (b) explains why the 422 is misleading, (c) tells the user the exact command to switch identity (`dev-portal identity add --role-hint admin ...`). Admin role bypasses the preflight entirely. - Reads, create, upload-icon, deprecate keep vendor-endpoint behaviour -- only PATCH has a meaningful admin variant on the server. Admin tokens still work on the vendor path for those (superset perms). 2. **MFA login: explicit `challenge` field + actual error surfaced**. User report from a Keboola-org TOTP account: MFA code: 521278 Error: Developer Portal MFA login failed (HTTP 404) Root cause: the apiary spec calls `challenge` optional with default `SOFTWARE_TOKEN_MFA`, but in practice the server 404s when it's omitted. Sending it explicitly fixes it. Single attempt only: an earlier experiment retried with `SMS_MFA` on the same session, but `/auth/login` consumes the session, so the retry always 404'd with "Invalid code or auth state for the user", masking the real first failure (most often a stale 30-second TOTP code from waiting too long to enter it). The error now includes the server response body (truncated to 500 chars) and a hint about TOTP code freshness, so users can tell whether the code was wrong, the session expired, or something else. 3. **`--password-stdin` no longer hangs interactively**. `sys.stdin.read()` waits for EOF, not Enter -- users who pasted a password and pressed Enter sat there until they Ctrl-C'd out. New `_read_password_stdin()` helper branches on `sys.stdin.isatty()`: TTY uses `getpass.getpass()` (hidden, line-based, Enter to confirm); pipe still does `read() -> strip()`. Both `identity add --password-stdin` and `identity edit --password-stdin` route through it. Help text updated to spell out the dual-mode behaviour. Tests (10 new): - TestReadPasswordStdin: TTY -> getpass, pipe -> read. - TestLoginMfaPath::test_mfa_prompt_completes_login: now matches body including `challenge: SOFTWARE_TOKEN_MFA`. - TestLoginMfaPath::test_mfa_failure_surfaces_server_body: real body bubbles up plus stale-TOTP hint. - TestPortalWrites::test_patch_app_vendor_role_hits_vendor_endpoint + test_patch_app_admin_role_hits_admin_endpoint: confirm dispatch. - TestDeveloperPortalIdentity::test_role_hint_accepts_admin + test_role_hint_normalises_case + test_role_hint_rejects_typo. - TestReadsAndPrepareApply::test_prepare_patch_vendor_role_rejects_admin_only_fields + test_prepare_patch_admin_role_allows_admin_only_fields. All 95 dev-portal tests pass; `make check` green (3827 / 8 skipped). * test(dev-portal): use DP_MFA_CHALLENGE_TYPE constant in client tests Replace the two hardcoded "SOFTWARE_TOKEN_MFA" match_json literals with the DP_MFA_CHALLENGE_TYPE constant from constants.py, following through on the NIT-1 constant extraction so the tests can't silently diverge from the client. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Adds a
kbagent dev-portalcommand group wrapping the Keboola Developer Portal API (apps-api.keboola.com). Production-safe by design: every write requires a human at a real terminal to type a random hex code — there is no--yesflag, no env-var override, and non-TTY shells exit 6 immediately.config.jsonalongside KB project tokens (0600,_warningheader extended to mention DP credentials). MFA codes prompted on/dev/ttyfor personal accounts; service accounts (service.{vendor}.{id}) skip MFA.--dry-runpreview path.require_random_code_confirmation()extracted fromcommands/permissions.pyintocommands/_helpers.pyso the same primitive backspermissions set/resetand every dev-portal write.dev-portal.*operations (--deny-writesblocks all writes automatically;deprecateisdestructive,publishisadmin).Version bumped to
0.48.0. All rule #17 silent-drift surfaces updated (AGENT_CONTEXT,CLAUDE.md,SKILL.md,commands-reference.md,gotchas.md, newdev-portal-workflow.md).Spec + plan
docs/superpowers/specs/2026-05-28-dev-portal-design.mddocs/superpowers/plans/2026-05-28-dev-portal.mdTest plan
make checkpasses (3667 passed, 8 skipped, 0 failures)tests/test_dev_portal_client.py,test_dev_portal_service.py,test_dev_portal_cli.pyall passtests/test_helpers.py::TestRequireRandomCodeConfirmationcovers TTY/non-TTY/correct-code/wrong-code/EOFkbagent dev-portal --helpshows the full surface;kbagent dev-portal patch --helpshows--dry-run(no--yes)kbagent dev-portal patch --app x.y --data /tmp/p.jsonon a non-TTY shell exits 6--dry-runexits 0, prints diff, no portal callE2E_DP_USERNAME/E2E_DP_PASSWORDto exercisetests/test_e2e.py::TestDevPortalE2E::test_list_apps_against_real_portal🤖 Generated with Claude Code