Skip to main content

secrets

Declaratively register API keys and service credentials a template needs so they appear on Settings › API keys and in the onboarding checklist. Use before adding any third-party credential or setup UI so API keys, OAuth connections, and scoped configuration use the correct shared primitive.

Informações da origem

Repositório
BuilderIO/agent-native
Última atividade na origem
2 de outubro de 2026 às 23:40
Idioma detectado do SKILL.md
inglês
Estrelas
7.065
Forks
640

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
secrets
description
Declaratively register API keys and service credentials a template needs so they appear on Settings › API keys and in the onboarding checklist. Use before adding any third-party credential or setup UI so API keys, OAuth connections, and scoped configuration use the correct shared primitive.
scope
dev
metadata
{"internal":true}
# Secrets Registry ## Non-negotiable rule Never hardcode credential values. Source, docs, tests, fixtures, prompts, seed data, and generated extension/app content may mention credential **names** such as `OPENAI_API_KEY`, but must not contain real API keys, tokens, webhook URLs, signing secrets, OAuth refresh tokens, or private Builder/customer data. Never write real secrets to non-gitignored files. Put temporary secret material under the root `.tmp/` or another explicitly gitignored path, then delete it when it is no longer needed. Provider secret values are supplied at runtime through the encrypted `app_secrets` vault, `saveCredential` / `resolveCredential`, OAuth, or `${keys.NAME}` substitution. Deployment configuration is reserved for deploy-level secrets and non-provider configuration. Examples must use obvious placeholders such as `<OPENAI_API_KEY>` or `${keys.SLACK_WEBHOOK}`, not real-looking copied values. Provider credentials and provider account identifiers are workspace data. Use standard workspace connections and org/workspace vault scopes; never put them in `.env` or deployment environment variables, and never add a provider-specific action or startup bootstrap just to write a credential for one organization. ## Google OAuth triage | Observation | Meaning | Next action | | --- | --- | --- | | `invalid_grant` from a deliberately fake code | Google accepted the client pair and rejected only the code | Do not rotate credentials; check flow state and callback registration | | `invalid_client` | Google rejected the client id/secret pair | Verify the exact pair and deployment source before rotating | | `redirect_uri_mismatch` | The client, host, callback path, and Google registration disagree | Compare that exact tuple in Google Cloud Console; publish a new deploy if site-scoped or build-time configuration changes | Do not reason about this from memory. The probe checks both contracts: the unqualified `/health/google` endpoint reports the sign-in contract, while `/health/google?client=managed` reports deployment-level managed OAuth. It asks Google directly whether each live `(client_id, redirect_uri)` pair is registered: ```bash pnpm check:google-redirect-uris -- --env all ``` The sign-in health path calls `checkGoogleSignInCredential()`, which prefers the active Better Auth pair and otherwise uses `resolveGoogleSignInCredentials()` from `packages/core/src/server/google-oauth-credentials.ts`. Managed health calls `checkGoogleManagedCredential()`, which resolves `["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"]` through `resolveSecretPair()` in `packages/core/src/server/credential-provider.ts`. App provider handlers use `resolveGoogleProviderCredentialCandidatesWithReader()` with `resolveSecret` under request context. These are separate flows and can intentionally land on different Google clients, so a clean result for one says nothing about the other. Read both contracts before changing anything: ```bash curl -s https://HOST/_agent-native/health/google | jq '{clientId,mismatchedPairs,credentialSource}' curl -s "https://HOST/_agent-native/health/google?client=managed" | jq '{clientId,mismatchedPairs,credentialSource}' ``` Different `clientId` values across those two, or `mismatchedPairs: true`, is a divergence to understand, not damage to undo. Never repair it by rotating a secret: writing a fresh value into whichever namespace the failing flow does not read verifies clean and changes nothing. Do not collapse the namespaces without first confirming which flow uses which client; separate sign-in and managed clients on one host can be deliberate. For prebuilt Netlify deploys, uploaded Functions read site-scoped secrets at runtime, and the health route resolves them per request. The build may receive masked placeholder values. After changing a site-scoped env var, publish a new deploy before verifying live behavior. Call it a rebuild when the changed value is baked into build output or static assets; a runtime-only secret does not need to be baked into the bundle. ## Credential Modeling Preflight Before registering a provider's fields, inspect the workspace/provider connection catalog first. If a reusable connection exists, use its app grant and scoped `resolveWorkspaceConnectionCredential(s)ForApp` path instead of registering a parallel secret. Only classify fields for app-local setup when no reusable connection exists: - **API or service key** - register it as `kind: "api-key"` with the narrowest correct `scope`, a human label, a description, a docs link, and a validator. - **OAuth authorization or refresh token** - use the OAuth token store and register a `kind: "oauth"` entry so the shared UI renders Connect and the runtime owns status, refresh, and reauthorization. - **Deploy- or app-level configuration** - use deployment/runtime configuration, not a per-user secret row. For a non-secret public setting already represented by `AgentNativeConfig`, put the default in `agent-native.config.ts` and use its `AGENT_NATIVE_CONFIG_<PATH>` alias only for a deployment override. Never put a credential or provider key in that public namespace. - **Account, customer, manager, or other non-secret identifier** - store it as scoped connection metadata or app data, not as a masked secret field. `required: true` is for a logical setup requirement. If a provider needs several values, do not automatically create one required checklist item per field; use one composite onboarding step or a registered connection readiness check. Custom setup UI is allowed for provider-specific prerequisites, ordering, or health checks, but it must delegate credential storage and connection state to the shared vault/OAuth/settings surfaces. ## When to use Use this for any external credential your template needs: API keys, service tokens, webhook secrets. It gives you: - A sidebar UI entry for each credential (masked input, rotate, test, delete). - Automatic onboarding-checklist items for `required: true` secrets. - A stable server-side read API (`readAppSecret`) that decrypts values on demand. - Validator hooks for health-checking keys before save and from a Test button. ## When NOT to use - OAuth flows that need to run the full authorization code exchange — use `@agent-native/core/oauth-tokens` directly to save/refresh tokens. The registry can still surface the OAuth connection in the sidebar by registering a secret with `kind: "oauth"` — that just delegates status lookup to oauth-tokens and renders a Connect button, no `app_secrets` row is written. - Purely process-level env vars that are never user-facing (e.g. `NODE_ENV`, deployment flags). Those belong in the onboarding `form` method or the `envKeys` list in `core-routes-plugin`. ## Registering a secret ```ts // server/plugins/register-secrets.ts import { defineNitroPlugin } from "@agent-native/core/server"; import { registerRequiredSecret } from "@agent-native/core/secrets"; export default defineNitroPlugin(() => { registerRequiredSecret({ key: "OPENAI_API_KEY", label: "OpenAI API Key", description: "Used for Whisper transcription of your recordings.", docsUrl: "https://platform.openai.com/api-keys", scope: "user", kind: "api-key", required: true, validator: async (value) => { const res = await fetch("https://api.openai.com/v1/models", { headers: { Authorization: `Bearer ${value}` }, }); return res.ok ? { ok: true } : { ok: false, error: `OpenAI rejected the key (HTTP ${res.status})` }; }, }); }); ``` ### OAuth in the unified UI ```ts registerRequiredSecret({ key: "GOOGLE_CONNECTED", label: "Google account", description: "Grants access to Gmail / Calendar APIs.", scope: "user", kind: "oauth", required: true, oauthProvider: "google", // must match the provider id in oauth-tokens oauthConnectUrl: "/_agent-native/google/auth-url", }); ``` The sidebar shows a Connect button instead of a text input; no `app_secrets` row is written — status is derived from `hasOAuthTokens("google")`. ### Builder.io: organization and personal connections Builder.io has two OAuth grants per caller, stored apart in `server/builder-oauth.ts`: the organization's (`org` scope, shared with every member) and a personal one (`user` scope, used only by its owner: ahead of the org's for a member, after it for an owner or admin). Name the one you mean; never let role pick it: - Connect with `/_agent-native/builder/connect?scope=org|personal`. `org` needs owner/admin, checked at start and again in the callback, and fails rather than landing as a personal grant. `personal` is for members only: owners and admins connect for the organization (`canRoleConnectPersonalBuilder`). - Disconnect with the `manage-builder-connection` action, `{ "disconnect": "org" | "personal" }` (the Settings Builder.io page calls it too). `org` needs owner/admin, checked against the stored member role; `personal` removes only the caller's grant, so they fall back to the org's. Without `disconnect` it reads `grants`, `canConnect`, and `defaultModel` (whether the default model runs on Builder.io and switches or stops once it is gone). Older clients post the same body to `/_agent-native/builder/disconnect`. - `/_agent-native/connection-status/builder` returns `grants` (`{}` none, `null` unreadable), `effective` (`personal` / `org` / `workspace` / `env` / `null`), and `canConnect`. Each grant has `kind`: `oauth`, or `keys` for a key pair saved by account activation or an older connect at that scope. Client code reads them from `useBuilderConnectFlow` and passes `scope` to `flow.start` and `BuilderConnectionMenu`. - "Restrict personal API keys" answers in `isPersonalBuilderGrantAllowed`; a restricted personal grant stays stored, is skipped for requests, and reports `restricted: true`. A scopeless connect keeps the old rule (owner/admin writes the org grant, anyone else a personal one) for older clients only. ## Registered options | Field | Type | Purpose | | ------------------ | --------------------------------------- | ------------------------------------------------------------------------ | | `key` | `string` | Env-var style name (`OPENAI_API_KEY`). Also the storage key. | | `label` | `string` | Human-readable title in the sidebar. | | `description` | `string?` | Subtitle under the label. | | `docsUrl` | `string?` | "Get key" link rendered on the card. | | `scope` | `"user" \| "workspace"` | Per-user or shared across the active org. | | `kind` | `"api-key" \| "oauth"` | Drives UI and storage behavior. | | `required` | `boolean?` | When true, an onboarding step is auto-injected. | | `validator` | `(v) => Promise<boolean \| {ok,error}>` | Runs on save and from the Test button. Never log `v`. | | `oauthProvider` | `string?` (oauth-kind only) | Provider id in `oauth-tokens` that backs this entry. | | `oauthConnectUrl` | `string?` (oauth-kind only) | URL the Connect button points at. | | `usedFor` | `{ appId?, feature, effectWhenRemoved }[]?` | What this app uses the key for. Set `appId` to the app's id; omit it only for every-app uses. | | `managedBy` | `{ id, owner, route }?` | The Settings page that creates and rotates the key. Its deletes then need `?managedBy=<id>`. | ### What a key powers Every registered secret should say what it powers, so API keys shows "Used by {feature}" and remove confirms list what stops. Write `effectWhenRemoved` as the user-visible outcome ("Uses another image provider, or stops if none is set up."), not the mechanism. - A provider key's model use ("Agent", models leaving the picker) is derived from the engine registry. Don't add it to `usedFor`. - Framework-wide uses live in `register-framework-secrets.ts` via `registerSecretUsage()`, which survives a template registering the same key. - Keys an owner flow writes (Builder.io credentials, `S3_*` storage fields, channel tokens, calendar tokens) are mapped in `secrets/managed-keys.ts`. A registration wins over the map: registering a key puts it on API keys unless the registration sets `managedBy`. - Before deleting a key or removing a provider, call `preview-secret-removal` and tell the user the effects. ### Settings › API keys The page reads the `list-api-keys` action; the agent reads the same thing. - `keys`: every saved row the resolver can use for the caller. That is their `user` row and pre-organization `solo:<email>` row, and for owners and admins the organization's `org` and `workspace` rows. Each entry has `scope` (`user` | `org`), `storedScope` (the row), a mask, `usedFor`, `provider`, and `canReplace` / `canDelete` / `canTest`. Members never see organization keys. - `managed`: keys with a `managedBy` owner, read-only, organization ones included without masks. `addable`: registered keys nobody saved yet. - Delete with `delete-api-key { name, scope, storedScope }` after the preview and the user's confirmation. It removes exactly that row, refuses managed and Vault-synced keys, and fails with 404 when nothing was removed. A provider key also takes its endpoint and older names at that row. - Values never go through an action. The page saves through `saveApiKeyValue` (the secrets routes below). To add or replace a key, send the user to the page (`open-settings-page`, page `api-keys`; a `#secrets:KEY` anchor opens Add key with that name, or the provider dialog when KEY is a model provider's key nobody saved). ## Which credential answers first Owners and admins run on the organization's credential; members run on their own. The caller's own row stays the fallback, so an org with no key of its own still runs on an owner's. Every resolver (`resolveSecretDetailed`, `resolveSecretPairs`, `resolveCredential`, `getOwnerApiKey`, the Builder key pair and OAuth reads, `${keys.NAME}`) orders its scopes through `server/credential-read-order.ts` (`readsOrgCredentialFirst`, `orderCredentialScopes`); a new resolver must too. An unreadable role fails the lookup and never reads as "member". Saves default to the organization for owners and admins and ask who can use it: on any form that saves a credential, pair `useCredentialSaveScope()` with `<WhoField>` from `@agent-native/toolkit/app/settings`. Members save personally and see no picker. A registered key keeps its registered scope. Disconnect with `deleteResolvedCredential(key, ctx)` from `@agent-native/core/credentials`. It removes every row of whichever owner answers (the caller's own, or the organization's, including a legacy `workspace` row) and refuses a member's removal of the organization's with a 403. Deleting only the caller's `user` row leaves a shared one answering. A save that clears a value already knows its scope: clear with `deleteCredential(key, { ...ctx, scope })`, which removes that owner's rows only. The resolved owner can be the organization even when an owner chose Personal. ## Reading a secret from an action ```ts import { z } from "zod"; import { defineAction } from "@agent-native/core/action"; import { readAppSecret } from "@agent-native/core/secrets"; import { getRequestUserEmail } from "@agent-native/core/server"; export default defineAction({ description: "Transcribe an audio file with Whisper", schema: z.object({ fileUrl: z.string() }), run: async ({ fileUrl }) => { const email = await getRequestUserEmail(); if (!email) throw new Error("Not signed in"); const stored = await readAppSecret({ key: "OPENAI_API_KEY", scope: "user", scopeId: email, }); const apiKey = stored?.value; if (!apiKey) { throw new Error( "OPENAI_API_KEY is not set. Add it in Settings.", ); } // …call OpenAI. NEVER log the key or include it in error messages. }, }); ``` Rules: - **Never log the value.** The read layer enforces this server-side; your code must do the same. - **Use env vars only for deploy-level secrets.** If a credential is user-scoped, org-scoped, or workspace-scoped, read the scoped vault/credential store. Do not add a `process.env` fallback that makes every user inherit one deployment's key. - **Scope matches the registration.** `scope: "user"` → pass the user email. `scope: "workspace"` → pass the active `orgId` from
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub