diff --git a/docs/guides/integration-bootstrap.md b/docs/guides/integration-bootstrap.md index c6aa599c7e..982e9b948c 100644 --- a/docs/guides/integration-bootstrap.md +++ b/docs/guides/integration-bootstrap.md @@ -8,6 +8,10 @@ This guide is about **ordering**. The goal is to finish the integration setup and prove it works **before** you save or present downstream packages or package apps that depend on it. +Agents should use this guide with `search` results for saved integrations, +secret references, and capability details before exploring local repository +source for package-app patterns. + ## What counts as an integration bootstrap Use this workflow when the requested result depends on any of the following: diff --git a/docs/use/first-steps.md b/docs/use/first-steps.md index 2477565c6f..61563934b2 100644 --- a/docs/use/first-steps.md +++ b/docs/use/first-steps.md @@ -22,6 +22,11 @@ integration, value, or secret reference, then run work through **execute**. alias, `job_run_now` can trigger an existing job immediately for debugging or catch-up runs, and `job_update` / `job_delete` let you correct or remove an existing scheduled job by id. +- **Bootstrap integration-backed work before building.** When a package, package + app, or workflow depends on OAuth, a saved secret, or a third-party API, use + `search` and the `kody_official_guide` `integration_bootstrap` guide first. + Confirm the integration or secret exists, run a cheap authenticated smoke test + in `execute`, then build the downstream artifact. - **Ask for natural-language goals**, for example: “Search Kody for GitHub pull request automation” or “Find a saved package for Cloudflare DNS helpers.” - **Do not paste secrets in chat.** Use saved secrets, generated UI, or the diff --git a/docs/use/packages.md b/docs/use/packages.md index bee6c0af47..864a4f299b 100644 --- a/docs/use/packages.md +++ b/docs/use/packages.md @@ -225,6 +225,12 @@ Use the package app model when the package needs: - hosted callback URLs - package-owned backend behavior +When a package app depends on OAuth, saved secrets, or a third-party API, run +the integration bootstrap first: use `search` for the saved integration or +secret reference, load `kody_official_guide` with +`guide: "integration_bootstrap"`, and complete a minimal authenticated `execute` +smoke test before treating the app as ready. + Treat package apps like Worker-style modules: - app code lives in the package repo diff --git a/docs/use/search.md b/docs/use/search.md index 8e0c3b2c0b..d9c687bb25 100644 --- a/docs/use/search.md +++ b/docs/use/search.md @@ -89,3 +89,9 @@ Use **search** as the default way to discover whether an integration or secret already exists before switching to **execute**. Runtime code inside **execute** can call **`codemode.secret_list(...)`** when it needs secret metadata, but **search** is the primary discovery path. + +For integration-backed packages, package apps, or workflows, pair that discovery +with the official `integration_bootstrap` guide. Inspect the relevant +`integration` or `secret` entity, run one cheap authenticated **execute** smoke +test, then build the downstream artifact. If setup is missing, load the official +OAuth or secret-backed setup guide that matches the auth path. diff --git a/packages/worker/src/mcp/capabilities/capability-search.workers.test.ts b/packages/worker/src/mcp/capabilities/capability-search.workers.test.ts index d65f5451ac..f504933445 100644 --- a/packages/worker/src/mcp/capabilities/capability-search.workers.test.ts +++ b/packages/worker/src/mcp/capabilities/capability-search.workers.test.ts @@ -61,15 +61,17 @@ test('offline search returns provided specs without depending on global ranks', name: 'kody_official_guide', domain: 'coding', description: - 'Load official Kody guides: integration_bootstrap, secret_backed_integration, oauth (/connect/oauth), generated_ui_oauth (hosted package app), connect_secret.', + 'Load official Kody guides: integration_bootstrap, secret_backed_integration, integration_backed_app, oauth (/connect/oauth), generated_ui_oauth (hosted package app), connect_secret, package_service_pattern.', keywords: [ 'integration bootstrap', 'secret backed integration', + 'integration backed app', 'oauth', 'generated ui', 'redirect uri', 'provider registration', 'secret', + 'package service', ], readOnly: true, idempotent: true, @@ -85,16 +87,18 @@ test('offline search returns provided specs without depending on global ranks', enum: [ 'integration_bootstrap', 'secret_backed_integration', + 'integration_backed_app', 'oauth', 'generated_ui_oauth', 'connect_secret', + 'package_service_pattern', ], }, }, required: ['guide'], }, inputTypeDefinition: - 'type KodyOfficialGuideInput = {\n\tguide: "integration_bootstrap" | "secret_backed_integration" | "oauth" | "generated_ui_oauth" | "connect_secret"\n}', + 'type KodyOfficialGuideInput = {\n\tguide: "integration_bootstrap" | "secret_backed_integration" | "integration_backed_app" | "oauth" | "generated_ui_oauth" | "connect_secret" | "package_service_pattern"\n}', }, } satisfies Record const env = { diff --git a/packages/worker/src/mcp/capabilities/coding/kody-official-guide.node.test.ts b/packages/worker/src/mcp/capabilities/coding/kody-official-guide.node.test.ts index cc59ffc0c0..f9cabbda4b 100644 --- a/packages/worker/src/mcp/capabilities/coding/kody-official-guide.node.test.ts +++ b/packages/worker/src/mcp/capabilities/coding/kody-official-guide.node.test.ts @@ -12,6 +12,24 @@ const ctx = { }, } +test('kody_official_guide description surfaces integration bootstrap flow', () => { + expect(kodyOfficialGuideCapability.description).toContain( + 'Prefer this capability plus `search` results', + ) + expect(kodyOfficialGuideCapability.description).toContain( + 'guide: "integration_bootstrap"', + ) + expect(kodyOfficialGuideCapability.description).toContain( + 'saved `integration` / `secret` entities', + ) + expect(kodyOfficialGuideCapability.description).toContain( + 'cheap authenticated smoke test before building', + ) + expect(kodyOfficialGuideCapability.description).toContain( + 'Available guides (order matters', + ) +}) + test('kody_official_guide returns markdown when fetch succeeds', async () => { const originalFetch = globalThis.fetch const url = buildKodyOfficialGuideUrlForTest('integration_bootstrap') diff --git a/packages/worker/src/mcp/capabilities/coding/kody-official-guide.ts b/packages/worker/src/mcp/capabilities/coding/kody-official-guide.ts index 77335f5629..591bbcf40e 100644 --- a/packages/worker/src/mcp/capabilities/coding/kody-official-guide.ts +++ b/packages/worker/src/mcp/capabilities/coding/kody-official-guide.ts @@ -110,8 +110,27 @@ function buildCapabilityDescription(): string { const g = kodyOfficialGuideCatalog[id] return `- \`${id}\`: ${g.summary}` }) + const intro = [ + 'Load an official Kody guide from the kody GitHub repository (markdown).', + 'Prefer this capability plus `search` results over local repo spelunking', + 'when Kody auth or integration behavior is already documented.', + '**For third-party integrations that will power a package, package app, or workflow,', + 'use `guide: "integration_bootstrap"` first.**', + 'It covers checking saved `integration` / `secret` entities', + 'and running the cheap authenticated smoke test before building.', + 'For non-OAuth APIs backed by saved secrets,', + 'then use `guide: "secret_backed_integration"` as the default recipe.', + 'After the smoke test passes and you are ready to build a package app,', + 'use `guide: "integration_backed_app"` for the default package-app pattern.', + 'For OAuth mechanics, then use `guide: "oauth"` (standard `/connect/oauth` path).', + 'Use `generated_ui_oauth` only for custom package-app OAuth.', + 'For API keys/PATs, use `connect_secret` for secret collection.', + 'For package-native long-lived service work built on `kody.services`,', + 'use `package_service_pattern`.', + 'If you are unsure, **call this capability** with the right `guide` instead of guessing.', + ].join(' ') return [ - 'Load an official Kody guide from the kody GitHub repository (markdown). **For third-party integrations that will power a package, package app, or workflow, use `guide: "integration_bootstrap"` first.** For non-OAuth APIs backed by saved secrets, then use `guide: "secret_backed_integration"` as the default recipe. After the smoke test passes and you are ready to build a package app, use `guide: "integration_backed_app"` for the default package-app pattern. For OAuth mechanics, then use `guide: "oauth"` (standard `/connect/oauth` path). Use `generated_ui_oauth` only for custom package-app OAuth. For API keys/PATs, use `connect_secret` for secret collection. For package-native long-lived service work built on `kody.services`, use `package_service_pattern`. If you are unsure, **call this capability** with the right `guide` instead of guessing.', + intro, '', 'Available guides (order matters—start with `integration_bootstrap` for integration-dependent work):', ...lines, diff --git a/packages/worker/src/mcp/server-instructions.node.test.ts b/packages/worker/src/mcp/server-instructions.node.test.ts index 513b029eee..406bd28c95 100644 --- a/packages/worker/src/mcp/server-instructions.node.test.ts +++ b/packages/worker/src/mcp/server-instructions.node.test.ts @@ -39,3 +39,15 @@ test('surfaces remote connector identifiers and compacts long descriptions', () expect(longConnectorLine).not.toContain(longDescription) expect(longConnectorDescription.length).toBeLessThanOrEqual(240) }) + +test('surfaces integration-backed workflow before package construction', () => { + const instructions = buildMcpServerInstructions(null) + + expect(instructions).toContain('Integration-backed work') + expect(instructions).toContain('guide: "integration_bootstrap"') + expect(instructions).toContain('integration` or `secret` entity') + expect(instructions).toContain('cheap authenticated `execute` smoke test') + expect(instructions).toContain('before local repo exploration') + expect(instructions).toContain('`oauth` for `/connect/oauth`') + expect(instructions).toContain('`secret_backed_integration`') +}) diff --git a/packages/worker/src/mcp/server-instructions.ts b/packages/worker/src/mcp/server-instructions.ts index 7fe66f1798..a617b3622f 100644 --- a/packages/worker/src/mcp/server-instructions.ts +++ b/packages/worker/src/mcp/server-instructions.ts @@ -59,6 +59,7 @@ Conventions - Do not ask the user to paste secrets in chat; use saved secrets or \`open_generated_ui\`. - \`package_save\`: create or replace a repo-backed saved package rooted at \`package.json\`. Standard package exports define the package surface. \`package.json#kody\` contains Kody-specific metadata such as tags, optional app config, and package-owned jobs. - \`package_get\` / \`package_list\` / \`package_delete\`: inspect or manage saved packages for the signed-in user. +- Integration-backed work: use \`search\` and official guides before local repo exploration. For packages, package apps, or workflows that depend on third-party auth, first call \`kody_official_guide\` with \`guide: "integration_bootstrap"\`, confirm the required \`integration\` or \`secret\` entity exists through \`search\`, run a cheap authenticated \`execute\` smoke test, then build. If setup is missing, load \`oauth\` for \`/connect/oauth\`, \`connect_secret\` for secret collection, and \`secret_backed_integration\` for the default non-OAuth recipe. - \`job_list\` / \`job_get\`: inspect the signed-in user's scheduled jobs, recent run outcomes, and current per-user alarm state when debugging scheduling issues. Pass \`includeCode: true\` to \`job_get\` when you need the stored repo-backed job source entrypoint and code. - \`job_schedule\`: schedule a repo-backed job for the signed-in user without creating a saved package first. Supports one-off, interval, and cron schedules. - \`job_schedule_once\`: compatibility wrapper for one-off repo-backed jobs when you only need a single run time. @@ -92,7 +93,7 @@ search execute - Single ESM module string with a default export such as \`export default async function main(input = {}) { ... }\`; \`params\` are passed as the first argument. Import runtime APIs from \`kody:runtime\`. Example: \`import { codemode, refreshAccessToken, createAuthenticatedFetch, oauthClientCredentials, secretHeaders, workflows } from 'kody:runtime'\`. Built-in capabilities returned by \`search\` are available through \`codemode\`: use \`await codemode.capability_id(input)\` for valid identifier names or \`await codemode["capability-id"](input)\` for non-identifier ids. \`workflows.create\` can queue either inline \`code\` or a saved-package \`exportName\` through the shared Workflow hub. Use \`secretHeaders.basic(...)\` or \`oauthClientCredentials(...)\` for client-credentials Basic Auth instead of asking users to precompute a Basic header. Prefer one \`execute\` when the plan is clear. Full rules for \`fetch\`, placeholders, \`secret_list\` / \`value_get\`, and \`x-kody-secret\`: see the \`execute\` tool description. - Cross-package imports use specifiers such as \`kody:@scope/my-package/export-name\`. For dynamic current-version calls inside package runtime code or authenticated execute calls, prefer \`packages.invokeChecked({ kodyId, exportName, params })\` or \`packages.check(...)\` followed by \`packages.invoke(check.invoke)\`; use static imports for library-like bundled snapshots. Saved package names must be scoped (\`@scope/\`) and the leaf segment must match \`kody.id\`. Package jobs are owned by packages, ad hoc jobs can be scheduled with \`job_schedule\`, and package apps are optional package surfaces. -- Official how-to guides from the Kody repo: if a requested package or workflow depends on a third-party integration, secrets, or OAuth, call \`kody_official_guide\` with \`guide: "integration_bootstrap"\` before building the package. Then load the relevant setup guide: \`oauth\` for standard third-party OAuth (\`/connect/oauth\`), \`connect_secret\` for secret collection, and \`secret_backed_integration\` for the default non-OAuth secret-backed recipe after bootstrap. If unsure, \`search\` for this capability and load the right guide before implementing. +- Official how-to guides from the Kody repo: if a requested package, package app, or workflow depends on a third-party integration, secrets, or OAuth, call \`kody_official_guide\` with \`guide: "integration_bootstrap"\` before building. If unsure, \`search\` for this capability and load the right guide before implementing. - Do not save or present an auth-dependent package as complete until \`search\` shows the required integration or secret reference exists and a minimal authenticated \`execute\` smoke test succeeds. open_generated_ui diff --git a/packages/worker/src/mcp/tools/execute.node.test.ts b/packages/worker/src/mcp/tools/execute.node.test.ts index 8e2cb8aa17..4baab59be9 100644 --- a/packages/worker/src/mcp/tools/execute.node.test.ts +++ b/packages/worker/src/mcp/tools/execute.node.test.ts @@ -39,7 +39,7 @@ function mockPerformanceSequence(...values: Array) { }) } -async function getExecuteHandler( +async function getExecuteRegistration( callerContext: { baseUrl: string user: null | { @@ -66,9 +66,27 @@ async function getExecuteHandler( } as never) expect(registerTool).toHaveBeenCalledTimes(1) - const [name, , handler] = registerTool.mock.calls[0] ?? [] + const [name, rawOptions, handler] = registerTool.mock.calls[0] ?? [] expect(name).toBe('execute') expect(typeof handler).toBe('function') + const options = rawOptions as { description: string } + return { options, handler } +} + +async function getExecuteHandler( + callerContext: { + baseUrl: string + user: null | { + userId: string + email?: string + displayName?: string + } + } = { + baseUrl: 'https://example.com', + user: null, + }, +) { + const { handler } = await getExecuteRegistration(callerContext) return handler as (input: { code: string storageId?: string @@ -95,6 +113,20 @@ async function getExecuteHandler( }> } +test('execute tool description surfaces integration-backed smoke-test flow', async () => { + const { options } = await getExecuteRegistration() + + expect(options.description).toContain('integration-backed packages') + expect(options.description).toContain( + "kody_official_guide({ guide: 'integration_bootstrap' })", + ) + expect(options.description).toContain( + 'cheap read-only authenticated smoke test', + ) + expect(options.description).toContain('before `package_save`') + expect(options.description).toContain('`oauth`, `connect_secret`') +}) + test('execute tool passes through raw MCP content blocks in success responses', async () => { const handler = await getExecuteHandler() mockPerformanceSequence(100, 142) diff --git a/packages/worker/src/mcp/tools/execute.ts b/packages/worker/src/mcp/tools/execute.ts index 2c97190563..7680985b30 100644 --- a/packages/worker/src/mcp/tools/execute.ts +++ b/packages/worker/src/mcp/tools/execute.ts @@ -66,6 +66,8 @@ Never ask the user to paste credentials in chat; use generated UI to collect or Prefer one \`execute\` when the workflow is clear; split calls when you need new user input or a changed plan. +For integration-backed packages, package apps, or workflows, use \`search\` and \`kody_official_guide({ guide: 'integration_bootstrap' })\` before building. Confirm the needed \`integration\` or \`secret\` entity exists, then run a cheap read-only authenticated smoke test in \`execute\` (for example a profile/viewer endpoint) before \`package_save\`, package app work, or workflow scheduling. If credentials are missing, load the matching official guide: \`oauth\`, \`connect_secret\`, or \`secret_backed_integration\`. + Example: \`import { codemode } from 'kody:runtime' diff --git a/packages/worker/src/mcp/tools/search-handler.node.test.ts b/packages/worker/src/mcp/tools/search-handler.node.test.ts index 8e195b2a24..c373a57496 100644 --- a/packages/worker/src/mcp/tools/search-handler.node.test.ts +++ b/packages/worker/src/mcp/tools/search-handler.node.test.ts @@ -69,7 +69,7 @@ const { registerSearchTool } = await import('./search.ts') const mockPerformanceNow = vi.spyOn(performance, 'now') -async function getSearchHandler() { +async function getSearchRegistration() { const registerTool = vi.fn() await registerSearchTool({ @@ -85,9 +85,15 @@ async function getSearchHandler() { } as never) expect(registerTool).toHaveBeenCalledTimes(1) - const [name, , handler] = registerTool.mock.calls[0] ?? [] + const [name, rawOptions, handler] = registerTool.mock.calls[0] ?? [] expect(name).toBe('search') expect(typeof handler).toBe('function') + const options = rawOptions as { description: string } + return { options, handler } +} + +async function getSearchHandler() { + const { handler } = await getSearchRegistration() return handler as (input: { query?: string entity?: string @@ -113,6 +119,17 @@ async function getSearchHandler() { }> } +test('search tool description surfaces integration-backed discovery flow', async () => { + vi.clearAllMocks() + const { options } = await getSearchRegistration() + + expect(options.description).toContain('Integration-backed packages') + expect(options.description).toContain('kody_official_guide') + expect(options.description).toContain('guide: "integration_bootstrap"') + expect(options.description).toContain('integration` or `secret` entities') + expect(options.description).toContain('authenticated `execute` smoke test') +}) + test('search tool reports timing metadata across success and error flows', async () => { vi.clearAllMocks() const handler = await getSearchHandler() diff --git a/packages/worker/src/mcp/tools/search.ts b/packages/worker/src/mcp/tools/search.ts index fad2c997b7..a6c364df75 100644 --- a/packages/worker/src/mcp/tools/search.ts +++ b/packages/worker/src/mcp/tools/search.ts @@ -1434,6 +1434,13 @@ Secrets: never raw in results; use Persisted values use \`codemode.value_get\` / \`codemode.value_list\`. Integrations use \`codemode.integration_get\` / \`codemode.integration_list\`. +Integration-backed packages, package apps, and workflows: search for the provider +and \`kody_official_guide\`, inspect exact \`integration\` or \`secret\` entities, +load \`guide: "integration_bootstrap"\`, and continue only after a cheap +authenticated \`execute\` smoke test succeeds. If setup is missing, use the guide +that matches the auth path: \`oauth\`, \`connect_secret\`, or +\`secret_backed_integration\`. + If results look incomplete: \`meta_list_capabilities\` (full registry) or \`meta_list_remote_connector_status\` (remote connectors).