Skip to content
← Public packages

@kody/alexa

Alexa custom skill webhook into Kody. Checks the skill id, maps common requests, and leaves utterance forwarding unwired.

AGENTS.md

87 lines · 4.5 KB · Markdown
Raw

@kody/alexa

Agent notes for the head-start Alexa skill stub. Human setup lives in README.md.

Alexa calls the minted skill webhook. This package answers with Alexa response JSON. It does not propagate the utterance. alexaForwardUrl is only detected so speech can say the secret is set.

Imports

import skillRequest from 'kody:@kody/alexa/skill-request'
import verifyAlexaRequest from 'kody:@kody/alexa/verify'

kody:@kody/alexa is the same function as ./skill-request.

Direct calls accept only body, dryRun, and allowUnsigned. Unknown keys throw. body is the Alexa request object or a JSON string. Alexa itself sends many extra fields; those belong inside body, not beside it.

The minted webhook calls the same export with { webhook, request } (inputMode: request). Do not reject that envelope for unknown platform keys.

Platform calls that target @kody/alexa need package_scope: "kody".

Smoke tests

Prefer local CLI execute after publish. Pass dryRun: true so the call does not pretend to enforce the skill id.

kody execute --local --invoke kody:@kody/alexa/skill-request --params '{"body":{"version":"1.0","request":{"type":"LaunchRequest"}},"dryRun":true}'
kody execute --local --invoke kody:@kody/alexa/verify --params '{"dryRun":true}'
import skillRequest from 'kody:@kody/alexa/skill-request'
import verifyAlexaRequest from 'kody:@kody/alexa/verify'

const launch = await skillRequest({
	body: { version: '1.0', request: { type: 'LaunchRequest' } },
	dryRun: true,
})

const intent = await skillRequest({
	body: {
		version: '1.0',
		request: {
			type: 'IntentRequest',
			intent: {
				name: 'Ask',
				slots: { query: { name: 'query', value: 'hello from the kitchen' } },
			},
		},
	},
	dryRun: true,
})

const signature = await verifyAlexaRequest({ dryRun: true })

Expect launch.response.outputSpeech.text to include Dry run and not wired yet. Expect intent to include hello from the kitchen. Expect signature.verified === false and signature.implemented === false.

A second smoke with an unknown key must fail:

await skillRequest({ body: { version: '1.0', request: { type: 'LaunchRequest' } }, dryRun: true, extra: true })

Webhook mint

Declaring kody.webhooks[name=skill] does not open ingress. After publish:

  1. webhookUrlMint with kodyId: "@kody/alexa", package_scope: "kody", webhookName: "skill" (or packageId if the scoped name is not accepted).
  2. The owner copies the URL from package settings, Webhooks section. MCP does not return the credential.
  3. Paste that URL into the Alexa skill HTTPS endpoint. responseMode is sync, so the HTTP body is this export's return value. Keep that value a bare Alexa response. Do not wrap it in { ok: true }.
  4. Optional synthetic check: webhookSyntheticDispatch with request.json set to a minimal LaunchRequest. Side effects are only spoken stub text. Do not log or repeat the webhook URL.

Edge cases

  • Signature verification is not implemented. ./verify always returns skipped: true and unsafeForProduction: true. dryRun or allowUnsigned is a loud dev bypass, not a real check.
  • Kody's webhook header allowlist does not forward Signature or SignatureCertChainUrl, so the live webhook cannot see the material Amazon signs with. Do not claim in-handler verification works until those headers are available or the platform verifies them.
  • alexaSkillApplicationId is a user secret name. Unresolved placeholders count as unset. Live calls (no dryRun, no allowUnsigned) with the secret set must match session.application.applicationId or context.System.application.applicationId. Mismatch speaks a refusal and sets sessionAttributes.applicationIdCheck to mismatch. Do not include the secret value in speech or logs.
  • dryRun: true sets applicationIdCheck to skipped-dry-run and prefixes speech with Dry run.
  • alexaForwardUrl is detected only so speech can say the secret is set. This package must not fetch it. Propagation from Kody is a later step, not this stub.
  • Supported request.type values are LaunchRequest, IntentRequest, and SessionEndedRequest. Anything else gets a spoken unsupported-type reply and ends the session.
  • SessionEndedRequest returns response: { shouldEndSession: true } and no output speech.
  • Launch and intent replies keep the session open and include a reprompt, because Alexa expects one when shouldEndSession is false.
  • .kody/icon.png is the official Alexa Smile wordmark already committed. Do not replace or redraw it.