Skip to content

feat(commands): comandos dinâmicos dirigidos por manifest com executor REST genérico (LIN-6332)#10

Open
djalmaaraujo wants to merge 11 commits into
mainfrom
djalma1/lin-6332-dynamic-base
Open

feat(commands): comandos dinâmicos dirigidos por manifest com executor REST genérico (LIN-6332)#10
djalmaaraujo wants to merge 11 commits into
mainfrom
djalma1/lin-6332-dynamic-base

Conversation

@djalmaaraujo

@djalmaaraujo djalmaaraujo commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Fecha a trilha CLI da base de comandos dinâmicos da LIN-6332.

O que muda

A CLI passa a montar comandos Cobra em runtime a partir de um cli-manifest.json gerado pelo backend Rails (linkana#14102 + linkana#14103) e vendorado aqui via go:embed. Um executor REST genérico faz o request; comandos manuais têm precedência sobre dinâmicos em colisão de nome.

  • internal/manifest/: types do schema v1, Load() com validação (command/method/path obrigatórios, tipos fechados, path_params consistentes com o path, nomes reservados rejeitados) e a cópia vendorada do contrato (4 endpoints: identity show, settings email-message list|show|update).
  • internal/client: método genérico Do(ctx, method, path, query, payload) roteado pelo do() existente — gate read/write (ErrReadOnly), Bearer, sufixo .json e 401 herdados sem mudança no core.
  • internal/commands/dynamic.go + dynamic_exec.go: builder de grupos aninhados a partir de command[], flags tipadas (string/integer/boolean nativos; date/datetime/decimal como string; array de scalar repete a flag; object/array-de-object como flag JSON), path params como args posicionais, help LLM-first (Endpoint/Auth/Arguments/Parameters/Response). Roteamento query/body honra o campo in do param. Erros com a mesma UX dos comandos manuais (hint de lk auth login no 401, ciente de impersonação).
  • lk version mostra manifest: <generated_at> (<source>).
  • SURFACE.txt: golden da árvore completa de comandos — qualquer drift/takeover na superfície aparece no diff.
  • make update-manifest: baixa a cópia commitada do repo Rails com validação de shape via jq; falha limpa em 404 sem tocar a cópia local.

Por quê

Hoje cada endpoint exige struct + método no client + comando Cobra à mão. Com o manifest, expor um endpoint novo vira mudança só no Rails + refresh do manifest aqui. O consumidor primário é agente LLM, então o help estruturado é documentação de primeira classe.

Como testar

Parte 1 — sem backend (5 min)

make build
./lk --help                                 # grupos `identity` e `settings` aparecem junto dos manuais
./lk identity show --help                   # help LLM-first: Endpoint, Auth, Response
./lk settings email-message update --help   # arg posicional <template> + flag --content do manifest
./lk version                                # bloco manifest com generated_at/source
./lk identity show                          # sem token: `error: not authenticated; run 'lk auth login'`, exit 1
make test && make lint && make cover        # cobertura total 96.1% (gate 95%)

Parte 2 — e2e contra o backend local (o teste de verdade)

Pré-requisito: o repo linkana na branch djalma1/lin-6332-email-messages (PR #14103), com Postgres de pé e banco dev semeado (bin/rails db:prepare db:seed).

  1. Suba o backend: DB_HOST=localhost bin/rails server -p 3000 (no repo linkana).
  2. Cunhe um PAT de um usuário admin do buyer demo (policy exige MANAGER+ pra ler e ADMIN+ pra escrever):
    bin/rails runner 'u = User.find_by!(email: "admin@demo.local"); t = ApiTokens::Build.call(key_name: "review", buyer_id: u.buyer_id, user_id: u.id).tap(&:save!); puts t.one_time_display_api_key'
  3. Configure a CLI e confira o diagnóstico:
    export LK_API_URL=http://localhost:3000 LK_TOKEN=lkn_...   # token do passo 2
    ./lk doctor        # esperado: 6/6 pass, incl. Authentication (admin@demo.local)
  4. Leituras (funcionam no modo read, que é o default):
    ./lk identity show                              # {id, email, name, role, buyer_id, is_staff}
    ./lk settings email-message list                # array de {template, content, updated_at}
    ./lk settings email-message show supplier_invite
  5. Gate de escrita — primeiro prove o bloqueio:
    ./lk settings email-message update supplier_invite --content "teste"
    # esperado: `error: CLI is in read mode: run 'lk mode write' to enable writes`, exit 1, nada muda no servidor
    ./lk mode write    # interativo: digite "write" (exige terminal de verdade — pipe é recusado, by design)
    ./lk settings email-message update supplier_invite --content "teste $(date +%s)"
    # esperado: 200 com o objeto atualizado; confira com o show em seguida
  6. Caminhos de erro:
    ./lk settings email-message show nao_existe                 # 404 {"error":"SettingEmailMessage not found"}, exit 1
    ./lk settings email-message update supplier_invite --content "$(python3 -c 'print("x"*300)')"
                                                                # 422 {"errors":["Content é muito longo..."]}, exit 1
    LK_TOKEN=lkn_invalid ./lk identity show                     # 401 com hint `lk auth login`, exit 1
    Pra ver o 403, repita o passo 2 com observer@demo.local e rode o list{"error":"..."} no stderr, exit 1.
  7. Volte pro modo seguro e revogue o PAT: ./lk mode read e, no linkana, bin/rails runner 'ApiToken.where(key_name: "review").each(&:revoke!)'.

Esse roteiro completo (14 passos, incluindo CSRF via curl) já foi executado contra o backend real — 14/14 pass; ver relatório na LIN-6332.

Notas de review

  • Colisão de nome: leaf manual existente vence e o dinâmico é pulado em silêncio — coberto por teste e visível no SURFACE.txt.
  • Falha de load do manifest embutido desabilita os dinâmicos sem panic (o embedded é validado por teste em CI; lk version denuncia com manifest: null).
  • 204/body vazio → stdout limpo, exit 0.
  • --format styled nos comandos dinâmicos cai no fallback JSON (RawMessage não implementa Styler) — by design nesta fase.
  • Igualdade com o manifest do backend é sobre endpoints (via jq), não byte-diff — a formatação de arrays difere entre o Ruby e a cópia vendorada.
  • make update-manifest só funciona depois que o #14102/#14103 mergearem no main do linkana (o arquivo ainda não existe lá; até lá o target falha limpo).

@djalmaaraujo
djalmaaraujo marked this pull request as ready for review July 17, 2026 23:33

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 21 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/commands/dynamic_exec.go">

<violation number="1" location="internal/commands/dynamic_exec.go:141">
P2: Object and array-of-object flags accept any valid JSON scalar/container, then send a payload that violates the manifest's declared type. Validate the decoded top-level shape (and array elements) before issuing the request so invalid flag values fail locally like native typed flags.</violation>
</file>

<file name="internal/manifest/manifest.go">

<violation number="1" location="internal/manifest/manifest.go:150">
P2: A manifest parameter without `name` passes validation but becomes an empty-name Cobra flag, making required parameters impossible to provide and sending no value. Reject empty parameter names while loading the manifest.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

if err := json.Unmarshal([]byte(raw), &v); err != nil {
return nil, fmt.Errorf("--%s must be valid JSON (%s): %w", name, kind, err)
}
return v, nil

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: Object and array-of-object flags accept any valid JSON scalar/container, then send a payload that violates the manifest's declared type. Validate the decoded top-level shape (and array elements) before issuing the request so invalid flag values fail locally like native typed flags.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/commands/dynamic_exec.go, line 141:

<comment>Object and array-of-object flags accept any valid JSON scalar/container, then send a payload that violates the manifest's declared type. Validate the decoded top-level shape (and array elements) before issuing the request so invalid flag values fail locally like native typed flags.</comment>

<file context>
@@ -0,0 +1,195 @@
+	if err := json.Unmarshal([]byte(raw), &v); err != nil {
+		return nil, fmt.Errorf("--%s must be valid JSON (%s): %w", name, kind, err)
+	}
+	return v, nil
+}
+
</file context>

var reservedParamNames = map[string]bool{"format": true, "help": true, "h": true}

func (p *Param) validate() error {
if reservedParamNames[p.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.

P2: A manifest parameter without name passes validation but becomes an empty-name Cobra flag, making required parameters impossible to provide and sending no value. Reject empty parameter names while loading the manifest.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/manifest/manifest.go, line 150:

<comment>A manifest parameter without `name` passes validation but becomes an empty-name Cobra flag, making required parameters impossible to provide and sending no value. Reject empty parameter names while loading the manifest.</comment>

<file context>
@@ -0,0 +1,173 @@
+var reservedParamNames = map[string]bool{"format": true, "help": true, "h": true}
+
+func (p *Param) validate() error {
+	if reservedParamNames[p.Name] {
+		return fmt.Errorf("reserved name (would shadow the built-in --%s flag)", p.Name)
+	}
</file context>

@djalmaaraujo
djalmaaraujo requested a review from cirdes July 18, 2026 11:32
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