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.

Informations de source

Dépôt
BuilderIO/agent-native
Dernière activité de la source
2 octobre 2026 à 23:40
Langue détectée de SKILL.md
anglais
Étoiles
7 065
Forks
640

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
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
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub