Referência

O manifesto: vssh-app.json

Página gerada do schema do manifesto (api/vssh-app.schema.json) pelo canal de publicação do sistema. Uma mudança se faz na fonte, schema/vssh-app.schema.json do vssh-sso.

Manifesto de um vssh-app. A fonte deste schema é o repositório do sistema, vssh-sso, em schema/; o canal de publicação o copia para api/ do vssh-sdk, onde o vssh-app-publish o lê e valida o manifesto por inteiro antes de o app entrar no repositório. O vssh-app-install valida só o mínimo, porque roda offline no servidor e não tem como buscar o schema. additionalProperties: false em todo objeto, a raiz inclusive: um campo desconhecido é recusado na publicação, porque requiredPackage (sem o s), gpuu ou opns publicariam limpos e seriam descartados em silêncio por quem lê. Um manifesto escrito para um SDK mais novo é recusado por um mais velho, e essa é a informação que interessa. Todo campo que o portal lê está declarado aqui, e tests/unit/contrato-do-manifesto.test.js do vssh-sso confere isso a cada campo novo.

Um objeto com os campos abaixo, e nenhum outro: o schema fecha a raiz com additionalProperties: false, e um campo que ele não conhece recusa o manifesto inteiro, com o nome do campo na mensagem. Obrigatórios: id, version, backend.

campotipoobrigatório
idstringsim
namestringnão
versionstringsim
typeum dos valores listadosnão
kindum dos valores listadosnão
escopoum dos valores listadosnão
engineobjectnão
providesarray de stringnão
requiresarray de stringnão
usaarray de stringnão
capacidadesobjectnão
minShellVersionstringnão
contributesobjectnão
requiredPackagesarray de stringnão
motoresarray de objectnão
instalacaoarray de stringnão
resourcesobjectnão
recursosobjectnão
gpubooleannão
secretsarray de objectnão
iconstringnão
categorystringnão
descriptionstringnão
developerobjectnão
publisherobjectnão
homepagestringnão
licensestringnão
handlesstring ou nullnão
opensobjectnão
backendobjectsim
windowobjectnão

id

string, obrigatório.

Imutável — vira path, ENDEREÇO (~/.vssh-apps/<id>/app.sock) e sentinel. O endereço não é alocado: ele é DERIVADO de (HOME, id), e é por isso que trocar o id é publicar outro app.

name

string, opcional.

Nome exibido. Default: o próprio id.

version

string, obrigatório.

Semver. O install é idempotente por versão, então bump a cada release.

type

um dos valores listados, opcional, padrão "app", valores "app", "engine".

'engine' = backend-only, sem janela nem ícone no Launchpad.

kind

um dos valores listados, opcional, padrão "app", valores "app", "service".

Eixo de LIFECYCLE, ortogonal a type: 'service' é daemon supervisionado (start automático, auto-restart, status). Um type: engine que oferece capacidade (provides) e não traz engine.loader sobe quando alguém pede a capacidade, e não no load do ambiente.

escopo

um dos valores listados, opcional, padrão "pessoa", valores "pessoa", "servidor".

Quantas instâncias o servidor roda. 'pessoa' é um backend por conta, como a pessoa, num socket em ~/.vssh-apps/<id>/. 'servidor' é um backend só para todas as contas do servidor: o vssh-app-install cria a conta de sistema vssh-<id> e a unit vssh-app-<id>.service, o socket fica em /run/vssh-apps/<id>/app.sock e o estado em /var/lib/vssh-apps/<id>/. O systemd sobe e relança a unit, e o portal não oferece start nem stop por pessoa. Pede kind: 'service' e um id de até 27 caracteres, que é o que cabe no nome da conta. O socket de um app de servidor aceita qualquer conta do servidor, então a autenticação é do app.

engine

object, opcional, sem campo além dos listados.

Só para type:'engine'. Um motor pode trazer o PRÓPRIO lado-cliente, em vez de o ambiente carregar um arquivo que conhece o motor pelo nome.

engine.loader

string, opcional.

Caminho, relativo à raiz servida pelo backend, de um script que o ambiente carrega depois de o backend responder. É ele que registra o motor (ex.: em RemoteDesktopEngines). Sem este campo o ambiente não carrega script nenhum do app — que é o caso de um motor cujo lado-cliente já mora no ambiente.

provides

array de string, opcional.

Capacidades que este app oferece a OUTROS apps e ao ambiente, no formato 'nome/vN'. Declarar é o que permite trocar o produtor sem tocar em nenhum consumidor: um app de chat pede 'llm/v1' e recebe o motor que estiver instalado, seja ollama, vLLM ou outro. Sem isto o consumidor precisa FIXAR o appId no código — que é o que o shell fazia com o 'scramjet-wisp'. Declarar não é provar: o ambiente não verifica se o app cumpre a capacidade, pelo mesmo motivo que o registro de motores não verifica (exigir transforma 'este app não faz isso' em 'este app não carrega'). Quem não puder atender falha por dentro, que é onde sabe dizer por quê.

requires

array de string, opcional.

Capacidades que este app exige de outros, no formato 'nome/vN': a metade que completa o provides. Um app de chat que precisa de um modelo declara requires: ["llm/v1"] e recebe o motor que estiver instalado, seja qual for. Quem confere é o portal, na instalação, contra o provides dos apps instalados naquele servidor: uma capacidade que ninguém oferece recusa a instalação nomeando-a, porque instalar um app que vai falhar por dentro no primeiro uso é pior que dizer o que falta. A conferência lê o catálogo do repositório, que é o único que responde antes de instalar; um catálogo sem este campo não confere. Só o nome com a versão, sem faixa nem comparação: uma capacidade é um contrato, e nome/v2 é outro contrato.

usa

array de string, opcional.

Capacidades que este app usa quando algum app as oferece, no formato 'nome/vN': a forma opcional do requires. O Prelo cita da biblioteca do Zotero, e escreve sem ela. A instalação não confere este campo, e a ponte aceita a capacidade em vssh.apps.pedir e em vssh.apps.listar como aceita uma de requires. Sem quem ofereça, o pedir responde 404 com o nome da capacidade, e o listar responde a lista vazia.

capacidades

object, opcional.

Onde cada capacidade de provides é atendida, para outro app a chamar: o nome da capacidade leva ao prefixo das rotas do backend que a respondem. O Zotero declara "bibliography/v1": { "rota": "/bibliografia/v1" }. Outro app chama pela ponte (vssh.apps.pedir(capacidade, caminho, opcoes)): o shell acha quem oferece, pergunta à pessoa na primeira vez que este app usa aquele, sobe o app se estiver parado e faz o pedido como a mesma pessoa, só dentro do prefixo. Quem chama precisa declarar a capacidade em requires ou em usa. Com mais de um app oferecendo a mesma capacidade, quem chama os lista por vssh.apps.listar(capacidade) e escolhe um com { app } no pedir; sem a escolha, o pedido vai ao primeiro, na ordem dos ids. O backend que responde recebe X-Vssh-Chamador com o id de quem chamou. Uma capacidade que não está em provides é ignorada, e uma de provides sem entrada aqui não se chama pela ponte.

capacidades.<chave>.rota

string, obrigatório.

O prefixo das rotas, a partir da raiz do backend do app, sem barra no fim e sem . nem .. como segmento. Um pedido pela ponte só alcança o que fica sob ele.

capacidades.<chave>.rotulo

string, opcional, comprimento mínimo 1, comprimento máximo 60.

Como o app que chama nomeia esta oferta num menu, quando mais de um app oferece a capacidade: Biblioteca do Zotero. O vssh.apps.listar o devolve sem subir o app. Sem ele, quem chama mostra o nome do app.

minShellVersion

string, opcional.

Versão MÍNIMA do shell de que este app precisa. O padrão é NÃO declarar: um campo obrigatório transformaria toda API nova em quebra de compatibilidade declarada, que é burocracia sem benefício. Quem declara está dizendo 'eu uso uma coisa que não existia antes' — a mesma regra do engines do npm, pelo mesmo motivo. NÃO substitui o vssh.capabilities(): um é gate de instalação, o outro é decisão de runtime. Quem confere é o portal, que sabe a versão do shell que serve; o vssh-app-install roda offline no servidor e não tem como saber.

contributes

object, opcional, sem campo além dos listados.

O que este app acrescenta às superfícies do AMBIENTE — coisas que não são a janela dele. Vale para qualquer app, e não só para type:'engine'. NÃO substitui engine.loader, e a diferença é de tamanho: o loader de um motor traz o lado-cliente INTEIRO (megabytes, assets, servidos pelo backend do app, que é um serviço e está de pé); uma contribuição é servida do DISCO pelo portal, então funciona com o backend desligado, que é o caso normal de um app comum. As duas formas que existem aqui são de naturezas diferentes de propósito — settings é CÓDIGO, contextMenu é DADO —, e o porquê está escrito em cada uma.

contributes.settings

string, opcional.

Caminho, relativo à raiz do PACOTE, de um script que registra uma seção em Configurações (SettingsRegistry.register). Carregado quando a janela de Configurações abre — não no boot: um app que ninguém configura não custa nada, e o ambiente não passa a depender de N apps para subir. ATENÇÃO ao que isto concede: o script roda na origem do shell, com a confiança do shell. O portão é o mesmo do resto do modelo — quem pode rodar vssh-app-install.

contributes.settingsOptIn

boolean, opcional, padrão false.

A seção que settings traz só aparece em Configurações depois que a pessoa a liga de dentro do app, por vssh.configuracoes.ligar(true); vssh.configuracoes.ligada() diz o estado, e vssh.configuracoes.abrir() abre a seção. É para o app cuja seção é exemplo ou ferramenta de quem escreve apps, e não preferência de quem o usa: os dois templates Hello World declaram. O app continua no menu iniciar, no Launchpad e em "Abrir com" como qualquer outro; só a seção espera o gesto. A escolha fica em secoesDeApps, por usuário, e acompanha a pessoa. Sem settings, não faz nada.

contributes.contextMenu

array de object, opcional, itens no máximo 8.

Itens que este app põe no menu de contexto do AMBIENTE — clique direito num arquivo, numa pasta, na área de trabalho. É DADO, e não um caminho de script como settings, e a razão é o momento: Configurações abre porque alguém pediu e pode demorar; o menu de contexto abre no clique direito e não pode esperar fetch nenhum. O app DECLARA; quem monta o item, decide a ordem e executa é o shell — um app não roda uma linha na origem do shell para pôr um item de menu. Item inválido é OMITIDO em silêncio, e os irmãos válidos continuam: um item torto não pode impedir o menu de abrir. Teto de 8 por app (o teto da SOMA, entre todos os apps, é do shell).

contributes.contextMenu[].id

string, obrigatório.

Estável e único DENTRO deste app. Serve de desempate quando dois itens pedem a mesma ordem.

contributes.contextMenu[].superficie

string, obrigatório, valores "arquivo", "pasta", "area-de-trabalho", "icone-do-app".

Onde o item aparece. icone-do-app é a JUMP LIST: o clique direito no ícone do app, no Launchpad e no Menu Iniciar — uma superfície só para os dois lugares, senão todo app teria de declarar o mesmo item duas vezes. Ela exige rota e recusa quando; as três de caminho exigem o contrário.

contributes.contextMenu[].rotulo

string, obrigatório, comprimento mínimo 1, comprimento máximo 48.

O texto do item. Nome de PRODUTO, na voz do ambiente ('Abrir no VSSHCode'), não jargão de manifesto. O teto de 48 não é segurança: é a largura do painel — um rótulo maior faz um menu que não cabe na tela.

contributes.contextMenu[].ordem

integer, opcional, padrão 100, mínimo 0, máximo 999.

A POSIÇÃO no bloco de abertura, medida contra os itens do próprio shell — que é o que faz um item contribuído poder ficar ANTES de um embutido, em vez de sempre no fim atrás de um separador. A régua publicada: Abrir/Nova Pasta 10, Novo Arquivo 15, Abrir Terminal Aqui 20, Editor de Texto / Abrir em Arquivos 25, VS Code 30, Extrair aqui 35, Fixar na barra lateral 40, Abrir com 60, Baixar 70, Imprimir 80. O default 100 põe o item depois de todo embutido de abertura, que é onde um app novo pertence enquanto ninguém disser o contrário. Valor fora da faixa RECUSA o item, e não é aparado para o teto: quem pediu 10^9 estava pedindo para ficar sempre em último por um mecanismo que não foi escolhido.

contributes.contextMenu[].quando

object, opcional, sem campo além dos listados.

Restringe onde o item aparece. Só vale para superficie: "arquivo" — numa pasta ou na área de trabalho não há extensão, e um filtro que nunca casa é um item declarado, aceito e invisível (por isso ele é RECUSADO ali, em vez de ignorado).

contributes.contextMenu[].quando.extensoes

array de string, opcional, itens no mínimo 1.

Com ou sem ponto; normalizado pelo portal. Ausente = todo arquivo. É independente de opens.extensions: aquele campo diz o que o app sabe abrir (e o põe no submenu 'Abrir com'); este diz onde ele quer um item PRÓPRIO, de primeiro nível.

contributes.contextMenu[].acao

string, opcional, valores "abrir", "abrirRota".

O verbo, e ele é o da SUPERFÍCIE — não uma escolha livre. abrir (nas três superfícies de caminho) abre este app com o caminho clicado, pelo open-context que já é contrato publicado. abrirRota (só no icone-do-app) abre o app num lugar DELE, declarado em rota. Omitir o campo é aceitar o verbo da superfície; declarar o OUTRO recusa o item, porque um app que pediu outra coisa não quis esta — um abrir no ícone seria o item "Abrir" que aquele menu já tem, fixo, e um abrirRota num arquivo teria de decidir o que fazer com o caminho clicado.

contributes.contextMenu[].rota

string, opcional, comprimento mínimo 1, comprimento máximo 512.

O lugar DENTRO do app, colado depois da URL que o portal já resolveu para ele — novo, docs/recentes?x=1. Obrigatório com superficie: "icone-do-app", e proibido nas outras. NÃO é URL: esquema (https:, javascript:), caminho absoluto e .. são recusados pelo ambiente, com a mesma regra do vssh.window.abrir — sem ela uma rota viraria uma janela do shell servindo outra coisa, com o título e o ícone deste app. Com o app já aberto a rota chega pelo open-context (campo rota), porque quem sabe navegar é o app.

contributes.browserExtension

object, opcional, sem campo além dos listados.

A extensão do navegador embutido que é o PAR deste app — a que ele precisa do outro lado para funcionar inteiro (capturar de uma página, integrar com um site). Sem isto, um par app+extensão não tem como se declarar: o app é instalado por um admin com vssh-app-install, a extensão é instalada pelo usuário em vsshb://extensions, são catálogos diferentes e nada casa as versões — quem instala o app não descobre que falta metade. É DADO, como o contextMenu ao lado, e não um caminho de script: o app declara, e quem oferece é o ambiente. Declarar OFERECE; instalar continua sendo escolha do usuário, porque instalar uma extensão remota executa o bundle dela no contexto do portal, e isso não é decisão do app nem do admin.

contributes.browserExtension.id

string, obrigatório.

O id da extensão no catálogo de extensões (o mesmo id do vssh-ext.json dela). Só o id: a URL do bundle e a versão saem do catálogo no momento de oferecer, e repeti-las aqui seria uma segunda verdade sobre o mesmo fato — que envelhece sozinha a cada release da extensão.

contributes.browserExtension.razao

string, opcional, comprimento mínimo 1, comprimento máximo 120.

Uma frase dizendo para que o app precisa dela, mostrada a quem for decidir. Sem isto o convite diz só o nome da extensão, e quem nunca ouviu falar dela não tem como responder.

requiredPackages

array de string, opcional.

Pacotes Linux de que o app precisa para funcionar (ffmpeg, pandoc, rclone…). Declarar é o ponto: hoje uma dependência de sistema se esconde num installCommand opaco, e não há como responder 'este app roda neste servidor?' sem executá-lo. Cada item é uma EXIGÊNCIA, e uma exigência pode ter alternativas separadas por '|' — 'chromium | google-chrome-stable' quer dizer que qualquer um dos dois serve. Quem VERIFICA é o portal (vssh-app-install recusa antes de instalar, e o painel admin mostra o que falta por servidor).

motores

array de object, opcional.

Artefatos grandes que o app usa e que não viajam no pacote: o servidor web de um editor, um ambiente Python com PyTorch, os pesos de um modelo. Cada motor é um tarball publicado à parte (no vssh-repo, pelo publicar-motor do vssh-sdk), e o servidor o baixa UMA vez, na fase root do vssh-app-install, para /var/lib/vssh-motores/<app>/<nome>/<versao>, fora do diretório do app: reinstalar o app na mesma versão do motor não baixa nada, e o rsync do instalador não o apaga. O sha256 é conferido antes de extrair, a troca é em dois passos (um download interrompido nunca parece instalado), e as versões que o manifesto deixou de declarar saem depois que a nova entra. Ao subir, o vssh-app-run exporta o caminho de cada motor em VSSH_MOTOR_<NOME> (maiúsculas, - vira _); um motor declarado sem quando que não está no disco recusa a subida, e o run.log diz o que pedir ao administrador.

motores[].nome

string, obrigatório.

O nome do motor dentro do app, que dá nome à variável: runtime vira VSSH_MOTOR_RUNTIME. Minúsculas, porque ele também é um diretório no servidor.

motores[].versao

string, obrigatório.

A versão do artefato. É ela que decide se o servidor baixa: a mesma versão já instalada não baixa de novo, e uma versão nova baixa e toma o lugar da antiga.

motores[].url

string, obrigatório.

De onde o servidor baixa o tarball, sem token: a rota /v1/motor/<nome>/<versão> do vssh-repo. O tarball tem um diretório na raiz, que a extração descarta.

motores[].sha256

string, obrigatório.

O sha256 do tarball, conferido no servidor antes de extrair. Um tarball trocado no caminho não chega a escrever arquivo nenhum.

motores[].tamanho

integer, obrigatório, mínimo 1.

O tamanho do tarball em bytes. O instalador confere o espaço livre antes de baixar, e a loja o mostra antes de instalar.

motores[].confere

string, opcional.

Um caminho relativo que o tarball extraído precisa trazer (bin/python, bin/vssh-code-server). Sem ele, a extração é recusada: a URL passou a servir outra coisa.

motores[].quando

string, opcional, valores "gpu".

A condição para o servidor baixar o motor. Com gpu, o vssh-app-install só o baixa num servidor com uma placa física (o vssh-gpu-info responde por qualquer fabricante; uma placa virtual não conta), e num servidor sem placa ele não desce e uma versão que já estava no disco sai. O app sobe sem o motor, sem VSSH_MOTOR_<NOME>, e decide o que fazer sem ele (o Escriba transcreve pela fila do cluster). Sem o campo, o motor desce em todo servidor.

instalacao

array de string, opcional, cada item um de "sistema", "pessoa".

Onde o app se instala. sistema é /opt/vssh-apps/<id>, pelas mãos de um administrador, para todas as pessoas do servidor. pessoa é ~/.local/share/vssh-apps/<id> de quem instala pela Loja sem ser administrador, só para essa pessoa. Sem o campo, vale ["sistema"]. Declarar pessoa é dizer que o app roda da HOME: o installCommand dele não precisa de root, e nada nele procura o próprio código em /opt/vssh-apps. Um app com motores só se instala no sistema, declare o que declarar, porque o motor mora numa pasta de root.

resources

object, opcional, sem campo além dos listados.

Limites de recurso do processo do app. O ambiente já aplica um teto PADRÃO a todo vssh-app (memória e número de tarefas) — este bloco existe para o app que precisa de outro, para mais ou para menos. Aplicado por systemd-run --user --scope no vssh-app-run, sobre o GRUPO de processos do app, então alcança os filhos que ele gerar. O que ele contém é UM app desgovernado, e não a soma deles: dois apps no teto ainda somam mais que a máquina.

resources.memoryHigh

string, opcional.

Onde o kernel começa a PRESSIONAR o app a devolver memória (systemd MemoryHigh). Não mata: reclama. É o limite que se quer ajustar primeiro, porque errar para baixo deixa o app lento, não morto. Aceita '2G', '70%' (do total físico) ou 'none' para não ter.

resources.memoryMax

string, opcional.

Teto duro (systemd MemoryMax): passar daqui é OOM kill do app — e é justamente o que impede que o OOM killer escolha a sessão inteira do usuário no lugar. Aceita '4G', '85%' ou 'none'.

resources.cpuQuota

string, opcional.

Teto de CPU (systemd CPUQuota), '100%' = um núcleo inteiro. Sem padrão do ambiente de propósito: CPU disputada deixa a sessão LENTA, e o escalonador já reparte; memória esgotada a derruba. Declare quando o app tiver de conviver, não para 'ser bem-comportado'.

resources.tasksMax

string, opcional.

Teto de processos E THREADS do app (systemd TasksMax). Conta thread, então um app com pool de workers precisa de folga — e é por isso que o padrão do ambiente é generoso. Aceita '512', '25%' (de kernel.pid_max) ou 'none'.

recursos

object, opcional, sem campo além dos listados.

O que o app pede ao sistema além dos tetos de resources, e que o sistema arbitra ao subir o app. O app declara o que precisa e pergunta o que recebeu por vssh.gpu (concedida() no backend, estado() na janela); quem decide, com o que o servidor tem, é o vssh-app-run. O app não escolhe como o recurso chega, e é isso que o poupa de conhecer CUDA_VISIBLE_DEVICES e o limits.json.

recursos.gpu

object, opcional, sem campo além dos listados.

A GPU do servidor. Declarar faz duas coisas. O ambiente consulta o kernel (/sys/class/drm e /dev/dri) e registra o que há: fabricante (NVIDIA, AMD, Intel, virtio ou uma placa virtual), driver, se é virtual, e se o processo do usuário abre o render node; um usuário fora do grupo render é o modo de falha mais comum, mais que a ausência de placa. E o app deixa de receber CUDA_VISIBLE_DEVICES="", que todo app sem esta declaração recebe para o runtime CUDA não enumerar dispositivo nenhum; é o que deixa um app de inferência conviver com os vizinhos. A arbitragem é por convenção: a variável não fecha /dev/dri, e a fronteira de verdade pediria controle de dispositivo no cgroup (eBPF, root). O que o sistema decidiu chega ao app por vssh.gpu.concedida() como { concedida, dispositivos, motivo }, e o motivo de uma negativa fica no run.log e no gerenciador de tarefas.

recursos.gpu.modo

um dos valores listados, obrigatório, valores "opcional", "necessaria".

opcional: o app sobe com ou sem GPU e pergunta o que recebeu; sem placa ele costuma ficar mais lento, e quem sabe se dá para seguir em CPU é o app. necessaria: sem GPU utilizável o app não sobe; o run.log nomeia o motivo, e o supervisor o mostra como falha de start, sem laço de reinício, até alguém pedir a subida de novo. Declare necessaria só para um app que não tem caminho em CPU: um servidor sem placa, ou um usuário fora do grupo render, deixa esse app parado em vez de lento.

recursos.fila

boolean, opcional, padrão false.

A fila de processamento: o app delega trabalho pesado (um container com imagem, comando e arquivos) ao cluster, num Job do Kubernetes no sítio em que a placa pedida cabe, em vez de rodá-lo na GPU da estação. A imagem precisa de bash e de curl ou python3, que levam as entradas e as saídas, e o comando vai inteiro, porque o ENTRYPOINT dela não roda. Declarar faz o portal escrever VSSH_PORTAL_URL e VSSH_PORTAL_TOKEN no ambiente do app a cada subida; com eles o backend chama vssh.fila (submeter, acompanhar, baixar, cancelar). O token é a identidade do app diante do portal, alcança /api/fila/* (e /api/salas/*, se o app também declarar recursos.salas), e morre com o app (stop o revoga). Sem esta declaração o app não recebe o par, e vssh.fila.disponivel() diz por quê. Um servidor com a fila desligada responde o mesmo: o app pergunta antes de prometer.

recursos.salas

boolean, opcional, padrão false.

As salas de edição: várias pessoas no mesmo documento Yjs, com o portal de relé. A página do app abre uma sala pelo bilhete que a sessão da pessoa pede, e isso não depende desta declaração. Declarar faz o portal escrever VSSH_PORTAL_URL e VSSH_PORTAL_TOKEN no ambiente do app, com o escopo das salas: o backend pede o bilhete como a pessoa dona da credencial e entra nas salas DESTE app, para gravar no arquivo dela o que as outras pessoas escreveram. A credencial não abre a sala de outro app.

recursos.metricas

boolean, opcional, padrão false.

As métricas e os erros do backend, publicados no ambiente. Declarar faz o portal escrever VSSH_PORTAL_URL e VSSH_PORTAL_TOKEN no ambiente do app, com o escopo das métricas, e com eles o backend chama vssh.metricas: contar soma a um contador (uma transcrição, os segundos de áudio processados), duracao registra quanto algo levou, e relatar_erro (relatarErro em Node) manda uma exceção ao log do ambiente, ao lado dos erros do portal. O que o app publica sai no /metrics do portal como vssh_app_events_total e vssh_app_event_duration_seconds, com o id do app, o nome do evento e até cinco rótulos que o app escolhe. Um rótulo diz de que tipo é o evento (o motor, a língua, onde rodou), e nunca quem o causou: o portal recusa o que passa de 300 séries por app. Sem esta declaração o app não recebe o par, e vssh.metricas.disponivel() diz por quê; publicar sem o par não faz nada.

gpu

boolean, opcional, padrão false.

A grafia anterior de recursos.gpu.modo: "opcional", lida pelos mesmos consumidores (o vssh-app-run e a projeção de GET /api/apps) e com o mesmo efeito. Escreva a nova. Esta fica aceita enquanto houver app publicado que a declare, e recursos.gpu.modo manda quando os dois aparecem no mesmo manifesto.

secrets

array de object, opcional.

Credenciais que o app precisa receber pelo ambiente (chave de API, senha de banco, token de S3). Declarar é o ponto: sem isto, cada app inventa o próprio jeito — normalmente um arquivo em texto plano no VSSH_APP_DATA_DIR — e o usuário não tem onde colocar a credencial. O valor NUNCA vem do manifesto: quem o guarda é o usuário, pelo shell, e ele mora em ~/.vssh-apps/<id>/secrets.json (modo 0600) no servidor do próprio usuário. O portal escreve e esquece — não há coluna de segredo no portal. O app lê como variável de ambiente comum.

secrets[].name

string, obrigatório.

Nome da variável de ambiente. Maiúsculas por convenção e por segurança: este nome vira um export <nome>= avaliado pelo shell no servidor.

secrets[].description

string, opcional.

O que é, e onde consegui-la. Aparece na tela onde o usuário digita — e é a diferença entre um campo que ele preenche e um campo que ele ignora.

secrets[].required

boolean, opcional, padrão false.

Sem este segredo o app não funciona. Informativo: o ambiente não recusa subir por falta dele, porque quem sabe degradar é o app.

icon

string, opcional.

Caminho relativo à raiz do pacote (.svg/.png/.jpg).

category

string, opcional.

A seção do Start Menu/Launchpad onde este app aparece — use o nome de categoria do menu freedesktop ('Development', 'Office', 'Graphics', 'Utility', 'System'…) para cair junto do que faz a mesma coisa. Quem não declara cai em 'Other'. O agrupamento é por este campo e nada mais: não há seção de 'apps integrados' que recolha vssh-app por ser vssh-app, porque isso separa por PROCEDÊNCIA o que o usuário procura por FUNÇÃO.

description

string, opcional.

developer

object, opcional, sem campo além dos listados.

Quem fez o software que este app entrega. Para um app escrito para o VSSH é quem o escreveu; para um port (um Recoll, um Xpra) é o projeto original, e é esse nome que a loja mostra em 'por'. O campo é uma declaração do manifesto, e a loja o mostra como tal: quem de fato publicou cada versão é registro do repositório, e sai de lá.

developer.name

string, obrigatório, comprimento mínimo 1.

Como a loja escreve o nome: uma pessoa, um projeto ou uma organização.

developer.url

string, opcional.

Onde encontrar quem assina: o site do projeto, o perfil ou o repositório.

publisher

object, opcional, sem campo além dos listados.

Quem empacotou o software para o VSSH, quando difere de developer: a pessoa ou o grupo que fez o port e responde por ele. Sem este campo a loja entende que quem desenvolveu também empacotou. Com ele, a loja escreve 'portado por' ao lado de 'por'.

publisher.name

string, obrigatório, comprimento mínimo 1.

Como a loja escreve o nome: uma pessoa, um projeto ou uma organização.

publisher.url

string, opcional.

Onde encontrar quem assina: o site do projeto, o perfil ou o repositório.

homepage

string, opcional.

A página do app: documentação, site do projeto ou o repositório. A loja a mostra na ficha do app.

license

string, opcional, comprimento mínimo 1.

A licença do software, como identificador SPDX ('MIT', 'GPL-3.0-or-later', 'Apache-2.0'). Texto livre também passa; o identificador é o que a loja consegue ligar ao texto da licença.

handles

string ou null, opcional, valores "terminal", "editor", "fileBrowser", "ide", "vscode", "browser", null.

Registra o app como substituto de um dos 5 launchers embutidos, pelo PAPEL que ele ocupa. O usuário ainda precisa escolher em Configurações → Ambiente → Abrir com. vscode é o nome antigo de ide e continua aceito (um manifesto já publicado não deixa de instalar por causa de uma palavra que trocamos), mas era o único valor do enum que nomeava um produto em vez de um papel — declare ide.

opens

object, opcional, sem campo além dos listados.

Tipos de arquivo que o app sabe abrir — coloca o app no menu 'Abrir com' e o torna elegível a padrão por tipo. Generaliza handles, que só cobre os 5 launchers.

opens.extensions

array de string, opcional.

Com ou sem ponto; normalizado pelo portal.

opens.mimeTypes

array de string, opcional.

opens.default

boolean, opcional.

Pede o duplo-clique nas extensões de extensions quando outro app instalado também as declara. É para quem mostra o arquivo (a Lupa com foto e PDF, o Palco com vídeo), e não para quem o importa ou organiza (um gerenciador de referências que também lê PDF). Um app sozinho numa extensão é o padrão dela com ou sem este campo; com dois pedindo a mesma, ninguém é eleito e a pessoa escolhe em Configurações > Tipos de arquivo. A escolha gravada pela pessoa vence sempre, e o campo não vale para tipo com destino próprio no ambiente (código, texto, comprimido, HTML).

opens.proprios

array de object, opcional, itens no máximo 32.

Os tipos de arquivo que o app cria, desenhados com a marca dele: a folha do ícone de arquivo leva o ícone do app no meio e o selo na cor dele, e a coluna Tipo do gerenciador de arquivos mostra o nome. Cada extensão daqui também entra em extensions. Uma extensão que o ambiente já desenha (imagem, vídeo, áudio, comprimido, código, texto, documento de escritório e PDF) continua com o ícone do ambiente. Com dois apps declarando a mesma extensão, vale a do app que abre o tipo no duplo-clique.

opens.proprios[].extensao

string, obrigatório.

A extensão, em minúsculas, com ou sem ponto: prelo.

opens.proprios[].nome

string, obrigatório, comprimento mínimo 1, comprimento máximo 60.

Como a coluna Tipo chama o arquivo: Documento do Prelo.

opens.proprios[].cor

string, obrigatório.

A cor do selo, em #rrggbb. O texto do selo é branco, e o vssh-app-publish e a instalação recusam a cor em que o branco fica abaixo de 4,5:1.

opens.proprios[].icone

string, opcional.

O ícone que vai no meio da folha, como caminho relativo à raiz do pacote. Sem ele, vai o icon do app.

opens.proprios[].selo

string, opcional.

O texto curto do selo, até 5 letras maiúsculas ou números: PRELO. Sem ele, a folha fica sem selo, e o ícone do app identifica o arquivo sozinho.

opens.urls

array de string, opcional.

HOSTS cujos links este app sabe abrir — é o que faz um link do YouTube cair no seu player em vez de virar aba do navegador. Só o host, sem esquema, sem porta e sem caminho: discriminar por porta ou por rota seria regra de roteamento, e quem roteia dentro do app é o app. youtube.com casa o host EXATO; *.youtube.com casa também os subdomínios (m., www., music.) — a mesma semântica dos padrões de userscript, e ⚠ NÃO um sufixo: evilyoutube.com não casa com youtube.com, embora termine com ele. O padrão recusa, de propósito: * e *.com (curinga sobre sufixo público — um app tomaria a internet inteira), IP literal (endereço não é identidade de site) e nome de rótulo único, do qual localhost é o caso que importa — aqui ele é o loopback DO SERVIDOR, e reivindicá-lo sequestraria todo servidor de desenvolvimento do ambiente. ⚠ O que o padrão NÃO consegue recusar é curinga sobre sufixo público de mais de um rótulo (*.co.uk), que exige a Public Suffix List; essa recusa é do portal. Declarar um host não elege ninguém: com dois apps declarando o mesmo host, o usuário decide em Configurações → Sites e, até lá, o link segue para o navegador.

backend

object, obrigatório, sem campo além dos listados.

backend.runtime

um dos valores listados, obrigatório, valores "python3", "node", "binary".

Prefira um runtime que o próprio mecanismo já exija (python3): um nvm/pyenv pessoal não aparece no PATH de um exec não-interativo por SSH.

backend.entrypoint

string, obrigatório.

Relativo à raiz do pacote. Deve bindar no socket unix de $VSSH_APP_SOCKET — o único endereço que existe, e é o que o transport logo abaixo diz, com um enum de um valor só. Com as libs do toolkit isso é uma linha: escutar(server) no Node, criar_servidor() no Python. Um app que binde uma porta em 127.0.0.1 não é proxiado por ninguém: healthcheck 000, janela em branco, e nada no log dizendo por quê.

backend.transport

um dos valores listados, opcional, padrão "socket", valores "socket".

Onde o backend escuta. Hoje há um valor só: 'socket' — um socket unix em $VSSH_APP_SOCKET, dentro de ~/.vssh-apps/<id>/, que já é 0700. A permissão de arquivo faz o que a conferência de X-Vssh-App-Token só promete, e o app deixa de ter porta alcançável por qualquer outra conta Linux da máquina (medido: 14 de 23 portas de app responderam a um GET sem token, 12 delas de contas alheias). ⚠ 'tcp' não existe mais, e o campo continua aqui só para que um manifesto que ainda o declare receba um erro de validação que NOMEIA o problema, em vez de instalar e nunca responder. Nenhum app fala TCP, e a própria lib do toolkit não sabe bindá-lo: escutar() só abre socket, e trata um VSSH_APP_PORT sozinho como servidor desatualizado.

backend.aoFechar

string, opcional, padrão "encerrar".

O que acontece com o backend quando a ULTIMA janela deste app fecha, ou quando ninguem mais o usa. 'encerrar' (padrao): o ambiente chama o stop dois minutos depois, e uma janela do app aberta nesse intervalo o cancela e reaproveita o backend de pe. 'manter': o backend fica, porque quem o mantem vivo e o trabalho e nao a janela, como o dono de sessao de terceiro. 'ocioso:<N>m' (de 1 a 9999 minutos): o backend fica enquanto alguem o usa, e o supervisor do servidor o encerra depois de N minutos sem conexao nenhuma no socket dele, venha ela do portal ou do cliente de desktop; e o valor de um motor sob demanda, que nao tem janela. O proxy do portal segura uma conexao parada por ate 10 minutos, e ela conta como uso. O padrao e 'encerrar' porque um padrao que vaza memoria tem de ser o que se escolhe: sem contrato, um app aberto uma vez seguiria ocupando RAM em toda sessao daquela conta, sem janela na tela. kind:'service' resolve 'encerrar' e 'manter' para 'manter', porque um daemon nao morre com uma janela, e respeita 'ocioso:<N>m'.

backend.installCommand

string, opcional.

Roda DUAS vezes: uma como root no install, outra por usuário no primeiro run. Escreva de forma idempotente.

backend.healthcheckPath

string, opcional, padrão "/".

Pollado direto no endereço do app pelo lifecycle (socket unix, via curl --unix-socket), até 15x/1s. A sondagem vai COM o header X-Vssh-App-Token, então gatear esta rota é permitido — e isentá-la do seu gate seria abrir uma rota sem motivo. NÃO contam como pronto: 000, 5xx e 401/403. Um 404 CONTA — o servidor respondeu —, então confira o caminho.

window

object, opcional, sem campo além dos listados.

A janela em que o app abre, no vocabulário das janelas do próprio ambiente. Ignorado por type:'engine'. O portal repassa este objeto inteiro ao cliente (window: m.window || {} em routes/apps.ts), e o shell lê só os campos abaixo, então uma chave a mais viaja o caminho todo sem que ninguém a leia. O objeto é fechado por isso: widht: 900 deixaria a janela no tamanho padrão sem erro em lugar nenhum, e o schema recusa a chave antes de o app publicar.

window.title

string, opcional.

window.width

integer, opcional, mínimo 200, máximo 10000.

window.height

integer, opcional, mínimo 150, máximo 10000.

window.richChrome

boolean, opcional.

Tabbar de verdade no cabeçalho + itens de aba no menu de contexto.

window.multiplasJanelas

boolean, opcional, padrão false.

Cada pedido de abrir cria uma janela NOVA, em vez de focar a que já existe. O padrão (false) é o certo para quem tem abas ou lista própria — abrir um segundo .md no editor não deve abrir um segundo editor. Declare true quando cada coisa aberta é uma sessão inteira e independente: um player, um visualizador de imagem, um terminal. ⚠ O backend continua sendo UM. Duas janelas do mesmo app são duas visões do mesmo processo, como duas abas do navegador no mesmo servidor — não há porta, token nem VSSH_APP_DATA_DIR separados, e um app que declara isto precisa aguentar dois open-context sem que o segundo apague o estado do primeiro. Vale para o "Abrir com" do gerenciador de arquivos e para o ícone no menu iniciar e no launchpad; NÃO vale para clique em notificação, que sempre foca a janela que a produziu.

window.cabecalho

um dos valores listados, opcional, padrão "ambiente", valores "ambiente", "app".

Quem desenha a barra de título. ambiente (padrão) é o cabeçalho do desktop. app é para quem JÁ TEM uma barra de título própria e boa — o editor, cuja barra é a do VS Code: o shell não monta a dele, e o app põe os botões de janela na sua, chamando vssh.window.minimize/maximize/close. Moldura, sombra e alças de resize continuam sendo do ambiente. Quem escolhe app fica responsável por três gestos que o cabeçalho padrão dava de graça: arrastar (vssh.window.arrastar), duplo-clique (alternarMaximizado) e menu de contexto (menuDoCabecalho) — um app que declara isto e não os liga entrega uma janela que não se move.

window.minimos

object, opcional, sem campo além dos listados.

O piso do redimensionamento pelas alças, para o app que precisa de mais que os 400×300 que o ambiente impõe a toda janela. Um lado só também vale: { "largura": 640 } deixa a altura no piso do ambiente. Maximizar e encaixar continuam alcançando qualquer tamanho, e o piso vale para toda janela do app, inclusive a que ele abre pelo verbo janela.abrir. O piso é da tela larga: abaixo de 600 px de tela o ambiente põe a janela na área útil inteira e o piso cede, então o app precisa caber em 320 px de largura, que é o que a moldura do Tuff (.tuff-app) dá sem CSS próprio.

window.minimos.largura

integer, opcional, mínimo 400, máximo 10000.

window.minimos.altura

integer, opcional, mínimo 300, máximo 10000.

window.posicao

um dos valores listados, opcional, padrão "cascata", valores "cascata", "centrada".

Onde a janela nasce. cascata (padrão) é a janela de trabalho: cada uma abre um pouco abaixo e à direita da anterior, a partir do canto do display em que está o ponteiro. centrada é o diálogo e o painel: no meio desse display. Uma janela restaurada de uma sessão anterior volta onde estava, e este campo não a alcança.

window.cancelarArraste

array de string, opcional.

Seletores CSS que não iniciam o arraste da janela, somados à lista do ambiente (botões, campos de formulário). Só alcançam o que o shell desenha em volta do iframe, como a barra de abas de quem tem richChrome: o ponteiro dentro do documento do app nunca chega ao shell. O shell descarta um seletor que o navegador não aceita.

window.icone

object, opcional, sem campo além dos listados.

O ícone da janela (barra de título, barra de tarefas, Alt+Tab), quando ele não é o icon do app. glifo é um desenho do próprio ambiente, pintado na cor de destaque que a pessoa escolheu, para o app parecer nativo; url é um caminho dentro do app, relativo à raiz dele, que o backend do app serve. Declare um dos dois; glifo vence quando há os dois, e o shell descarta um caminho que sai do app.

window.icone.glifo

string, opcional, valores "pasta", "globo".

window.icone.url

string, opcional, comprimento mínimo 1.