Skip to content

feat: add token-mode bootstrap skill#7

Open
michiosw wants to merge 2 commits intomainfrom
feat/kontext-token-mode-bootstrap-skill
Open

feat: add token-mode bootstrap skill#7
michiosw wants to merge 2 commits intomainfrom
feat/kontext-token-mode-bootstrap-skill

Conversation

@michiosw
Copy link
Contributor

@michiosw michiosw commented Mar 18, 2026

Summary

  • add the kontext-token-mode-bootstrap skill under .claude/skills
  • teach the bootstrap flow to use the current provider-based management API instead of the removed integrations endpoints
  • update the Claude/OpenAI skill metadata and README copy to match requireProvider(providerHandle, token)

Validation

  • node --check .claude/skills/kontext-token-mode-bootstrap/scripts/bootstrap-token-mode.mjs
  • verified the installed helper against the local API on http://localhost:4000
  • confirmed it reuses the Runtime Demo public app, reuses or updates the google-workspace provider, and writes the public client ID env output

Copy link

@kontext-review kontext-review bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Kontext Review: REQUEST_CHANGES (4 findings, Advisory)

Verification certificate:

  • Verdict: REQUEST_CHANGES
  • Depth: full
  • Confidence: 0.75
  • Certificate valid: yes
  • Scenarios traced: 1
  • Verification bounds:
    • Changed files and explicitly traced call paths

Inline findings posted:

  • [HIGH][security] Architectural finding (.claude/skills/kontext-token-mode-bootstrap/scripts/bootstrap-token-mode.mjs:550)
  • [LOW][maintainability] Architectural finding (.claude/skills/kontext-token-mode-bootstrap/scripts/bootstrap-token-mode.mjs:12)
  • [LOW][correctness] Architectural finding (.claude/skills/kontext-token-mode-bootstrap/scripts/bootstrap-token-mode.mjs:600)
  • [LOW][docs] Architectural finding (.claude/skills/kontext-token-mode-bootstrap/SKILL.md:1)

Reviewer summary:

  • The PR introduces a new skill kontext-token-mode-bootstrap to bootstrap a public PKCE app and token-mode integration setup.
  • It includes a Node.js script bootstrap-token-mode.mjs that interacts with the Kontext Management API to create or update applications and integrations.
  • The script handles environment variable parsing, API authentication, resource creation/updating, and optionally writes the public client ID to a local environment file.
  • The PR also updates README.md and adds SKILL.md and agents/openai.yaml for documentation and metadata.
Scores and diagnostics
  • Architecture: 7.0/10
  • Organization: 7.0/10
  • Cleanliness: 7.0/10
  • Overall: 7.0/10
  • Candidate findings: 4
  • Published inline: 4
  • Published summary-only: 0
  • Suppressed findings: 0
  • Verification verdict: REQUEST_CHANGES

Limitations

  • The script relies on the git command line tool to check if a file is tracked. If git is not installed or the script is run outside a git repository, the check will silently pass. This is an acceptable limitation given the context.

  • Model: gemini-3.1-pro-preview

  • Omitted files: 0

Generated at 2026-03-18T18:16:50.990Z.

Comment on lines +550 to +583
integration = updatedResponse.integration;
updated = true;
}

let validation = null;
if (validateIntegration) {
validation = await apiRequest({
baseUrl,
token,
method: "POST",
path: `/integrations/${integration.id}/validate`,
});
}

const refreshed = (
await apiRequest({
baseUrl,
token,
method: "GET",
path: `/integrations/${integration.id}`,
})
).integration;

return {
integration: refreshed,
created,
updated,
validation,
};
}

async function ensureAttached({ baseUrl, token, applicationId, integrationId }) {
const attached = await apiRequest({
baseUrl,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH][security] Architectural finding

Impact: Can reduce maintainability or correctness if left unresolved.
Suggested fix: Apply a targeted refactor and add/adjust tests where needed.
Evidence: The script correctly checks if the target environment file is tracked by git and refuses to write to it if it is, preventing accidental commits of sensitive data.
Evidence citations:

  • .claude/skills/kontext-token-mode-bootstrap/scripts/bootstrap-token-mode.mjs:550

Comment on lines +12 to +583
function normalizeBaseUrl(value) {
return value.replace(/\/api\/v1\/?$/, "").replace(/\/$/, "");
}

function normalizeUrl(value) {
return value.replace(/\/$/, "");
}

function required(name, value) {
if (!value) {
throw new Error(`Missing required value: ${name}`);
}

return value;
}

function parseOptionalTrimmed(value) {
if (typeof value !== "string") {
return undefined;
}

const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}

function requiredTrimmed(name, value) {
const normalized = parseOptionalTrimmed(required(name, value));
if (!normalized) {
throw new Error(`${name} must not be empty.`);
}

return normalized;
}

function parseStringArray(name, raw, fallback) {
if (!raw) {
return fallback;
}

let parsed;
try {
parsed = JSON.parse(raw);
} catch (error) {
throw new Error(`${name} must be valid JSON: ${error.message}`);
}

if (!Array.isArray(parsed) || parsed.some((value) => typeof value !== "string")) {
throw new Error(`${name} must be a JSON array of strings.`);
}

return parsed.map((value) => value.trim()).filter(Boolean);
}

function parseBoolean(name, value, fallback) {
const normalized = parseOptionalTrimmed(value);
if (!normalized) {
return fallback;
}

const lower = normalized.toLowerCase();
if (["1", "true", "yes"].includes(lower)) {
return true;
}
if (["0", "false", "no"].includes(lower)) {
return false;
}

throw new Error(`${name} must be one of: true, false, 1, 0, yes, no.`);
}

function normalizeAuthMode(value) {
const normalized = parseOptionalTrimmed(value);
if (!normalized) {
return undefined;
}

if (!VALID_AUTH_MODES.has(normalized)) {
throw new Error(
`KONTEXT_INTEGRATION_AUTH_MODE must be one of: ${Array.from(VALID_AUTH_MODES).join(", ")}`,
);
}

return normalized;
}

function buildOauthConfig({ provider, issuer, scopes }) {
const oauth = {};

if (provider) {
oauth.provider = provider;
}
if (issuer) {
oauth.issuer = issuer;
}
if (Array.isArray(scopes) && scopes.length > 0) {
oauth.scopes = scopes;
}

return Object.keys(oauth).length > 0 ? oauth : undefined;
}

function buildIntegrationPayload(input) {
const payload = {};

if (input.name !== undefined) {
payload.name = input.name;
}
if (input.url !== undefined) {
payload.url = input.url;
}
if (input.authMode !== undefined) {
payload.authMode = input.authMode;
}
if (input.oauth !== undefined) {
payload.oauth = input.oauth;
}
if (input.serverToken !== undefined) {
payload.serverToken = input.serverToken;
}

return payload;
}

function buildApplicationOauthPayload({
redirectUris,
pkceRequired,
scopes,
allowedResources,
}) {
const payload = {
type: "public",
redirectUris,
pkceRequired,
scopes,
};

if (Array.isArray(allowedResources) && allowedResources.length > 0) {
payload.allowedResources = allowedResources;
}

return payload;
}

function arraysEqual(left, right) {
if (left.length !== right.length) {
return false;
}

return left.every((value, index) => value === right[index]);
}

function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

async function requestToken({ baseUrl, resource, clientId, clientSecret }) {
const body = new URLSearchParams({
grant_type: "client_credentials",
scope: "management:all",
audience: resource,
});

const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");

const response = await fetch(`${baseUrl}/oauth2/token`, {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
accept: "application/json",
authorization: `Basic ${basicAuth}`,
},
body: body.toString(),
});

if (!response.ok) {
const message = await response.text();
throw new Error(
`Service account authentication failed (${response.status}): ${message}`,
);
}

const payload = await response.json();
return payload.access_token;
}

async function apiRequest({ baseUrl, token, method, path: requestPath, body }) {
const response = await fetch(`${baseUrl}/api/v1${requestPath}`, {
method,
headers: {
authorization: `Bearer ${token}`,
accept: "application/json",
...(body ? { "content-type": "application/json" } : {}),
},
...(body ? { body: JSON.stringify(body) } : {}),
});

if (!response.ok) {
const message = await response.text();
throw new Error(`${method} ${requestPath} failed (${response.status}): ${message}`);
}

if (response.status === 204) {
return null;
}

return response.json();
}

async function listAllApplications({ baseUrl, token }) {
const items = [];
let cursor;

while (true) {
const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : "";
const response = await apiRequest({
baseUrl,
token,
method: "GET",
path: `/applications${query}`,
});

items.push(...(response.items ?? []));
if (!response.nextCursor) {
return items;
}

cursor = response.nextCursor;
}
}

async function listAllIntegrations({ baseUrl, token }) {
const items = [];
let cursor;

while (true) {
const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : "";
const response = await apiRequest({
baseUrl,
token,
method: "GET",
path: `/integrations${query}`,
});

items.push(...(response.items ?? []));
if (!response.nextCursor) {
return items;
}

cursor = response.nextCursor;
}
}

async function resolveOrCreateApplication({
baseUrl,
token,
applicationId,
applicationName,
createApplication,
redirectUris,
pkceRequired,
scopes,
allowedResources,
}) {
let application;
let oauth;
let created = false;
let updated = false;
let allowedResourcesNote = null;

if (applicationId) {
const response = await apiRequest({
baseUrl,
token,
method: "GET",
path: `/applications/${applicationId}`,
});
application = response.application;
} else {
application = (await listAllApplications({ baseUrl, token })).find(
(item) => item.name === applicationName,
);
}

if (!application) {
if (!createApplication) {
throw new Error(
`Application named "${applicationName}" was not found. Set KONTEXT_CREATE_APPLICATION=true to create it.`,
);
}

if (redirectUris.length === 0) {
throw new Error(
"KONTEXT_APPLICATION_REDIRECT_URIS_JSON is required when creating a public application.",
);
}

const createdResponse = await apiRequest({
baseUrl,
token,
method: "POST",
path: "/applications",
body: {
name: applicationName,
oauth: buildApplicationOauthPayload({
redirectUris,
pkceRequired,
scopes,
allowedResources,
}),
},
});

application = createdResponse.application;
oauth = createdResponse.oauth;
created = true;
} else {
const oauthResponse = await apiRequest({
baseUrl,
token,
method: "GET",
path: `/applications/${application.id}/oauth`,
});
oauth = oauthResponse.oauth;
}

if (oauth.type !== "public") {
throw new Error(
`Application "${application.name}" is a ${oauth.type} client. Token mode bootstrap requires a public application.`,
);
}

if (!created) {
const patch = {};

if (oauth.pkceRequired !== pkceRequired) {
patch.pkceRequired = pkceRequired;
}

const currentScopes = Array.isArray(oauth.scopes) ? oauth.scopes : [];
if (!arraysEqual(currentScopes, scopes)) {
patch.scopes = scopes;
}

if (redirectUris.length > 0) {
const currentRedirectUris = Array.isArray(oauth.redirectUris)
? oauth.redirectUris
: [];
if (!arraysEqual(currentRedirectUris, redirectUris)) {
patch.redirectUris = redirectUris;
}
}

if (allowedResources.length > 0) {
allowedResourcesNote =
"Existing application reused. allowedResources were not mutated by this script.";
}

if (Object.keys(patch).length > 0) {
const patched = await apiRequest({
baseUrl,
token,
method: "PATCH",
path: `/applications/${application.id}/oauth`,
body: patch,
});
oauth = patched.oauth;
updated = true;
}
}

return {
application,
oauth,
created,
updated,
allowedResourcesNote,
};
}

function buildDesiredIntegration({
integrationName,
integrationUrl,
integrationAuthMode,
oauthProvider,
oauthIssuer,
oauthScopes,
serverToken,
}) {
const resolvedServerToken = parseOptionalTrimmed(serverToken);
if (resolvedServerToken && integrationAuthMode !== "server_token") {
throw new Error(
"KONTEXT_SERVER_TOKEN can only be used when KONTEXT_INTEGRATION_AUTH_MODE=server_token.",
);
}

if (
(oauthProvider || oauthIssuer || (oauthScopes && oauthScopes.length > 0)) &&
integrationAuthMode !== "oauth"
) {
throw new Error(
"OAuth fields can only be used when KONTEXT_INTEGRATION_AUTH_MODE=oauth.",
);
}

return {
name: integrationName,
url: integrationUrl,
authMode: integrationAuthMode,
oauth: buildOauthConfig({
provider: oauthProvider,
issuer: oauthIssuer,
scopes: oauthScopes,
}),
serverToken: resolvedServerToken,
};
}

async function resolveExistingIntegration({
baseUrl,
token,
integrationId,
integrationName,
}) {
if (integrationId) {
const response = await apiRequest({
baseUrl,
token,
method: "GET",
path: `/integrations/${integrationId}`,
});
return response.integration;
}

if (!integrationName) {
return null;
}

return (
(await listAllIntegrations({ baseUrl, token })).find(
(item) => item.name === integrationName,
) ?? null
);
}

function shouldUpdateIntegration(existing, desired) {
if (!existing) {
return false;
}

if (desired.name !== undefined && desired.name !== existing.name) {
return true;
}
if (
desired.url !== undefined &&
normalizeUrl(desired.url) !== normalizeUrl(existing.url)
) {
return true;
}
if (
desired.authMode !== undefined &&
desired.authMode !== (existing.authMode ?? "none")
) {
return true;
}
if (desired.oauth) {
const existingScopes = Array.isArray(existing.oauth?.scopes)
? existing.oauth.scopes
: [];
const desiredScopes = Array.isArray(desired.oauth.scopes)
? desired.oauth.scopes
: [];

if ((desired.oauth.provider ?? null) !== (existing.oauth?.provider ?? null)) {
return true;
}
if ((desired.oauth.issuer ?? null) !== (existing.oauth?.issuer ?? null)) {
return true;
}
if (!arraysEqual(desiredScopes, existingScopes)) {
return true;
}
}
if (desired.serverToken !== undefined) {
return true;
}

return false;
}

async function upsertIntegration({
baseUrl,
token,
existing,
desired,
validateIntegration,
}) {
let integration = existing;
let created = false;
let updated = false;

if (!integration) {
if (!desired.name || !desired.url) {
throw new Error(
"Creating an integration requires KONTEXT_INTEGRATION_NAME and KONTEXT_INTEGRATION_URL.",
);
}

const createdResponse = await apiRequest({
baseUrl,
token,
method: "POST",
path: "/integrations",
body: buildIntegrationPayload({
name: desired.name,
url: desired.url,
authMode: desired.authMode ?? "none",
oauth: desired.oauth,
serverToken: desired.serverToken,
}),
});

integration = createdResponse.integration;
created = true;
} else if (shouldUpdateIntegration(integration, desired)) {
const updatedResponse = await apiRequest({
baseUrl,
token,
method: "PATCH",
path: `/integrations/${integration.id}`,
body: buildIntegrationPayload({
name: desired.name,
url: desired.url,
authMode: desired.authMode,
oauth: desired.oauth,
serverToken: desired.serverToken,
}),
});

integration = updatedResponse.integration;
updated = true;
}

let validation = null;
if (validateIntegration) {
validation = await apiRequest({
baseUrl,
token,
method: "POST",
path: `/integrations/${integration.id}/validate`,
});
}

const refreshed = (
await apiRequest({
baseUrl,
token,
method: "GET",
path: `/integrations/${integration.id}`,
})
).integration;

return {
integration: refreshed,
created,
updated,
validation,
};
}

async function ensureAttached({ baseUrl, token, applicationId, integrationId }) {
const attached = await apiRequest({
baseUrl,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW][maintainability] Architectural finding

Impact: Can reduce maintainability or correctness if left unresolved.
Suggested fix: Apply a targeted refactor and add/adjust tests where needed.
Evidence: The script is well-organized into small, purpose-specific functions, making it easy to read and maintain.
Evidence citations:

  • .claude/skills/kontext-token-mode-bootstrap/scripts/bootstrap-token-mode.mjs:12

Comment on lines +600 to +681

return true;
}

async function writeEnvValue({ outputFile, variableName, value }) {
const absolutePath = path.resolve(outputFile);
await ensureFileIsNotTracked(absolutePath);
let contents = "";

try {
contents = await fs.readFile(absolutePath, "utf8");
} catch (error) {
if (error.code !== "ENOENT") {
throw error;
}
}

const line = `${variableName}=${value}`;
const pattern = new RegExp(`^${escapeRegex(variableName)}=.*$`, "m");
let nextContents;

if (pattern.test(contents)) {
nextContents = contents.replace(pattern, line);
} else if (contents.length === 0) {
nextContents = `${line}\n`;
} else {
const separator = contents.endsWith("\n") ? "" : "\n";
nextContents = `${contents}${separator}${line}\n`;
}

await fs.writeFile(absolutePath, nextContents, "utf8");
return absolutePath;
}

async function ensureFileIsNotTracked(absolutePath) {
let repoRoot;

try {
const result = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
cwd: path.dirname(absolutePath),
});
repoRoot = result.stdout.trim();
} catch {
return;
}

const relativePath = path.relative(repoRoot, absolutePath);
if (relativePath.startsWith("..")) {
return;
}

try {
await execFileAsync("git", ["ls-files", "--error-unmatch", relativePath], {
cwd: repoRoot,
});
throw new Error(
`Refusing to write ${absolutePath} because it is tracked by git. Use a local-only env file instead.`,
);
} catch (error) {
if (
error instanceof Error &&
error.message.includes("Refusing to write")
) {
throw error;
}
}
}

