Give a tool a secret
A tool that needs an API key or a signing key declares it by name. Kindgi resolves it on every call, checks it, and hands it to the handler in the call's context. The value never appears in your code or your config.
This tool signs a receipt with a key:
import { createHmac } from 'node:crypto';
import { defineTool } from '@kindgi/sdk/define';import type { ToolId } from '@kindgi/sdk/types';import { z } from 'zod';
const defined = defineTool({ id: 'my-pack.sign-receipt' as ToolId, description: 'Signs an order receipt so the customer portal can verify it.', version: '0.1.0', input: z.object({ orderId: z.string(), totalCents: z.number().int() }), output: z.object({ signature: z.string() }), effects: [], mutating: false, needsSpec: { secrets: { RECEIPT_SIGNING_KEY: { type: 'string', minLength: 32 } }, }, handler: async ({ orderId, totalCents }, ctx) => { const key = ctx.secrets?.RECEIPT_SIGNING_KEY; if (key === undefined) throw new Error('RECEIPT_SIGNING_KEY is not set'); const signature = createHmac('sha256', key).update(`${orderId}:${totalCents}`).digest('hex'); return { signature }; },});
if (defined.kind === 'err') { throw new Error(`my-pack.sign-receipt failed to compile: ${defined.error.message}`);}
export default defined.value;import hashlibimport hmac
from pydantic import BaseModel, Field
from kindgi import ToolContext, tool
class Receipt(BaseModel): order_id: str = Field(alias="orderId") total_cents: int = Field(alias="totalCents")
class Signed(BaseModel): signature: str
@tool( id="my-pack.sign-receipt", mutating=False, needs_spec={"secrets": {"RECEIPT_SIGNING_KEY": {"type": "string", "minLength": 32}}},)def sign_receipt(receipt: Receipt, ctx: ToolContext) -> Signed: """Signs an order receipt so the customer portal can verify it.""" key = ctx.secrets["RECEIPT_SIGNING_KEY"].encode() message = f"{receipt.order_id}:{receipt.total_cents}".encode() return Signed(signature=hmac.new(key, message, hashlib.sha256).hexdigest())- The declaration maps each secret's name to a JSON Schema for its value. Every declared secret is required.
- On every call, Kindgi resolves each declared secret for the call's
tenant, in the environment the runtime serves (
KINDGI_ENV; underkindgi dev,local: the pack's env files), and checks it against its schema. If one is missing or doesn't fit, the call fails before the handler runs. - The context holds only what the tool declares.
ctx.secretsis optional in the TypeScript type because a unit test builds its own context.
Store the secret
Section titled “Store the secret”Until the secret exists, calls fail and name it:
"status": "failed", "failureMessage": "handler-error: Tool \"my-pack.sign-receipt\" handler threw: secret-unavailable: tool \"my-pack.sign-receipt\" needs secret \"RECEIPT_SIGNING_KEY\" in env \"local\": No secret \"RECEIPT_SIGNING_KEY\" for env \"local\" in .env, .env.local at /pack",Store a value. kindgi secrets set prompts for it without echoing; here it
reads it from stdin:
openssl rand -hex 32 | tr -d '\n' | pnpm exec kindgi secrets set RECEIPT_SIGNING_KEY --env=local --scope=tenant --from-stdinopenssl rand -hex 32 | tr -d '\n' | npx --yes @kindgi/cli@0.1 secrets set RECEIPT_SIGNING_KEY --env=local --scope=tenant --from-stdin Set RECEIPT_SIGNING_KEY at tenant in local.Under kindgi dev that writes it to the pack's .env.local. The next call
gets it; nothing restarts. Run the tool (here from my-pack.sign, a one-step
flow):
kindgi runs start --flow=my-pack.sign --input='{"orderId":"ord_1001","totalCents":4200}' "status": "completed", … "output": { "signature": "b02d6ae82a21be7754b7c605ae55210fdb2d6c14ca25f03b56c0d62c6f27e595" },A value that doesn't fit the schema fails the call too, without the value in the message:
"failureMessage": "handler-error: Tool \"my-pack.sign-receipt\" handler threw: secret-invalid: secret \"RECEIPT_SIGNING_KEY\" in env \"local\" doesn't match the schema tool \"my-pack.sign-receipt\" declares for it: must NOT have fewer than 32 characters",To replace a value, set it again with --write-mode=add-version; without
it, set refuses a name that exists. Store a secret
covers kindgi secrets.
Test it
Section titled “Test it”A test passes the secret in the context it builds:
import { invokeTool } from '@kindgi/sdk/define';import type { TenantId } from '@kindgi/sdk/types';import { expect, test } from 'vitest';
import signReceipt from './index.js';
test('signs with the key from the context', async () => { const result = await invokeTool( signReceipt, { orderId: 'ord_1001', totalCents: 4200 }, { tenantId: 'test-tenant' as TenantId, abortSignal: new AbortController().signal, secrets: { RECEIPT_SIGNING_KEY: 'k'.repeat(32) }, }, ); expect(result.kind).toBe('ok');});from kindgi import ToolContextfrom tools.receipts import Receipt, sign_receipt
def test_signs_with_the_key_from_the_context(): ctx = ToolContext.for_test(secrets={"RECEIPT_SIGNING_KEY": "k" * 32}) signed = sign_receipt(Receipt(orderId="ord_1001", totalCents=4200), ctx) assert len(signed.signature) == 64Why not the process environment
Section titled “Why not the process environment”The pack service is one process for every tenant your runtime serves, so its
environment can't hold a key that belongs to one of them. Keep
process.env / os.environ for your app's own settings (a service URL, a
database your app owns), and declare them as the pack's environment:
Declare the environment your code reads.
An HTTP tool names its credential in
authorization.secretRef instead, and Kindgi adds it to the request.