function buildRuntimeSummary(integrationName) {
return {
retrievalMethod: "kontext.require(integration, token)",
integrationName,
firstTimeConnect: "hosted_connect",
note: "Pass the authenticated Kontext token into runtime code and handle IntegrationConnectionRequiredError for first-time connect.",
};
}

async function main() {
const { values, positionals } = parseArgs({
options: {
"api-base-url": { type: "string" },
"service-account-client-id": { type: "string" },

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW][correctness] Architectural finding

Impact: Can reduce maintainability or correctness if left unresolved.
Suggested fix: Apply a targeted refactor and add/adjust tests where needed.
Evidence: The script correctly parses command line arguments and environment variables, and validates required inputs.
Evidence citations:

  • .claude/skills/kontext-token-mode-bootstrap/scripts/bootstrap-token-mode.mjs:600

Comment on lines +1 to +274
---
name: kontext-token-mode-bootstrap
description: Bootstrap a public Kontext application for token-mode runtime credential exchange via the Management API using a service account. Use when replacing hardcoded end-user API tokens with `kontext.require(integration, token)`, creating or reusing a public PKCE app, ensuring and attaching a user-chosen provider integration, and optionally writing the public client ID into a local env file. Do not use this skill for confidential `userId` retrieval or Bring your own auth.
---

# Kontext Token Mode Bootstrap

Use this skill when the app should keep user credentials at runtime and stop relying on hardcoded provider tokens in source code or env.

This skill covers one path:
- bootstrap or reuse a **public** Kontext application with PKCE
- ensure or update a provider integration chosen by the caller
- attach that integration to the public app
- optionally write the **public client ID only** into a local env file
- rewrite runtime code to use token mode:

```ts
const credential = await kontext.require(integrationName, token);
```

This skill does **not** cover:
- confidential `kontext.require(integration, { userId })` retrieval
- Bring your own auth, issuer, JWKS, or partner connect bootstrap
- hardcoding a provider recipe like Gmail into the skill itself

If the request is about confidential backend retrieval with `userId`, use `kontext-sdk-credentials` instead.
If the request is about trusting the app's own auth system and avoiding double auth, use `kontext-byoa-setup` instead.

## Runtime Shape

Use this flow:

1. Admin or setup agent runs the bundled bootstrap script with a service account.
2. The script creates or reuses a **public** app with PKCE.
3. The script creates, updates, or reuses the target integration based on env inputs.
4. The script attaches that integration to the app.
5. The script prints the app client ID and can write it into a local env file.
6. The app signs the end user in with PKCE.
7. Runtime code passes the authenticated Kontext token into `kontext.require(...)`.
8. If the integration is not connected yet, handle `IntegrationConnectionRequiredError` and send the user to `connectUrl`.
9. Retry after connect and continue the task.

Provider-specific configuration belongs in the command inputs, not in this skill. The skill should stay generic.

## Execution Rules

Follow these rules in order:

1. Validate bootstrap inputs first.
2. If `KONTEXT_SERVICE_ACCOUNT_CLIENT_ID` or `KONTEXT_SERVICE_ACCOUNT_CLIENT_SECRET` is missing, stop immediately and tell the user exactly which variable is missing.
3. If `KONTEXT_OUTPUT_ENV_FILE` is missing, infer a conventional ignored local env target when the repo already clearly uses one, for example `apps/web/.env.local`, and pass it inline when running the helper.
4. If the user names an existing integration such as `google-workspace`, treat that as **reuse and attach first**. Do not try to invent provider templates or broad management recipes unless the user explicitly asked for integration creation details.
5. Run the bundled helper from the installed skill directly. Do not vendor or copy the helper into the target repo.
6. Keep repo exploration narrow until bootstrap is done or blocked. Do not pivot into unrelated examples, demos, or legacy integrations just because they mention the same provider.
7. After bootstrap succeeds, edit the target runtime surface and remove the hardcoded credential path.

## Required Inputs

Service account:
- `KONTEXT_SERVICE_ACCOUNT_CLIENT_ID`
- `KONTEXT_SERVICE_ACCOUNT_CLIENT_SECRET`
- `KONTEXT_API_BASE_URL` (optional, defaults to `https://api.kontext.dev`)
- `MANAGEMENT_API_RESOURCE` (optional, defaults to `${KONTEXT_API_BASE_URL}/api/v1`)

Target application:
- `KONTEXT_APPLICATION_ID`, or
- `KONTEXT_APPLICATION_NAME`

Create the app when missing:
- `KONTEXT_CREATE_APPLICATION=true`
- `KONTEXT_APPLICATION_REDIRECT_URIS_JSON='["http://localhost:3000/callback"]'`

Optional public-app settings:
- `KONTEXT_APPLICATION_SCOPES_JSON` defaults to `["mcp:invoke"]`
- `KONTEXT_APPLICATION_PKCE_REQUIRED` defaults to `true`
- `KONTEXT_APPLICATION_ALLOWED_RESOURCES_JSON` defaults to `["mcp-gateway"]` when creating a new app

Target integration:
- `KONTEXT_INTEGRATION_ID`, or
- `KONTEXT_INTEGRATION_NAME`

Create or update a generic integration:
- `KONTEXT_INTEGRATION_URL`
- `KONTEXT_INTEGRATION_AUTH_MODE`
- `KONTEXT_INTEGRATION_OAUTH_PROVIDER`
- `KONTEXT_INTEGRATION_OAUTH_ISSUER`
- `KONTEXT_INTEGRATION_OAUTH_SCOPES_JSON`
- `KONTEXT_SERVER_TOKEN`
- `KONTEXT_VALIDATE_INTEGRATION=true`

Optional env output:
- `KONTEXT_OUTPUT_ENV_FILE` such as `.env.local`
- `KONTEXT_PUBLIC_CLIENT_ID_ENV_NAME` defaults to `NEXT_PUBLIC_KONTEXT_CLIENT_ID`

## Secret Handling Rules

Follow these rules strictly:

- Never print `KONTEXT_SERVICE_ACCOUNT_CLIENT_SECRET` unless the user explicitly asks.
- Never print `KONTEXT_SERVER_TOKEN` unless the user explicitly asks.
- Never write service account credentials, shared server tokens, or live provider credentials into tracked files.
- Only write the **public** client ID into env output files.
- Never commit secrets or live tokens.
- If the target env file is tracked, stop and tell the user instead of writing into it.

## Workflow

1. Confirm the request really wants token mode and a public PKCE app.
2. Validate the bootstrap inputs before scanning the repo:
- service account vars
- output env target
- application name or ID
- integration name or create/update inputs
3. Run the bundled setup helper immediately. Do this before broad codebase exploration:

```bash
node scripts/bootstrap-token-mode.mjs
```

4. If the helper is blocked, stop with the exact missing input. Do not continue with unrelated repo exploration.
5. Find the hardcoded provider credential path in the specific target surface the user asked for:
- `Bearer ...`
- `process.env.*TOKEN`
- `process.env.*KEY`
- literal provider access tokens
6. Read the output:
- public app client ID
- integration ID and name
- whether the integration was created, updated, or reused
- whether the env file was written
7. Replace the hardcoded credential path with token mode:

```ts
const credential = await kontext.require(integrationName, token);
```

8. If the runtime surface is an MCP server or backend route, keep the user token flowing into that server call.
9. If runtime code can hit a first-time-connect case, handle `IntegrationConnectionRequiredError` and use `err.connectUrl`.
10. Summarize the final setup and the exact runtime integration name.

## Preferred Command Pattern

Bootstrap a new public PKCE app, create a generic OAuth integration, attach it, and write the public client ID into `.env.local`:

```bash
KONTEXT_SERVICE_ACCOUNT_CLIENT_ID=... \
KONTEXT_SERVICE_ACCOUNT_CLIENT_SECRET=... \
KONTEXT_APPLICATION_NAME="My Demo Agent" \
KONTEXT_CREATE_APPLICATION=true \
KONTEXT_APPLICATION_REDIRECT_URIS_JSON='["http://localhost:3000/callback"]' \
KONTEXT_INTEGRATION_NAME="My Provider" \
KONTEXT_INTEGRATION_URL="https://provider.example.com/mcp" \
KONTEXT_INTEGRATION_AUTH_MODE=oauth \
KONTEXT_INTEGRATION_OAUTH_PROVIDER="provider-name" \
KONTEXT_INTEGRATION_OAUTH_SCOPES_JSON='["scope-a","scope-b"]' \
KONTEXT_OUTPUT_ENV_FILE=.env.local \
KONTEXT_PUBLIC_CLIENT_ID_ENV_NAME=NEXT_PUBLIC_KONTEXT_DEMO_CLIENT_ID \
node scripts/bootstrap-token-mode.mjs
```

Attach an existing integration to an existing public app and only print the public client ID:

```bash
KONTEXT_SERVICE_ACCOUNT_CLIENT_ID=... \
KONTEXT_SERVICE_ACCOUNT_CLIENT_SECRET=... \
KONTEXT_APPLICATION_ID=app_... \
KONTEXT_INTEGRATION_ID=int_... \
node scripts/bootstrap-token-mode.mjs
```

Create or update a generic `user_token` integration:

```bash
KONTEXT_SERVICE_ACCOUNT_CLIENT_ID=... \
KONTEXT_SERVICE_ACCOUNT_CLIENT_SECRET=... \
KONTEXT_APPLICATION_NAME="My Agent" \
KONTEXT_CREATE_APPLICATION=true \
KONTEXT_APPLICATION_REDIRECT_URIS_JSON='["http://localhost:3000/callback"]' \
KONTEXT_INTEGRATION_NAME="My API" \
KONTEXT_INTEGRATION_URL="https://mcp.example.com" \
KONTEXT_INTEGRATION_AUTH_MODE=user_token \
node scripts/bootstrap-token-mode.mjs
```

## Runtime Integration Pattern

For client auth:

```ts
const client = createKontextClient({
clientId: process.env.NEXT_PUBLIC_KONTEXT_CLIENT_ID!,
redirectUri: `${window.location.origin}/callback`,
onAuthRequired: (url) => {
window.location.href = url.toString();
},
});
```

For runtime credential exchange:

```ts
const kontext = new Kontext({
clientId: process.env.NEXT_PUBLIC_KONTEXT_CLIENT_ID!,
apiUrl: process.env.KONTEXT_API_URL,
});

const credential = await kontext.require("provider-integration", token);
```

First-time connect handling:

```ts
import { IntegrationConnectionRequiredError } from "@kontext-dev/js-sdk/errors";

try {
const credential = await kontext.require("provider-integration", token);
} catch (error) {
if (error instanceof IntegrationConnectionRequiredError && error.connectUrl) {
window.location.href = error.connectUrl;
}
throw error;
}
```

## Prompt Template

Use this when the user wants a coding agent to remove hardcoded creds:

```text
Use $kontext-token-mode-bootstrap.

This app currently uses hardcoded end-user credentials for an external provider. Replace that with Kontext token mode.

Bootstrap:
- create or reuse a public Kontext application with PKCE
- create, update, or reuse the provider integration described by the current env inputs
- attach that integration to the public app
- write the public client ID into the local env file if KONTEXT_OUTPUT_ENV_FILE is set

Runtime:
- keep the end user on PKCE
- use kontext.require("<integration>", token) at runtime
- if the integration is not connected yet, use the hosted connect flow and continue after connect

Do not switch this to confidential userId mode.
Do not hardcode provider scopes into the skill. Read them from the current env or prompt.
```

## Success Output

Return a short summary in this shape:

Token-mode bootstrap configured.

Application:
- Name: <application name>
- Application ID: <application id>
- OAuth type: public
- Client ID: <public client id>

Integration:
- Name: <integration name>
- Integration ID: <integration id>
- Auth mode: <auth mode>
- Status: <created|updated|reused>
- App attachment: <attached_now|already_attached>

Runtime:
- Retrieval method: kontext.require(integration, token)
- First-time connect: hosted connect page

Env output:
- File: <only show when written>
- Variable: <only show when written>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW][docs] Architectural finding

Impact: Can reduce maintainability or correctness if left unresolved.
Suggested fix: Apply a targeted refactor and add/adjust tests where needed.
Evidence: The documentation in SKILL.md and README.md is clear, comprehensive, and accurately describes the skill's functionality and usage.
Evidence citations:

  • .claude/skills/kontext-token-mode-bootstrap/SKILL.md:1
  • .claude/skills/kontext-token-mode-bootstrap/SKILL.md:1

@michiosw michiosw force-pushed the feat/kontext-token-mode-bootstrap-skill branch from 7f6619c to 2b21575 Compare March 18, 2026 18:16
Copy link

@kontext-review kontext-review bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Kontext Review: REQUEST_CHANGES (1 findings, Advisory)

Verification certificate:

  • Verdict: REQUEST_CHANGES
  • Depth: lite
  • Confidence: 0.75
  • Certificate valid: yes
  • Scenarios traced: 1
  • Verification bounds:
    • Changed files and explicitly traced call paths

Inline findings posted:

  • [LOW][docs] Architectural finding (.claude/skills/kontext-token-mode-bootstrap/SKILL.md:45)

Reviewer summary:

  • Added an 'Execution Rules' section to explicitly define the order of operations for the skill.
  • Reordered the 'Workflow' section to prioritize input validation and running the setup helper before scanning the repository for hardcoded credentials.
Scores and diagnostics
  • Architecture: 7.0/10
  • Organization: 7.0/10
  • Cleanliness: 7.0/10
  • Overall: 7.0/10
  • Candidate findings: 2
  • Published inline: 1
  • Published summary-only: 0
  • Suppressed findings: 1
  • Verification verdict: REQUEST_CHANGES

Limitations

  • Verification is limited to the semantic review of the markdown documentation changes.

  • No runtime code execution paths were traced as the changes are purely instructional for a Claude skill.

  • Model: gemini-3.1-pro-preview

  • Omitted files: 0

Generated at 2026-03-18T18:17:19.232Z.

Comment on lines +45 to +56
## Execution Rules

Follow these rules in order:

1. Validate bootstrap inputs first.
2. If `KONTEXT_SERVICE_ACCOUNT_CLIENT_ID` or `KONTEXT_SERVICE_ACCOUNT_CLIENT_SECRET` is missing, stop immediately and tell the user exactly which variable is missing.
3. If `KONTEXT_OUTPUT_ENV_FILE` is missing, infer a conventional ignored local env target when the repo already clearly uses one, for example `apps/web/.env.local`, and pass it inline when running the helper.
4. If the user names an existing integration such as `google-workspace`, treat that as **reuse and attach first**. Do not try to invent provider templates or broad management recipes unless the user explicitly asked for integration creation details.
5. Run the bundled helper from the installed skill directly. Do not vendor or copy the helper into the target repo.
6. Keep repo exploration narrow until bootstrap is done or blocked. Do not pivot into unrelated examples, demos, or legacy integrations just because they mention the same provider.
7. After bootstrap succeeds, edit the target runtime surface and remove the hardcoded credential path.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW][docs] Architectural finding

Impact: Can reduce maintainability or correctness if left unresolved.
Suggested fix: Apply a targeted refactor and add/adjust tests where needed.
Evidence: The PR introduces a new 'Execution Rules' section that explicitly defines the order of operations, such as validating inputs first, stopping if service account credentials are missing, inferring the output env file, and reusing existing integrations.
Evidence citations:

  • .claude/skills/kontext-token-mode-bootstrap/SKILL.md:45:56

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

Copy link

@kontext-review kontext-review bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Kontext Review: REQUEST_CHANGES (1 findings, Advisory)

Verification certificate:

  • Verdict: REQUEST_CHANGES
  • Depth: lite
  • Confidence: 0.75
  • Certificate valid: yes
  • Scenarios traced: 1
  • Verification bounds:
    • Changed files and explicitly traced call paths

Inline findings posted:

  • [LOW][maintainability] Architectural finding (.claude/skills/kontext-token-mode-bootstrap/scripts/bootstrap-token-mode.mjs:446)

Reviewer summary:

  • Renamed integration to provider across the bootstrap script and documentation.
  • Added support for preset and custom provider types.
  • Updated SKILL.md to reflect the new provider terminology and environment variables.
Scores and diagnostics
  • Architecture: 7.0/10
  • Organization: 7.0/10
  • Cleanliness: 7.0/10
  • Overall: 7.0/10
  • Candidate findings: 1
  • Published inline: 1
  • Published summary-only: 0
  • Suppressed findings: 0
  • Verification verdict: REQUEST_CHANGES

Limitations

  • The diff for bootstrap-token-mode.mjs is truncated, so the mapping of environment variables to buildDesiredProvider arguments could not be fully verified.

  • Model: gemini-3.1-pro-preview

  • Omitted files: 0

Generated at 2026-03-18T18:36:37.112Z.

scopes: oauthScopes,
}),
serverToken: resolvedServerToken,
return {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW][maintainability] Architectural finding

Impact: Can reduce maintainability or correctness if left unresolved.
Suggested fix: Apply a targeted refactor and add/adjust tests where needed.
Evidence: The indentation in buildDesiredProvider for the create object is slightly misaligned, but it is valid JavaScript.
Evidence citations:

  • .claude/skills/kontext-token-mode-bootstrap/scripts/bootstrap-token-mode.mjs:446

@michiosw michiosw changed the title Feat/kontext token mode bootstrap skill feat: add token-mode bootstrap skill Mar 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant