diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f37145..587d240 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,9 +62,11 @@ jobs: cache: true - name: golangci-lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v9 with: - version: latest + # @note pinned: v1 releases are built with Go 1.24 and refuse this + # module's Go 1.25 target, and `latest` has broken the job before + version: v2.13.2 validate-examples: name: Validate Examples @@ -105,5 +107,5 @@ jobs: - name: Run acceptance tests env: - CHATBOTKIT_API_KEY: ${{ secrets.CHATBOTKIT_API_KEY }} + CHATBOTKIT_API_TOKEN: ${{ secrets.CHATBOTKIT_API_KEY }} run: go test -v ./internal/provider/ -run "^TestAcc" diff --git a/.golangci.yml b/.golangci.yml index 9177d6c..b3e59d6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,7 @@ # golangci-lint configuration -# https://golangci-lint.run/usage/configuration/ +# https://golangci-lint.run/docs/configuration/ + +version: "2" run: timeout: 5m @@ -8,19 +10,29 @@ run: linters: enable: - errcheck - - gosimple - govet - ineffassign - staticcheck - unused -linters-settings: - errcheck: - # Check for ignored error returns in tests too - check-blank: false - gosimple: - # S1039: unnecessary use of fmt.Sprintf - checks: ["all"] + settings: + errcheck: + # Check for ignored error returns in tests too + check-blank: false + staticcheck: + # @note v2 folds gosimple (S1*) and stylecheck (ST1*) into staticcheck. + # Keep what v1 ran here: staticcheck and every gosimple check, without + # the style checks that were never enabled. + checks: ["SA*", "S1*"] + + # @note v1 applied these exclusions by default, which is why idioms such as + # `defer resp.Body.Close()` passed. v2 makes them opt-in presets. + exclusions: + presets: + - comments + - common-false-positives + - legacy + - std-error-handling issues: # Don't limit the number of issues diff --git a/CHANGELOG.md b/CHANGELOG.md index a62ff4d..11cd7dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,43 @@ All notable changes to the ChatBotKit Terraform Provider are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +## [1.10.0] - 2026-09-18 + +### Added + +- `api_token` is the new name for the provider credential. `api_key` is + deprecated and still read when `api_token` is not set. The token is read + from `CHATBOTKIT_API_TOKEN` (or `CBK_API_TOKEN`); the older + `CHATBOTKIT_API_SECRET` and `CHATBOTKIT_API_KEY` names, and their `CBK_` + forms, still work. `CBK_API_URL` is accepted as a shorthand for + `CHATBOTKIT_API_URL`. The run-as user is also read from + `CHATBOTKIT_API_RUNAS_USERID` (or `CBK_API_RUNAS_USERID`), the names the CLI + uses; `CHATBOTKIT_RUN_AS` still works and gains a `CBK_RUN_AS` shorthand. +- The provider reads the platform origin from the `CHATBOTKIT_API_URL` + environment variable when `base_url` is not set, and derives the GraphQL + endpoint from it (`http://localhost:3000` becomes + `http://localhost:3000/api/v1/graphql`). This is the same variable the SDKs' + CLI reads, so one setting points every tool at a self-hosted platform. Plain + `http` works for local use, and `base_url` still wins when configured. + +### Changed + +- **BREAKING (behaviour):** `chatbotkit_instagram_integration`, `chatbotkit_messenger_integration` and `chatbotkit_whatsapp_integration` gain an `app_secret` attribute (sensitive; reads are masked so the configured value is kept), and on update a missing `access_token` / `app_secret` is now sent as an explicit `null`, which clears the credential on the platform. Previously an omitted credential was silently kept - configurations that relied on that must set the value explicitly. Requires the platform release that passes credential nulls through the GraphQL update mutations. +- `chatbotkit_skillset_ability` import now takes `/`; a bare ability id is rejected with a clear message. Reads paginate the skillset's abilities instead of stopping at the first 100. +- **BREAKING (API wire format):** the `chatbotkit_skillset_ability` resource + now sends and reads the renamed platform link fields `linkedSecretId` / + `linkedFileId` / `linkedBotId` / `linkedSpaceId` (previously `secretId` / + `fileId` / `botId` / `spaceId`) on create/update/read, and the GraphQL + `Ability` relations `linkedSecret` / `linkedFile` / `linkedBot` / + `linkedSpace` (previously `secret` / `file` / `bot` / `space`). The HCL + attribute names `secret_id`, `bot_id`, `file_id` and `space_id` are + unchanged, so existing configurations need no edits. There are no + compatibility aliases on the platform side; upgrade the provider together + with the platform deploy. A link removed outside Terraform is now cleared to + `null` in state on read. + ## [1.9.0] - 2026-06-30 ### Added diff --git a/README.md b/README.md index ec81d60..9dc4575 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ -[![ChatBotKit](https://img.shields.io/badge/credits-ChatBotKit-blue.svg)](https://chatbotkit.com) [![CBK.AI](https://img.shields.io/badge/credits-CBK.AI-blue.svg)](https://cbk.ai) -[![Email](https://img.shields.io/badge/Email-Support-blue?logo=mail.ru)](mailto:support@chatbotkit.com) +[![Email](https://img.shields.io/badge/Email-Support-blue?logo=mail.ru)](mailto:support@cbk.ai) [![Discord](https://img.shields.io/badge/Discord-Support-blue?logo=discord)](https://go.cbk.ai/discord) [![Terraform Registry](https://img.shields.io/badge/Terraform-Registry-purple.svg)](https://registry.terraform.io/providers/chatbotkit/chatbotkit/latest) [![Follow on Twitter](https://img.shields.io/twitter/follow/chatbotkit.svg?logo=twitter)](https://twitter.com/chatbotkit) @@ -73,10 +72,10 @@ provider_installation { } ``` -### 2. Set API Key +### 2. Set API Token ```bash -export CHATBOTKIT_API_KEY="your-api-key" +export CHATBOTKIT_API_TOKEN="your-api-token" ``` ### 3. Test with Example Configuration @@ -94,8 +93,8 @@ terraform apply # Run unit tests go test -v ./internal/provider/ -run "^Test[^Acc]" -# Run acceptance tests (requires CHATBOTKIT_API_KEY) -CHATBOTKIT_API_KEY=your-api-key go test -v ./internal/provider/ -run "^TestAcc" +# Run acceptance tests (requires CHATBOTKIT_API_TOKEN) +CHATBOTKIT_API_TOKEN=your-api-token go test -v ./internal/provider/ -run "^TestAcc" ``` ## Directory Structure @@ -185,7 +184,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # Create a new bot diff --git a/VERSION b/VERSION index f8e233b..81c871d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.9.0 +1.10.0 diff --git a/docs/data-sources/dataset.md b/docs/data-sources/dataset.md index 4a8edd7..176c708 100644 --- a/docs/data-sources/dataset.md +++ b/docs/data-sources/dataset.md @@ -88,7 +88,6 @@ The following attributes are exported: - `search_min_score` - Minimum similarity score for search results. - `reranker` - The reranking model used. - `separators` - Custom separators for text chunking. -- `store` - The storage backend used. - `visibility` - The visibility setting of the dataset. - `meta` - A map of metadata key-value pairs. - `created_at` - The timestamp when the dataset was created. diff --git a/docs/index.md b/docs/index.md index 9c51cad..7636c4e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,7 +21,7 @@ terraform { } provider "chatbotkit" { - api_key = var.chatbotkit_api_key + api_token = var.chatbotkit_api_token } # Create a knowledge base dataset @@ -50,7 +50,7 @@ resource "chatbotkit_bot" "assistant" { ## Authentication -The ChatBotKit provider requires an API key for authentication. You can obtain an API key from the [ChatBotKit Dashboard](https://chatbotkit.com). +The ChatBotKit provider requires a API token for authentication. You can obtain a API token from the [ChatBotKit Dashboard](https://chatbotkit.com). ### Configuration Options @@ -59,13 +59,13 @@ You can configure authentication in two ways: 1. **Provider Configuration** (recommended for variables): ```terraform provider "chatbotkit" { - api_key = var.chatbotkit_api_key + api_token = var.chatbotkit_api_token } ``` 2. **Environment Variable**: ```bash - export CHATBOTKIT_API_KEY="your-api-key" + export CHATBOTKIT_API_TOKEN="your-api-token" ``` When both are set, the provider configuration takes precedence. @@ -74,18 +74,19 @@ When both are set, the provider configuration takes precedence. ### Optional -- `api_key` (String, Sensitive) - The API key for authenticating with the ChatBotKit API. Can also be set via the `CHATBOTKIT_API_KEY` environment variable. -- `base_url` (String) - Custom API endpoint URL. Defaults to `https://api.chatbotkit.com/graphql`. This is typically only needed for testing or enterprise deployments. -- `run_as` (String) - The ID of a sub-account (partner user) to operate on behalf of. When set, requests include the `X-RunAs-UserId` header, so a single `api_key` (a partner/master token) can manage many sub-accounts — configure one provider alias per sub-account. Can also be set via the `CHATBOTKIT_RUN_AS` environment variable. +- `api_token` (String, Sensitive) - The API token for authenticating with the ChatBotKit API. Can also be set via the `CHATBOTKIT_API_TOKEN` environment variable, or `CBK_API_TOKEN` for short. +- `api_key` (String, Sensitive, Deprecated) - The former name of `api_token`. It is used when `api_token` is not set. +- `base_url` (String) - The GraphQL endpoint URL. Defaults to `https://api.chatbotkit.com/graphql`. For a self-hosted platform use its GraphQL endpoint, e.g. `http://localhost:3000/api/v1/graphql`, or set the platform origin in the `CHATBOTKIT_API_URL` environment variable. Plain `http` works for local use. +- `run_as` (String) - The ID of a child User to operate on behalf of. When set, requests include the `X-RunAs-UserId` header, so one `api_token` belonging to the parent User can manage many child Users. Configure one provider alias per child User. Can also be set via the `CHATBOTKIT_API_RUNAS_USERID` environment variable (or `CBK_API_RUNAS_USERID` for short); `CHATBOTKIT_RUN_AS` and `CBK_RUN_AS` are still read. -### Operating on sub-accounts (multi-tenancy) +### Operating on child Users (multi-tenancy) -A partner/master token combined with `run_as` lets one configuration manage many isolated sub-accounts — the standard Terraform multi-account pattern (provider aliases, like the AWS provider's `assume_role`): +A API token belonging to a parent User, combined with `run_as`, lets one configuration manage many isolated child Users. This follows the standard Terraform multi-account pattern of provider aliases, similar to the AWS provider's `assume_role`: ```hcl provider "chatbotkit" { alias = "acme" - run_as = var.acme_account_id # api_key from CHATBOTKIT_API_KEY + run_as = var.acme_account_id # api_token from CHATBOTKIT_API_TOKEN } provider "chatbotkit" { diff --git a/docs/resources/context.md b/docs/resources/context.md index d7c2890..97ecba7 100644 --- a/docs/resources/context.md +++ b/docs/resources/context.md @@ -7,7 +7,7 @@ description: |- # chatbotkit_context (Resource) -Manages a ChatBotKit Context. A context binds the current account (or, under `run_as`, a partner sub-account) to a set of platform resources — a blueprint, bot, dataset, or skillset — along with a free-form payload. It is commonly used to scope a sub-account to a pre-defined configuration, or to attach per-account data (such as a repository or project id) that an agent reads at runtime. +Manages a ChatBotKit Context. A context binds the current User, or the child User selected by `run_as`, to a set of platform resources such as a blueprint, bot, dataset, or skillset, along with a free-form payload. It is commonly used to scope a User to a predefined configuration or attach per-User data, such as a repository or project ID, that an agent reads at runtime. ## Example Usage @@ -47,11 +47,11 @@ resource "chatbotkit_context" "onboarding" { } ``` -### Per-sub-account context (with run_as) +### Per-User context (with run_as) ```terraform -# A provider alias whose run_as targets a partner sub-account; the context is -# created inside that sub-account. +# A provider alias whose run_as targets a child User. The context is +# created for that User. provider "chatbotkit" { alias = "customer" run_as = var.customer_account_id diff --git a/docs/resources/dataset.md b/docs/resources/dataset.md index 6b7d044..ddf769a 100644 --- a/docs/resources/dataset.md +++ b/docs/resources/dataset.md @@ -84,7 +84,6 @@ The following arguments are supported: - `search_min_score` - (Optional) Minimum similarity score (0-1) for search results to be included. - `reranker` - (Optional) The reranking model to use for improving search relevance. - `separators` - (Optional) Custom separators for text chunking. -- `store` - (Optional) The storage backend to use. - `visibility` - (Optional) The visibility level of the dataset. Can be "private" or "public". - `meta` - (Optional) A map of metadata key-value pairs. diff --git a/docs/resources/instagram_integration.md b/docs/resources/instagram_integration.md index e87410d..d5fb899 100644 --- a/docs/resources/instagram_integration.md +++ b/docs/resources/instagram_integration.md @@ -37,6 +37,7 @@ resource "chatbotkit_instagram_integration" "advanced" { bot_id = chatbotkit_bot.assistant.id access_token = var.instagram_access_token + app_secret = var.instagram_app_secret session_duration = 3600000 # 1 hour in milliseconds contact_collection = true @@ -51,7 +52,8 @@ The following arguments are supported: - `name` - (Optional) The name of the integration. This is displayed in the ChatBotKit dashboard. - `description` - (Optional) A description of the integration's purpose. - `bot_id` - (Optional) The ID of the ChatBotKit bot to connect. -- `access_token` - (Optional, Sensitive) The Instagram access token used to send and receive messages. +- `access_token` - (Optional, Sensitive) The Instagram access token used to send and receive messages. Removing `access_token` from your configuration sends an explicit null on update, which clears the access token on the platform. +- `app_secret` - (Optional, Sensitive) The Instagram app secret used to verify webhook signatures. The API never returns the configured value (it is masked on read), so the value from your configuration is kept in state. Removing `app_secret` from your configuration sends an explicit null on update, which clears the app secret on the platform. - `attachments` - (Optional) Whether to enable file attachments. - `session_duration` - (Optional) The duration of a conversation session in milliseconds. - `contact_collection` - (Optional) Whether to collect contact information from users. diff --git a/docs/resources/messenger_integration.md b/docs/resources/messenger_integration.md index efc86a9..a5b8a62 100644 --- a/docs/resources/messenger_integration.md +++ b/docs/resources/messenger_integration.md @@ -37,6 +37,7 @@ resource "chatbotkit_messenger_integration" "advanced" { bot_id = chatbotkit_bot.assistant.id access_token = var.messenger_access_token + app_secret = var.messenger_app_secret session_duration = 3600000 # 1 hour in milliseconds attachments = true } @@ -66,7 +67,8 @@ The following arguments are supported: - `name` - (Optional) The name of the integration. This is displayed in the ChatBotKit dashboard. - `description` - (Optional) A description of the integration's purpose. - `bot_id` - (Optional) The ID of the ChatBotKit bot to connect. -- `access_token` - (Optional, Sensitive) The Facebook Messenger page access token. +- `access_token` - (Optional, Sensitive) The Facebook Messenger page access token. Removing `access_token` from your configuration sends an explicit null on update, which clears the page access token on the platform. +- `app_secret` - (Optional, Sensitive) The Facebook Messenger app secret used to verify webhook signatures. The API never returns the configured value (it is masked on read), so the value from your configuration is kept in state. Removing `app_secret` from your configuration sends an explicit null on update, which clears the app secret on the platform. - `session_duration` - (Optional) The duration of a conversation session in milliseconds. - `attachments` - (Optional) Whether to enable file attachments in conversations. - `blueprint_id` - (Optional) The ID of a blueprint to associate with this integration. diff --git a/docs/resources/skillset_ability.md b/docs/resources/skillset_ability.md index 302395a..09ebd4e 100644 --- a/docs/resources/skillset_ability.md +++ b/docs/resources/skillset_ability.md @@ -118,8 +118,10 @@ In addition to all arguments above, the following attributes are exported: ## Import -Skillset abilities can be imported using their ID: +Skillset abilities can only be looked up through their skillset, so the import ID must combine both IDs as `/`: ```bash -terraform import chatbotkit_skillset_ability.example ability_abc123def456 +terraform import chatbotkit_skillset_ability.example skillset_abc123def456/ability_abc123def456 ``` + +Importing with a bare ability ID is rejected with an error explaining the expected format. diff --git a/docs/resources/whatsapp_integration.md b/docs/resources/whatsapp_integration.md index 3471ab0..721cc48 100644 --- a/docs/resources/whatsapp_integration.md +++ b/docs/resources/whatsapp_integration.md @@ -38,6 +38,7 @@ resource "chatbotkit_whatsapp_integration" "advanced" { bot_id = chatbotkit_bot.assistant.id access_token = var.whatsapp_access_token + app_secret = var.whatsapp_app_secret phone_number_id = var.whatsapp_phone_number_id session_duration = 3600000 # 1 hour in milliseconds contact_collection = true @@ -70,7 +71,8 @@ The following arguments are supported: - `name` - (Optional) The name of the integration. This is displayed in the ChatBotKit dashboard. - `description` - (Optional) A description of the integration's purpose. - `bot_id` - (Optional) The ID of the ChatBotKit bot to connect. -- `access_token` - (Optional, Sensitive) The WhatsApp Business API access token. +- `access_token` - (Optional, Sensitive) The WhatsApp Business API access token. Removing `access_token` from your configuration sends an explicit null on update, which clears the access token on the platform. +- `app_secret` - (Optional, Sensitive) The WhatsApp Business app secret used to verify webhook signatures. The API never returns the configured value (it is masked on read), so the value from your configuration is kept in state. Removing `app_secret` from your configuration sends an explicit null on update, which clears the app secret on the platform. - `phone_number_id` - (Optional) The WhatsApp Business phone number ID. - `session_duration` - (Optional) The duration of a conversation session in milliseconds. - `contact_collection` - (Optional) Whether to collect contact information from users. diff --git a/examples/README.md b/examples/README.md index 7d5bdbf..7a0fad4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -22,9 +22,9 @@ These examples demonstrate production-ready architectures based on ChatBotKit bl | [soc-investigator](./soc-investigator/) | An autonomous security-operations agent that runs on a cycle: pull SIEM alerts, correlate into cases, triage, investigate, enrich, and accumulate knowledge — showing the deterministic-scripts vs agentic-skills split | File-based skills (`SKILL.md`) + stdlib scripts, `pack/shell` + space skills, Workspace case store + knowledge base, Correlation/dedup, Scheduled cycle triggers, Human approval gate | | [community-scout](./community-scout/) | A product-led-growth agent that monitors Reddit on a cycle for threads where the product genuinely helps, scores relevance, drafts a disclosed reply, and suggests it to the team on Slack — it has no Reddit-post tool, so a human posts | Read-only Reddit (`pack/reddit[read-only]`), Slack hand-off (`slack/conversation/start`), File-based skills + dedup script, Workspace mention store, Scheduled cycle triggers, Structural human-in-the-loop gate | | [internal-services-fetch](./internal-services-fetch/) | An agent that reaches internal corporate services with the `fetch` action, authenticated by a shared service token (machine-to-machine) and a personal OAuth secret (acting on behalf of the signed-in employee) | Fetch action, Shared bearer secret, Personal OAuth secret, `${SECRET_DEFAULT}` vs named references, Both secrets in one request, Slack integration | -| [multi-tenant-agents-shared](./multi-tenant-agents-shared/) | The same agent deployed into each customer's own sub-account, from one module. One master token + `run_as` per provider alias | Sub-accounts (partner users), `run_as` (X-RunAs-UserId), Provider aliases, Shared module, Per-tenant isolation | -| [multi-tenant-agents-per-customer](./multi-tenant-agents-per-customer/) | A bespoke agent per customer (each its own module/folder), composed into one shared state and deployed in a single apply. One master token + `run_as` per provider alias | Sub-accounts (partner users), `run_as` (X-RunAs-UserId), Provider aliases, Per-customer modules, Shared state | -| [code-foundry](./code-foundry/) | An autonomous code foundry on a multi-account architecture: a shared "tools" account (GitHub token-minter, an exported `global-coding-tools` skillset, design/coding spaces) that per-user sub-accounts install cross-account; each user's Coding Agent has a heartbeat and a per-user context (repo + Vercel) | Multi-account (`run_as` + provider aliases), Shared account + reusable per-user module, Cross-account skillset install (`@shared@global-coding-tools`), GitHub App JWT secret, `bot/apply` token minting, Per-user context, Heartbeat | +| [multi-tenant-agents-shared](./multi-tenant-agents-shared/) | The same agent deployed into each customer's child User from one module. One parent User API token + `run_as` per provider alias | Child Users, `run_as` (X-RunAs-UserId), Provider aliases, Shared module, Per-tenant isolation | +| [multi-tenant-agents-per-customer](./multi-tenant-agents-per-customer/) | A bespoke agent per customer, each in its own module or folder, composed into one shared state and deployed in a single apply. One parent User API token + `run_as` per provider alias | Child Users, `run_as` (X-RunAs-UserId), Provider aliases, Per-customer modules, Shared state | +| [code-foundry](./code-foundry/) | An autonomous code foundry with a multi-User architecture: child Users install tools from a shared tools User, and each child User's Coding Agent has a heartbeat and per-User context for its repository and Vercel project | Multi-User (`run_as` + provider aliases), Shared User + reusable child-User module, Cross-User skillset install (`@shared@global-coding-tools`), GitHub App JWT secret, `bot/apply` token minting, Per-User context, Heartbeat | | [dual-agent-programmable-workflows](./dual-agent-programmable-workflows/) | Two-agent architecture for workflow programming and execution | Multi-agent collaboration, Shared resources, Asymmetric access patterns, Scheduled triggers | | [system-diagnostics-agent](./system-diagnostics-agent/) | Self-monitoring agent that reports on its own capabilities | Self-introspection, Blueprint resource discovery, Scheduled diagnostics, Automated reporting | | [second-brain](./second-brain/) | Personal knowledge management system with Notion and Calendar | Persistent workspace, Notion integration, Google Calendar, Telegram bot, Dynamic skillsets | @@ -40,14 +40,14 @@ These examples demonstrate production-ready architectures based on ChatBotKit bl ### Prerequisites 1. [Terraform](https://www.terraform.io/downloads.html) >= 1.0 -2. A ChatBotKit account and API key from [chatbotkit.com](https://chatbotkit.com) +2. A ChatBotKit account and API token from [chatbotkit.com](https://chatbotkit.com) ### Quick Start -1. Set your ChatBotKit API key: +1. Set your ChatBotKit API token: ```bash -export CHATBOTKIT_API_KEY="your-api-key" +export CHATBOTKIT_API_TOKEN="your-api-token" ``` 2. Choose an example and navigate to its directory: @@ -158,32 +158,32 @@ A product-led-growth agent that runs on a cycle to watch public conversations (R ### Multi-Tenant Agents Examples A pair of examples deploying a separate, isolated agent for each customer in that -customer's own ChatBotKit sub-account (a "partner user"). Both use **one** master -token plus the provider's `run_as` attribute (the `X-RunAs-UserId` header) to -target each customer's sub-account — no per-customer tokens, no `for_each`. +customer's own child User. Both use **one** API token belonging to the parent User +plus the provider's `run_as` attribute (the `X-RunAs-UserId` header) to target +each customer's child User. No per-customer tokens or `for_each` are required. - **[multi-tenant-agents-shared](./multi-tenant-agents-shared/)** — the same agent for every customer, from one module, via provider aliases. - **[multi-tenant-agents-per-customer](./multi-tenant-agents-per-customer/)** — a bespoke agent per customer, each in its own folder. **What you'll learn:** -- The platform's sub-account (partner user) model for multi-tenant SaaS -- Using one master token + `run_as` to operate on many sub-accounts (like the AWS provider's `assume_role`) +- The platform's parent/child User model for multi-tenant SaaS +- Using one parent User API token with `run_as` to operate on many child Users, similar to the AWS provider's `assume_role` - Provider aliases composing one reused module (shared) vs. a distinct module per customer (per-customer) -- Per-customer isolation via separate sub-accounts (`run_as`), all from a single shared state and one apply — no `for_each` +- Per-customer isolation via separate child Users (`run_as`), all from a single shared state and one apply without `for_each` **Use when:** You are building a multi-tenant product where each customer needs their own isolated agent and resources, not a shared account. -### Code Foundry Example (multi-account) -An autonomous code foundry on a multi-account architecture. A single shared "tools" account holds the expensive, sensitive machinery once — a GitHub bot that mints repository-scoped App tokens (JWT secret), a Coding Tools skillset exported account-wide as `global-coding-tools`, shared Design/Coding spaces, and a Designs Manager with a Sync trigger. Each user gets a thin, isolated sub-account whose Coding Agent installs the shared toolset cross-account (`@shared@global-coding-tools`) and works on that user's own repo. +### Code Foundry Example (multi-User) +An autonomous code foundry with a multi-User architecture. A shared tools User holds the expensive, sensitive machinery once: a GitHub bot that mints repository-scoped App tokens, a Coding Tools skillset exposed to child Users as `global-coding-tools`, shared Design and Coding spaces, and a Designs Manager with a Sync trigger. Each customer gets a thin, isolated child User whose Coding Agent installs the shared toolset across Users (`@shared@global-coding-tools`) and works on that customer's repository. **What you'll learn:** -- A shared-account + per-user-sub-account architecture on one partner token, using provider aliases + `run_as` (building on the multi-tenant examples) -- Exporting a skillset account-wide (`visibility = protected` + alias) and installing it cross-account from a sub-account with `conversation/skillset/install` and an `@shared@` reference -- Minting repository-scoped GitHub App tokens without putting the App key in each sub-account: the agent `bot/apply`s a shared GitHub bot that holds the JWT secret -- Driving "which repo" from a per-user **context** rather than hard-coding it (a security boundary, since agents belong to users) — set via the partner-user context API, now exposed via GraphQL (and a future native `chatbotkit_context` resource) +- A shared User plus child-User architecture using one parent User API token, provider aliases, and `run_as` +- Exposing a skillset to child Users (`visibility = protected` plus an alias) and installing it with `conversation/skillset/install` and an `@shared@` reference +- Minting repository-scoped GitHub App tokens without putting the App key in each child User: the agent `bot/apply`s a shared GitHub bot that holds the JWT secret +- Selecting the repository through per-User **context** rather than hard-coding it, using the User context API exposed through GraphQL and the native `chatbotkit_context` resource - A **heartbeat** trigger that keeps a coding agent making the next step on a long-running task across ticks -**Use when:** You are productising an autonomous agent to many users and want the heavy/sensitive tooling defined once and borrowed cross-account, with each user isolated and scoped to their own repo by context. +**Use when:** You are productising an autonomous agent for many customers and want the heavy or sensitive tooling defined once and borrowed across Users, with each child User isolated and scoped to its own repository by context. ### Dual-Agent Programmable Workflows Example Two-agent architecture where a Workflow Architect programs custom scripts and a Task Runner executes them. @@ -422,12 +422,12 @@ terraform init # Re-initialize to download the provider ### "Invalid API key" ```bash -# Verify your API key is set -echo $CHATBOTKIT_API_KEY +# Verify your API token is set +echo $CHATBOTKIT_API_TOKEN # Or configure directly in the provider block provider "chatbotkit" { - api_key = "your-api-key" + api_token = "your-api-token" } ``` diff --git a/examples/agent-framework/README.md b/examples/agent-framework/README.md index a48aeed..542310f 100644 --- a/examples/agent-framework/README.md +++ b/examples/agent-framework/README.md @@ -110,10 +110,10 @@ the integration (and its variables) and supply credentials to activate them. ## Usage -1. Set your ChatBotKit API key: +1. Set your ChatBotKit API token: ```bash - export CHATBOTKIT_API_KEY="your-api-key" + export CHATBOTKIT_API_TOKEN="your-api-token" ``` 2. (Optional) Provide Slack credentials to activate the channel: diff --git a/examples/agent-framework/main.tf b/examples/agent-framework/main.tf index e370840..18b644c 100644 --- a/examples/agent-framework/main.tf +++ b/examples/agent-framework/main.tf @@ -22,7 +22,7 @@ # the live agent — the files are the source of truth. # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable # - (Optional) Provide Slack credentials via variables to activate the channel terraform { @@ -34,7 +34,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/examples/ai-employee/README.md b/examples/ai-employee/README.md index 3727674..89f3131 100644 --- a/examples/ai-employee/README.md +++ b/examples/ai-employee/README.md @@ -106,10 +106,10 @@ The AI Employee is a digital team member engineered to operate within profession ## Usage -1. Set your ChatBotKit API key: +1. Set your ChatBotKit API token: ```bash -export CHATBOTKIT_API_KEY="your-api-key" +export CHATBOTKIT_API_TOKEN="your-api-token" ``` 2. Configure OAuth2 secrets (done via ChatBotKit platform): diff --git a/examples/ai-employee/main.tf b/examples/ai-employee/main.tf index c337c09..f911cbd 100644 --- a/examples/ai-employee/main.tf +++ b/examples/ai-employee/main.tf @@ -13,7 +13,7 @@ # - Notion integration for knowledge management # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable # - Configure Google Mail OAuth2 credentials (platform/google/mail) # - Configure Notion OAuth2 credentials (platform/notion) @@ -26,7 +26,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/examples/basic/main.tf b/examples/basic/main.tf index 427f34b..b83b187 100644 --- a/examples/basic/main.tf +++ b/examples/basic/main.tf @@ -7,7 +7,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # Example: Create a new bot diff --git a/examples/code-foundry/README.md b/examples/code-foundry/README.md index 7366033..a0d7e7c 100644 --- a/examples/code-foundry/README.md +++ b/examples/code-foundry/README.md @@ -1,20 +1,19 @@ -# Code Foundry (multi-account) +# Code Foundry (multi-User) -An autonomous code foundry built on a **multi-account architecture**: one -**shared "tools" account** holds the expensive, sensitive machinery once, and each -**user gets a thin, isolated sub-account** with a Coding Agent that borrows the -shared tools cross-account and works on that user's own repository. +An autonomous code foundry built on a **multi-User architecture**: one shared +tools User holds the expensive, sensitive machinery once, and each customer +gets a thin, isolated child User with a Coding Agent that borrows those tools +across Users and works on that customer's repository. -This is the pattern for productising agents to many users without duplicating the -toolbox per user and without putting credentials (a GitHub App key) in each -sub-account. +This pattern supports productising agents for many customers without duplicating +the toolbox or putting credentials such as a GitHub App key in each child User. ## Architecture ``` - shared account (partner-user alias: "shared") per-user sub-accounts + shared User (alias: "shared") child Users ┌──────────────────────────────────────────┐ ┌───────────────────────────┐ - │ GitHub bot — mints repo-scoped tokens │ install │ alice/ │ + │ GitHub bot - mints repo-scoped tokens │ install │ alice/ │ │ (github/repository/token/create + JWT) │◀────────│ Coding Agent │ │ Coding Tools skillset │ @shared@│ + install shared tools │ │ = global-coding-tools (protected) │ global- │ + heartbeat │ @@ -25,21 +24,23 @@ sub-account. └──────────────────────────────────────────┘ ``` -One partner/master token (`CHATBOTKIT_API_KEY`) operates on every account; each is -selected with a provider alias + `run_as` (the `X-RunAs-UserId` header) — the same -multi-account mechanism as the [`multi-tenant-agents-shared`](../multi-tenant-agents-shared) +One API token belonging to the parent User (`CHATBOTKIT_API_TOKEN`) operates on every +child User. Each is selected with a provider alias and `run_as` (the +`X-RunAs-UserId` header), using the same multi-User mechanism as the +[`multi-tenant-agents-shared`](../multi-tenant-agents-shared) example. ## The two halves -- **`modules/shared`** — the shared account. A GitHub bot that mints +- **`modules/shared`** - the shared User. A GitHub bot that mints repository-scoped GitHub App tokens (JWT secret), a **Coding Tools** skillset - exported account-wide as `global-coding-tools` (`visibility = protected` + a + exposed to child Users as `global-coding-tools` (`visibility = protected` plus a stable `alias`), shared Design and Coding spaces, and a Designs Manager bot with - a Sync trigger. The shared account's partner user **must have the alias `shared`**. -- **`modules/coder`** — one per user. A Coding Agent whose only built-in ability is - to install the shared toolset cross-account (`conversation/skillset/install` with - `@shared@global-coding-tools`). Plus a **heartbeat** and the user's **context**. + a Sync trigger. The shared User **must have the alias `shared`**. +- **`modules/coder`** - one per child User. A Coding Agent whose only built-in ability is + to install the shared toolset across Users (`conversation/skillset/install` with + `@shared@global-coding-tools`), plus a **heartbeat** and the child User's + **context**. ## How a token gets minted (and why context matters) @@ -48,48 +49,48 @@ The coding agent never holds the GitHub App key. To touch a repo it calls bot signs an App JWT (from the shared secret) and returns a short-lived, repository-scoped token. -But _which_ repository? It can't be hard-coded — each agent belongs to a different -user, and they may interact with the agent, so a hard-coded repo would be a -security risk. The repo comes from the sub-user's **context** (`githubOwner` / -`githubRepo` / `vercelProjectId`). That's why setting context per sub-user is part +But _which_ repository? It cannot be hard-coded because each agent belongs to a +different child User. The repository comes from the User's **context** +(`githubOwner` / `githubRepo` / `vercelProjectId`). That is why setting context +per child User is part of the setup, and why the context API needed a programmatic surface (see below). ## The two gaps this example closes -1. **Context via GraphQL → a native Terraform resource.** The partner-user context - API (`/api/v1/partner/user/{userId}/context/...`) was REST-only. It is now also +1. **Context via GraphQL to a native Terraform resource.** The User context + API (`/api/v1/user/{userId}/context/...`) was REST-only. It is now also exposed via GraphQL (`contexts` query; `createContext` / `updateContext` / - `deleteContext` mutations, scoped to the run_as'd sub-user), and the Terraform - provider has been regenerated from the updated schema — so context is a native - **`chatbotkit_context`** resource. The coder module uses it directly; created in - the user's sub-account (the module's provider is run_as'd to it), so each agent is - scoped to its own repo with no hard-coding. + `deleteContext` mutations, scoped to the selected child User), and the + Terraform provider has been regenerated from the updated schema. Context is + therefore a native **`chatbotkit_context`** resource. The coder module creates + it for the child User selected by its provider, so each agent is scoped to its + own repository without hard-coding. 2. **A coding-agent heartbeat.** Coding tasks span many steps. Each coder - sub-account has a recurring heartbeat trigger that nudges the agent to make the + child User has a recurring heartbeat trigger that nudges the agent to make the next concrete step on its active task, reusing one conversation within the session window so it keeps its place across ticks. ## Files ``` -main.tf provider aliases (shared + per-user) + module calls -modules/shared/main.tf the shared "tools" account (GitHub, Coding Tools, spaces, Designs Manager) -modules/coder/main.tf one user's coding agent + install-shared-tools + heartbeat + context -terraform.tfvars.example account IDs, GitHub App id/key, git email +main.tf provider aliases (shared + per-User) + module calls +modules/shared/main.tf the shared tools User (GitHub, Coding Tools, spaces, Designs Manager) +modules/coder/main.tf one child User's coding agent + shared tools + heartbeat + context +terraform.tfvars.example User IDs, GitHub App ID/key, git email ``` ## Usage ```bash -export CHATBOTKIT_API_KEY="" +export CHATBOTKIT_API_TOKEN="" export TF_VAR_github_app_private_key="$(cat github-app.pem)" -cp terraform.tfvars.example terraform.tfvars # fill in account IDs + app id +cp terraform.tfvars.example terraform.tfvars # fill in User IDs + App ID terraform init terraform apply ``` -Accounts (partner users) are created out of band; the shared one must be aliased -`shared`. To add a user: add an account-id variable, a provider alias, and a +Child Users are created out of band; the shared User must be aliased `shared`. +To add a customer: add a User ID variable, a provider alias, and a `./modules/coder` call (mirroring `alice`/`bob`). ## Notes and seams @@ -100,12 +101,12 @@ Accounts (partner users) are created out of band; the shared one must be aliased shape differs. - **Context resource.** `chatbotkit_context` is generated from the GraphQL schema; its `payload` is `map(string)`, so values (repo owner/name, repo URL, Vercel - project id) are flat strings. Regenerate the provider (the GraphQL → stubs + project ID) are flat strings. Regenerate the provider (the GraphQL-to-stubs pipeline) if you change the schema again. - **Designs Manager** Sync trigger is `schedule = "never"` (run on demand) and syncs an upstream design repo into the shared Design space. ## Related examples -- [`multi-tenant-agents-shared`](../multi-tenant-agents-shared) — the provider-alias + `run_as` multi-account pattern this builds on. -- [`agent-framework`](../agent-framework) — the single agent as a project of files. +- [`multi-tenant-agents-shared`](../multi-tenant-agents-shared) - the provider-alias plus `run_as` multi-User pattern this builds on. +- [`agent-framework`](../agent-framework) - the single agent as a project of files. diff --git a/examples/code-foundry/main.tf b/examples/code-foundry/main.tf index b7a0167..a3c543d 100644 --- a/examples/code-foundry/main.tf +++ b/examples/code-foundry/main.tf @@ -1,17 +1,17 @@ -# Code Foundry — a multi-account architecture. +# Code Foundry: a multi-User architecture. # -# One SHARED "tools" account holds the expensive, sensitive machinery once: a +# One shared tools User holds the expensive, sensitive machinery once: a # GitHub bot that mints repository-scoped App tokens, a Coding Tools skillset -# exported account-wide as `global-coding-tools`, shared Design/Coding spaces, and -# a Designs Manager that keeps designs in sync. Each USER gets a thin, isolated -# sub-account with just a Coding Agent that installs the shared toolset -# cross-account (`@shared@global-coding-tools`) and works on that user's repo. +# exposed to child Users as `global-coding-tools`, shared Design/Coding spaces, and +# a Designs Manager that keeps designs in sync. Each customer gets a thin, +# isolated child User with a Coding Agent that installs the shared toolset +# across Users (`@shared@global-coding-tools`) and works on that customer's repo. # -# Everything runs on one partner/master token (CHATBOTKIT_API_KEY). Each account -# is selected with a provider alias + `run_as` (the X-RunAs-UserId header) — the -# same multi-account pattern as the multi-tenant examples. +# Everything runs on one parent User API token (CHATBOTKIT_API_TOKEN). Each child User +# is selected with a provider alias and `run_as` (the X-RunAs-UserId header), +# using the same multi-User pattern as the multi-tenant examples. # -# shared account (alias "shared") per-user sub-accounts +# shared User (alias "shared") child Users # ┌───────────────────────────┐ ┌─────────────────────────┐ # │ GitHub bot (mint tokens) │ install│ alice: Coding Agent │ # │ Coding Tools │──────────▶ + heartbeat + context │ @@ -20,8 +20,8 @@ # │ Designs Manager + Sync │ └─────────────────────────┘ # └───────────────────────────┘ # -# IMPORTANT: the shared account's partner user MUST have the alias `shared`, since -# sub-accounts reference its skillset as `@shared@global-coding-tools`. +# IMPORTANT: the shared User MUST have the alias `shared`, since child Users +# reference its skillset as `@shared@global-coding-tools`. terraform { required_providers { @@ -32,7 +32,7 @@ terraform { } variable "shared_account_id" { - description = "The shared 'tools' account (partner user) ID — must have alias `shared`" + description = "The shared tools User ID; it must have alias `shared`" type = string } @@ -53,16 +53,16 @@ variable "git_email" { } variable "alice_account_id" { - description = "Alice's coding sub-account (partner user) ID" + description = "Alice's child User ID" type = string } variable "bob_account_id" { - description = "Bob's coding sub-account (partner user) ID" + description = "Bob's child User ID" type = string } -# One partner/master token via CHATBOTKIT_API_KEY; each alias selects an account. +# One parent User API token via CHATBOTKIT_API_TOKEN; each alias selects a User. provider "chatbotkit" { alias = "shared" @@ -80,7 +80,7 @@ provider "chatbotkit" { } # ============================================================================ -# the shared toolbox (one account) +# the shared toolbox (one User) # ============================================================================ module "shared" { @@ -92,13 +92,13 @@ module "shared" { } # ============================================================================ -# per-user coding agents (one sub-account each, same module) +# per-customer coding agents (one child User each, same module) # ============================================================================ # Each installs `@shared@global-coding-tools` at runtime, so depend on the shared -# account being provisioned first. +# shared User being provisioned first. # -# @note repo_name (and vercel_project_id) are UUID-ish here so each user's project -# is unique and non-guessable — they are placeholders for real identifiers. In a +# @note repo_name (and vercel_project_id) are UUID-ish here so each customer's project +# is unique and non-guessable. They are placeholders for real identifiers. In a # fuller setup Terraform could PROVISION these too and feed the results straight in, # e.g. the `integrations/github` provider to create the repo and the `vercel` # provider to create the project: @@ -145,12 +145,12 @@ output "shared_github_bot_id" { } output "coding_tools_alias" { - description = "Cross-account reference for the exported coding toolset" + description = "Cross-User reference for the shared coding toolset" value = module.shared.coding_tools_alias } output "coding_agents" { - description = "Coding agent bot IDs per user sub-account" + description = "Coding agent bot IDs per child User" value = { alice = module.alice.bot_id bob = module.bob.bot_id diff --git a/examples/code-foundry/modules/coder/main.tf b/examples/code-foundry/modules/coder/main.tf index 02b0933..a0c3b16 100644 --- a/examples/code-foundry/modules/coder/main.tf +++ b/examples/code-foundry/modules/coder/main.tf @@ -1,16 +1,16 @@ -# A per-user coding sub-account. +# A per-customer child User. # -# Each user gets their own isolated sub-account containing just a Coding Agent -# and a single ability that installs the shared toolset cross-account. The heavy -# tooling lives once in the `shared` account; this module borrows it. +# Each customer gets an isolated child User containing a Coding Agent and one +# ability that installs the shared toolset across Users. The heavy tooling lives +# once in the shared User; this module borrows it. # -# Applied via a provider alias whose `run_as` targets this user's sub-account. +# Applied through a provider alias whose `run_as` targets this child User. # # Two things make the setup work end to end: # 1. install the shared coding skillset (`@shared@global-coding-tools`) -# 2. set this user's CONTEXT (which repo + which Vercel project) so the shared -# GitHub bot knows which repository to mint a token for — hard-coding the -# repo would be a security risk because each agent belongs to a user. +# 2. set this child User's CONTEXT (which repo + which Vercel project) so the shared +# GitHub bot knows which repository to mint a token for; hard-coding the +# repo would be a security risk because each agent belongs to a child User. terraform { required_providers { @@ -21,22 +21,22 @@ terraform { } variable "user_name" { - description = "Display name of the user this sub-account belongs to" + description = "Display name of the customer represented by this child User" type = string } variable "repo_owner" { - description = "The GitHub repository owner for this user's project" + description = "The GitHub repository owner for this child User's project" type = string } variable "repo_name" { - description = "The GitHub repository name for this user's project" + description = "The GitHub repository name for this child User's project" type = string } variable "vercel_project_id" { - description = "The Vercel project ID this user's agent deploys to" + description = "The Vercel project ID this child User's agent deploys to" type = string default = "" } @@ -73,9 +73,9 @@ resource "chatbotkit_skillset" "coder_tools" { description = "Bootstrap toolset: installs the shared coding skillset on demand" } -# The only ability the agent ships with: install the shared toolset cross-account. +# The only ability the agent ships with: install the shared toolset across Users. # `@shared@global-coding-tools` resolves to the Coding Tools skillset in the -# account aliased `shared`. +# User aliased `shared`. resource "chatbotkit_skillset_ability" "install_coding_skillset" { skillset_id = chatbotkit_skillset.coder_tools.id name = "Install Coding Skillset" @@ -98,7 +98,7 @@ resource "chatbotkit_bot" "coder" { You must install the relevant skills / tools to obtain specific platform capabilities. For access to the repo use the github tools to mint a token and get the repo access. - The repository and Vercel project for this account come from your context — do + The repository and Vercel project for this User come from your context. Do not assume or hard-code them. Then you must use the provided shell environment to perform the actions. @@ -111,7 +111,7 @@ resource "chatbotkit_bot" "coder" { } # ============================================================================ -# heartbeat — keep working the active task +# heartbeat - keep working the active task # ============================================================================ # Coding tasks span many steps. This recurring tick nudges the agent to make the # next concrete step on whatever it is currently working on, reusing one @@ -122,7 +122,7 @@ resource "chatbotkit_trigger_integration" "heartbeat" { description = <<-EOT Continue the current coding task. Review your workspace and the repo for in-progress work and make the next concrete step toward completion (commit and - push as you go). If there is no active task, stop and wait — do not invent work. + push as you go). If there is no active task, stop and wait. Do not invent work. EOT bot_id = chatbotkit_bot.coder.id schedule = var.heartbeat_schedule @@ -131,15 +131,15 @@ resource "chatbotkit_trigger_integration" "heartbeat" { } # ============================================================================ -# context — which repo + Vercel project this user's agent works on +# context - which repo and Vercel project this child User's agent works on # ============================================================================ # The shared GitHub bot reads this to know which repository to scope a token to. -# Created in THIS sub-account (the module's provider is run_as'd to it), so each -# agent is scoped to its own repo — never hard-coded. payload is map(string). +# Created for this child User through the module's provider, so each agent is +# scoped to its own repository. The payload is map(string). resource "chatbotkit_context" "project" { name = "project" - description = "Repository and Vercel project for this user's coding agent" + description = "Repository and Vercel project for this child User's coding agent" payload = { githubOwner = var.repo_owner @@ -154,6 +154,6 @@ resource "chatbotkit_context" "project" { # ============================================================================ output "bot_id" { - description = "The coding agent bot ID in this user's sub-account" + description = "The coding agent bot ID in this child User" value = chatbotkit_bot.coder.id } diff --git a/examples/code-foundry/modules/shared/main.tf b/examples/code-foundry/modules/shared/main.tf index 4f96249..d9d9ad1 100644 --- a/examples/code-foundry/modules/shared/main.tf +++ b/examples/code-foundry/modules/shared/main.tf @@ -1,16 +1,16 @@ -# The SHARED "tools" account. +# The shared tools User. # -# This is the master toolbox every coding sub-account borrows from. It holds: +# This is the toolbox every child User borrows from. It holds: # - a GitHub bot that mints short-lived, repository-scoped GitHub App tokens -# - a "Coding Tools" skillset, exported account-wide as `global-coding-tools` -# (visibility = protected, alias = global-coding-tools) so sub-accounts can -# install it cross-account with `@shared@global-coding-tools` +# - a "Coding Tools" skillset exposed as `global-coding-tools` +# (visibility = protected, alias = global-coding-tools) so child Users can +# install it across Users with `@shared@global-coding-tools` # - shared Design and Coding spaces (design files + coding skills) # - a Designs Manager bot + Sync trigger that keep the Design space in sync # # This module is applied via a provider alias whose `run_as` targets the shared -# account. That account MUST have the alias `shared` (set on the partner user), -# because sub-accounts reference its skillset as `@shared@global-coding-tools`. +# User. That User MUST have the alias `shared`, because child Users reference +# its skillset as `@shared@global-coding-tools`. terraform { required_providers { @@ -32,7 +32,7 @@ variable "github_app_private_key" { } # ============================================================================ -# GitHub — the bot that mints repository-scoped tokens +# GitHub - the bot that mints repository-scoped tokens # ============================================================================ resource "chatbotkit_secret" "github_app" { @@ -84,16 +84,16 @@ resource "chatbotkit_bot" "github" { } # ============================================================================ -# Coding Tools — the exported, account-wide skillset (global-coding-tools) +# Coding Tools - the skillset exposed to child Users (global-coding-tools) # ============================================================================ -# This is what sub-accounts install. visibility=protected + a stable alias make -# it referenceable cross-account as `@shared@global-coding-tools`. +# This is what child Users install. visibility=protected plus a stable alias make +# it referenceable across Users as `@shared@global-coding-tools`. resource "chatbotkit_skillset" "coding_tools" { name = "Coding Tools" visibility = "protected" alias = "global-coding-tools" - description = "The shared coding toolset borrowed by every coding sub-account" + description = "The shared coding toolset borrowed by every child User" } # Shell tools (ephemeral per-conversation sandbox). @@ -119,7 +119,7 @@ resource "chatbotkit_skillset_ability" "import_url" { } # Mint a repo token by delegating to the GitHub bot (which reads the caller's -# context to learn which repository this is about — see the README). +# context to learn which repository this is about; see the README). resource "chatbotkit_skillset_ability" "mint_github_repo_token" { skillset_id = chatbotkit_skillset.coding_tools.id bot_id = chatbotkit_bot.github.id @@ -137,7 +137,7 @@ resource "chatbotkit_skillset_ability" "mint_github_repo_token" { } # ============================================================================ -# Design space — shared design system files +# Design space - shared design system files # ============================================================================ resource "chatbotkit_space" "design" { @@ -162,7 +162,7 @@ resource "chatbotkit_skillset_ability" "read_design_file" { } # ============================================================================ -# Coding space — shared coding skills (how-to playbooks) +# Coding space - shared coding skills (how-to playbooks) # ============================================================================ resource "chatbotkit_space" "coding" { @@ -187,7 +187,7 @@ resource "chatbotkit_skillset_ability" "read_coding_skills" { } # ============================================================================ -# Designs Manager — keeps the Design space in sync with upstream +# Designs Manager - keeps the Design space in sync with upstream # ============================================================================ resource "chatbotkit_skillset" "designs_manager_tools" { @@ -232,7 +232,7 @@ output "github_bot_id" { } output "coding_tools_alias" { - description = "The cross-account reference for the exported coding toolset" + description = "The cross-User reference for the shared coding toolset" value = "@shared@${chatbotkit_skillset.coding_tools.alias}" } diff --git a/examples/code-foundry/terraform.tfvars.example b/examples/code-foundry/terraform.tfvars.example index 58e4f81..74fd7f7 100644 --- a/examples/code-foundry/terraform.tfvars.example +++ b/examples/code-foundry/terraform.tfvars.example @@ -1,10 +1,10 @@ -# Copy to terraform.tfvars and fill in. The partner/master token comes from the -# CHATBOTKIT_API_KEY environment variable (not a variable here). +# Copy to terraform.tfvars and fill in. The parent User API token comes from the +# CHATBOTKIT_API_TOKEN environment variable (not a variable here). # -# Accounts (partner users) are created out of band (dashboard or partner API). -# The shared account MUST have the alias `shared`. +# Child Users are created out of band through the dashboard or User API. +# The shared User MUST have the alias `shared`. -shared_account_id = "user_xxxxxxxxxxxx" # the shared "tools" account (alias: shared) +shared_account_id = "user_xxxxxxxxxxxx" # the shared tools User (alias: shared) alice_account_id = "user_aaaaaaaaaaaa" bob_account_id = "user_bbbbbbbbbbbb" diff --git a/examples/community-scout/README.md b/examples/community-scout/README.md index a9b7c04..b5e7f73 100644 --- a/examples/community-scout/README.md +++ b/examples/community-scout/README.md @@ -102,7 +102,7 @@ agent/skills/learn/ SKILL.md (judgmen ## Usage ```bash -export CHATBOTKIT_API_KEY="..." +export CHATBOTKIT_API_TOKEN="..." terraform apply \ -var="slack_bot_token=xoxb-..." \ -var="slack_signing_secret=..." diff --git a/examples/community-scout/main.tf b/examples/community-scout/main.tf index 8c5d75b..fd45d6d 100644 --- a/examples/community-scout/main.tf +++ b/examples/community-scout/main.tf @@ -27,7 +27,7 @@ # heartbeat.md -> the cycle tick (the trigger description) # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable # - Provide Slack credentials (variables below) so the scout can suggest to the team terraform { @@ -39,7 +39,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/examples/complete/README.md b/examples/complete/README.md index 3a4c4d4..bb8bba4 100644 --- a/examples/complete/README.md +++ b/examples/complete/README.md @@ -48,21 +48,21 @@ This example demonstrates a complete ChatBotKit setup using Terraform, including ## Prerequisites 1. [Terraform](https://www.terraform.io/downloads.html) >= 1.0 -2. A ChatBotKit account and API key +2. A ChatBotKit account and API token ## Usage -1. Set your ChatBotKit API key: +1. Set your ChatBotKit API token: ```bash -export CHATBOTKIT_API_KEY="your-api-key" +export CHATBOTKIT_API_TOKEN="your-api-token" ``` Or configure it directly in `main.tf`: ```hcl provider "chatbotkit" { - api_key = "your-api-key" + api_token = "your-api-token" } ``` diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 5b411bc..374d08f 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -7,7 +7,7 @@ # - A trigger integration linked to the bot # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable or configure api_key below +# - Set the CHATBOTKIT_API_TOKEN environment variable or configure api_token below terraform { required_providers { @@ -18,7 +18,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/examples/content-upload/main.tf b/examples/content-upload/main.tf index d1fb4ac..2afa385 100644 --- a/examples/content-upload/main.tf +++ b/examples/content-upload/main.tf @@ -7,7 +7,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # --- File content ----------------------------------------------------------- diff --git a/examples/deep-researcher/README.md b/examples/deep-researcher/README.md index 589b5d2..82cfa9c 100644 --- a/examples/deep-researcher/README.md +++ b/examples/deep-researcher/README.md @@ -92,7 +92,7 @@ the platform catalogue.) ## Usage ```bash -export CHATBOTKIT_API_KEY="..." +export CHATBOTKIT_API_TOKEN="..." terraform init terraform apply ``` diff --git a/examples/deep-researcher/main.tf b/examples/deep-researcher/main.tf index 493fbc4..2f7fd82 100644 --- a/examples/deep-researcher/main.tf +++ b/examples/deep-researcher/main.tf @@ -35,7 +35,7 @@ # files are the source of truth. # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable terraform { required_providers { @@ -46,7 +46,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/examples/dual-agent-programmable-workflows/main.tf b/examples/dual-agent-programmable-workflows/main.tf index fd6a25a..32acbec 100644 --- a/examples/dual-agent-programmable-workflows/main.tf +++ b/examples/dual-agent-programmable-workflows/main.tf @@ -12,7 +12,7 @@ # - Scheduled trigger for automated task execution # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable terraform { required_providers { @@ -23,7 +23,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/examples/dynamic-mcp-search-and-install/README.md b/examples/dynamic-mcp-search-and-install/README.md index 7be96f6..87bf2fb 100644 --- a/examples/dynamic-mcp-search-and-install/README.md +++ b/examples/dynamic-mcp-search-and-install/README.md @@ -80,9 +80,9 @@ Uses the `conversation/mcp/install[url]` template to activate an MCP server by i ## Usage -1. Set your ChatBotKit API key: +1. Set your ChatBotKit API token: ```bash -export CHATBOTKIT_API_KEY="your-api-key" +export CHATBOTKIT_API_TOKEN="your-api-token" ``` 2. Initialize Terraform: diff --git a/examples/dynamic-mcp-search-and-install/main.tf b/examples/dynamic-mcp-search-and-install/main.tf index a564663..8fa49de 100644 --- a/examples/dynamic-mcp-search-and-install/main.tf +++ b/examples/dynamic-mcp-search-and-install/main.tf @@ -11,7 +11,7 @@ # - Self-extending system that grows functionality as needed # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable terraform { required_providers { @@ -22,7 +22,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/examples/internal-services-fetch/README.md b/examples/internal-services-fetch/README.md index ff8a8a2..f30e328 100644 --- a/examples/internal-services-fetch/README.md +++ b/examples/internal-services-fetch/README.md @@ -67,7 +67,7 @@ in `X-Forwarded-Authorization` — both supplied as bearer values by the secrets ## Usage ```bash -export CHATBOTKIT_API_KEY="sk-...your-api-key..." +export CHATBOTKIT_API_TOKEN="sk-...your-api-token..." cp terraform.tfvars.example terraform.tfvars # fill in the secret values terraform init diff --git a/examples/internal-services-fetch/main.tf b/examples/internal-services-fetch/main.tf index 1793a3f..a4fbe39 100644 --- a/examples/internal-services-fetch/main.tf +++ b/examples/internal-services-fetch/main.tf @@ -39,7 +39,7 @@ # prefix or you would end up with `Bearer Bearer `. # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable # - Provide the secret/OAuth values in terraform.tfvars (see the .example file) terraform { @@ -51,7 +51,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/examples/internal-services-fetch/terraform.tfvars.example b/examples/internal-services-fetch/terraform.tfvars.example index c77e4a6..2183512 100644 --- a/examples/internal-services-fetch/terraform.tfvars.example +++ b/examples/internal-services-fetch/terraform.tfvars.example @@ -1,8 +1,8 @@ # Secret values for the internal-services-fetch example. # -# The ChatBotKit API key itself is supplied separately via the environment: +# The ChatBotKit API token itself is supplied separately via the environment: # -# export CHATBOTKIT_API_KEY="sk-...your-api-key..." +# export CHATBOTKIT_API_TOKEN="sk-...your-api-token..." # # Copy this file to terraform.tfvars and fill in the real values. Do not commit # terraform.tfvars — it contains secrets. diff --git a/examples/mcp-factory/README.md b/examples/mcp-factory/README.md index 8521fea..ad717ba 100644 --- a/examples/mcp-factory/README.md +++ b/examples/mcp-factory/README.md @@ -101,9 +101,9 @@ This blueprint demonstrates a factory-style architecture for an MCP server that ## Usage -1. Set your ChatBotKit API key: +1. Set your ChatBotKit API token: ```bash -export CHATBOTKIT_API_KEY="your-api-key" +export CHATBOTKIT_API_TOKEN="your-api-token" ``` 2. Initialize Terraform: diff --git a/examples/mcp-factory/main.tf b/examples/mcp-factory/main.tf index 90deca3..61242ab 100644 --- a/examples/mcp-factory/main.tf +++ b/examples/mcp-factory/main.tf @@ -12,7 +12,7 @@ # - Clear separation of concerns # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable terraform { required_providers { @@ -23,7 +23,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/examples/multi-tenant-agents-per-customer/README.md b/examples/multi-tenant-agents-per-customer/README.md index 8fd534f..88c1a60 100644 --- a/examples/multi-tenant-agents-per-customer/README.md +++ b/examples/multi-tenant-agents-per-customer/README.md @@ -1,8 +1,8 @@ -# Multi-Tenant Agents — a different agent per customer +# Multi-Tenant Agents - a different agent per customer Deploy a **bespoke** agent for each customer, each isolated in that customer's own -ChatBotKit **sub-account** (a "partner user"). Each customer is its own folder/module -with a hand-written agent, and a single shared root composes them all — so one +ChatBotKit **child User**. Each customer is its own folder/module +with a hand-written agent, and a single shared root composes them all, so one `terraform apply` deploys every customer's agent, in one shared state. For the **same** agent across all customers, see the sibling example @@ -15,32 +15,33 @@ multi-tenant-agents-per-customer/ └── globex/main.tf # Globex's bespoke agent (research + sandbox) ``` -`acme` and `globex` are deliberately different shapes — that is the whole point. -Each is a module the root wires to that customer's sub-account. +`acme` and `globex` are deliberately different shapes. Each is a module the root +wires to that customer's child User. -## How it works: one token + `run_as` +## How it works: one API token plus `run_as` -You hold **one** partner/master token (set via `CHATBOTKIT_API_KEY`). The root -configures one provider alias per customer, each with `run_as = var._account_id` -(the `X-RunAs-UserId` header), and passes that alias to the customer's module — so -each bespoke agent is created in the right sub-account. No per-customer tokens, no +You hold **one** API token belonging to the parent User (set via +`CHATBOTKIT_API_TOKEN`). The root configures one provider alias per customer, each +with `run_as = var._account_id` (the `X-RunAs-UserId` header), and +passes that alias to the customer's module, so +each bespoke agent is created in the right child User. No per-customer tokens, no `for_each`. This mirrors the AWS provider's `assume_role` multi-account pattern. -> Where do account IDs come from? Create a sub-account per customer in the -> dashboard, or via the partner API (`partner/user/create`). Account IDs are not -> secret; the single master token is. +> Where do User IDs come from? Create a child User per customer in the +> dashboard or through the User API (`user/create`). User IDs are not +> secret; the parent User API token is. ## Usage ```bash -export CHATBOTKIT_API_KEY="sk-...partner-token..." # one master token +export CHATBOTKIT_API_TOKEN="sk-...parent-user-api-token..." # one parent User API token -cp terraform.tfvars.example terraform.tfvars # fill in account IDs +cp terraform.tfvars.example terraform.tfvars # fill in User IDs terraform init terraform apply ``` -One apply deploys every customer's bespoke agent into their own sub-account; bot +One apply deploys every customer's bespoke agent into their own child User; bot IDs are in the `bots` output, and everything lives in one shared state. ## Adding a customer @@ -58,5 +59,5 @@ customer should get the **same** agent you improve in one place, use ## Cleanup -`terraform destroy` removes the agents; it does **not** delete the sub-accounts — -remove those via the partner API (`partner/user/{id}/delete`). +`terraform destroy` removes the agents; it does **not** delete the child Users. +Remove those through the User API (`user/{id}/delete`). diff --git a/examples/multi-tenant-agents-per-customer/acme/main.tf b/examples/multi-tenant-agents-per-customer/acme/main.tf index b4fda4a..b7a15b3 100644 --- a/examples/multi-tenant-agents-per-customer/acme/main.tf +++ b/examples/multi-tenant-agents-per-customer/acme/main.tf @@ -1,8 +1,8 @@ # Acme's bespoke agent (a module). # # This folder holds Acme's own agent definition. It is composed into the shared -# root (../main.tf), which wires it to Acme's sub-account via a provider alias + -# run_as — so this module just declares resources and lets the root pick the +# root (../main.tf), which wires it to Acme's child User via a provider alias and +# run_as, so this module just declares resources and lets the root pick the # account. Acme wants a customer-support agent; Globex's folder is different. terraform { diff --git a/examples/multi-tenant-agents-per-customer/globex/main.tf b/examples/multi-tenant-agents-per-customer/globex/main.tf index 4a5c103..952b42a 100644 --- a/examples/multi-tenant-agents-per-customer/globex/main.tf +++ b/examples/multi-tenant-agents-per-customer/globex/main.tf @@ -1,8 +1,8 @@ # Globex's bespoke agent (a module). # # Composed into the shared root (../main.tf), which wires it to Globex's -# sub-account via a provider alias + run_as. Globex wants a research agent with a -# sandbox — a completely different shape from Acme's support bot. Each customer +# child User via a provider alias and run_as. Globex wants a research agent with +# a sandbox, a completely different shape from Acme's support bot. Each customer # folder evolves independently. terraform { diff --git a/examples/multi-tenant-agents-per-customer/main.tf b/examples/multi-tenant-agents-per-customer/main.tf index 295d858..516070e 100644 --- a/examples/multi-tenant-agents-per-customer/main.tf +++ b/examples/multi-tenant-agents-per-customer/main.tf @@ -1,11 +1,11 @@ -# Architecture 1 — a DIFFERENT agent per customer, in one shared state. +# Architecture 1: a different agent per customer in one shared state. # # Each customer is its own folder/module (./acme, ./globex) with a bespoke agent. # This root composes them all into a single state and deploys them in one apply, -# wiring each module to its customer's sub-account with a provider alias + run_as. +# wiring each module to its customer's child User with a provider alias and run_as. # -# One partner/master token (set via CHATBOTKIT_API_KEY) operates on every -# sub-account; run_as selects which one (the X-RunAs-UserId header). No +# One parent User API token (set via CHATBOTKIT_API_TOKEN) operates on every +# child User; run_as selects which one (the X-RunAs-UserId header). No # per-customer tokens, no for_each. # # To add a customer: add a folder/module, an account-id variable, a provider @@ -20,17 +20,17 @@ terraform { } variable "acme_account_id" { - description = "Acme's sub-account (partner user) ID" + description = "Acme's child User ID" type = string } variable "globex_account_id" { - description = "Globex's sub-account (partner user) ID" + description = "Globex's child User ID" type = string } -# api_key comes from CHATBOTKIT_API_KEY (one partner/master token). Each alias -# selects which sub-account to operate on. +# api_token comes from CHATBOTKIT_API_TOKEN (one parent User API token). Each alias +# selects which child User to operate on. provider "chatbotkit" { alias = "acme" run_as = var.acme_account_id @@ -41,7 +41,7 @@ provider "chatbotkit" { run_as = var.globex_account_id } -# Each customer's bespoke agent, deployed into their sub-account. +# Each customer's bespoke agent, deployed into their child User. module "acme" { source = "./acme" providers = { chatbotkit = chatbotkit.acme } @@ -53,7 +53,7 @@ module "globex" { } output "bots" { - description = "Bot IDs per customer sub-account" + description = "Bot IDs per customer child User" value = { acme = module.acme.bot_id globex = module.globex.bot_id diff --git a/examples/multi-tenant-agents-per-customer/terraform.tfvars.example b/examples/multi-tenant-agents-per-customer/terraform.tfvars.example index 77fc458..f85b3f2 100644 --- a/examples/multi-tenant-agents-per-customer/terraform.tfvars.example +++ b/examples/multi-tenant-agents-per-customer/terraform.tfvars.example @@ -1,12 +1,12 @@ -# Sub-account (partner user) IDs — one per customer. +# Child User IDs, one per customer. # -# These select which sub-account each customer's module deploys into, via run_as. -# They are account IDs, not secrets. The single partner/master token is supplied -# separately via the CHATBOTKIT_API_KEY environment variable: +# These select which child User each customer's module deploys into through run_as. +# They are User IDs, not secrets. The parent User API token is supplied +# separately via the CHATBOTKIT_API_TOKEN environment variable: # -# export CHATBOTKIT_API_KEY="sk-...your-partner-token..." +# export CHATBOTKIT_API_TOKEN="sk-...your-parent-user-api-token..." # -# Copy this file to terraform.tfvars and fill in the real account IDs. +# Copy this file to terraform.tfvars and fill in the real User IDs. -acme_account_id = "user_REPLACE-with-acme-sub-account-id" -globex_account_id = "user_REPLACE-with-globex-sub-account-id" +acme_account_id = "user_REPLACE-with-acme-user-id" +globex_account_id = "user_REPLACE-with-globex-user-id" diff --git a/examples/multi-tenant-agents-shared/README.md b/examples/multi-tenant-agents-shared/README.md index 51a5220..7adb84b 100644 --- a/examples/multi-tenant-agents-shared/README.md +++ b/examples/multi-tenant-agents-shared/README.md @@ -1,25 +1,25 @@ -# Multi-Tenant Agents — shared agent +# Multi-Tenant Agents - shared agent Deploy the **same** agent for every customer, each isolated in that customer's own -ChatBotKit **sub-account** (a "partner user"). The agent is defined once in -[`modules/agent`](./modules/agent) and rolled out to every sub-account, so you +ChatBotKit **child User**. The agent is defined once in +[`modules/agent`](./modules/agent) and rolled out to every child User, so you maintain it in one place and improvements ship to all customers. For a **different** agent per customer, see the sibling example [`../multi-tenant-agents-per-customer`](../multi-tenant-agents-per-customer). -## How it works: one token + `run_as` +## How it works: one API token plus `run_as` -You hold **one** partner/master token. The provider's `run_as` attribute selects -which sub-account to operate on (it sends the `X-RunAs-UserId` header), so the -same token manages every customer — no per-customer tokens. This is the standard -Terraform multi-account approach: **provider aliases**, one per account, just like -the AWS provider's `assume_role`. +You hold **one** API token belonging to the parent User. The provider's `run_as` +attribute selects which child User to operate on by sending the +`X-RunAs-UserId` header, so the same API token manages every customer. This uses +Terraform's standard multi-account approach: **provider aliases**, one per child +User, similar to the AWS provider's `assume_role`. ```hcl provider "chatbotkit" { alias = "acme" - run_as = var.acme_account_id # api_key comes from CHATBOTKIT_API_KEY + run_as = var.acme_account_id # api_token comes from CHATBOTKIT_API_TOKEN } module "acme" { @@ -29,24 +29,24 @@ module "acme" { } ``` -No `for_each` — one provider alias + one module call per customer. +No `for_each` is needed: use one provider alias and one module call per customer. -> Where do account IDs come from? Create a sub-account per customer in the -> dashboard, or via the partner API (`partner/user/create`) if you automate -> onboarding. That is a one-time step, separate from Terraform. Account IDs are -> not secret; the single master token is. +> Where do User IDs come from? Create a child User per customer in the dashboard +> or through the User API (`user/create`) if you automate onboarding. That is a +> one-time step separate from Terraform. User IDs are not secret; the parent +> User API token is. ## Usage ```bash -export CHATBOTKIT_API_KEY="sk-...your-partner-token..." # one master token +export CHATBOTKIT_API_TOKEN="sk-...your-parent-user-api-token..." # one parent User API token -cp terraform.tfvars.example terraform.tfvars # fill in account IDs +cp terraform.tfvars.example terraform.tfvars # fill in User IDs terraform init terraform apply ``` -A single apply deploys the agent into every customer's sub-account; bot IDs are +A single apply deploys the agent into every customer's child User; bot IDs are in the `bots` output. ## Adding a customer @@ -56,11 +56,11 @@ alias with `run_as`, and a `module ""` call wired to that alias. ## Trade-off -Centralized and uniform — great when every customer should get the same agent. +Centralized and uniform, which is useful when every customer should get the same agent. If customers need genuinely different agents that evolve independently, use [`../multi-tenant-agents-per-customer`](../multi-tenant-agents-per-customer). ## Cleanup -`terraform destroy` removes the agents; it does **not** delete the sub-accounts — -remove those via the partner API (`partner/user/{id}/delete`). +`terraform destroy` removes the agents; it does **not** delete the child Users. +Remove those through the User API (`user/{id}/delete`). diff --git a/examples/multi-tenant-agents-shared/main.tf b/examples/multi-tenant-agents-shared/main.tf index ec0bd15..06f043d 100644 --- a/examples/multi-tenant-agents-shared/main.tf +++ b/examples/multi-tenant-agents-shared/main.tf @@ -1,15 +1,15 @@ -# Architecture 2 — the SAME agent for every customer, via provider aliases + run_as. +# Architecture 2: the same agent for every customer via provider aliases and run_as. # -# One partner/master token (set once via CHATBOTKIT_API_KEY) operates on every -# sub-account; each provider alias selects a customer with `run_as = ` +# One parent User API token (set once via CHATBOTKIT_API_TOKEN) operates on every +# child User; each provider alias selects a customer with `run_as = ` # (the X-RunAs-UserId header). No per-customer tokens. Provider aliases are -# Terraform's native multi-account mechanism — the same way the AWS provider +# Terraform's native multi-account mechanism, similar to how the AWS provider # targets many accounts. # # Every customer is deployed from the SAME ./modules/agent module, so you maintain # the agent in one place and it ships to everyone. Put the per-customer account # IDs in terraform.tfvars (see terraform.tfvars.example), then a single -# `terraform apply` deploys the agent into every sub-account. +# `terraform apply` deploys the agent into every child User. # # To add a customer: add an account-id variable, a provider alias, and a module call. @@ -22,17 +22,17 @@ terraform { } variable "acme_account_id" { - description = "Acme's sub-account (partner user) ID" + description = "Acme's child User ID" type = string } variable "globex_account_id" { - description = "Globex's sub-account (partner user) ID" + description = "Globex's child User ID" type = string } -# api_key comes from CHATBOTKIT_API_KEY (one partner/master token). Each alias -# just selects which sub-account to operate on. +# api_token comes from CHATBOTKIT_API_TOKEN (one parent User API token). Each alias +# selects which child User to operate on. provider "chatbotkit" { alias = "acme" run_as = var.acme_account_id @@ -43,7 +43,7 @@ provider "chatbotkit" { run_as = var.globex_account_id } -# The same agent, deployed into each customer's sub-account. +# The same agent, deployed into each customer's child User. module "acme" { source = "./modules/agent" providers = { chatbotkit = chatbotkit.acme } @@ -59,7 +59,7 @@ module "globex" { } output "bots" { - description = "Bot IDs per customer sub-account" + description = "Bot IDs per customer child User" value = { acme = module.acme.bot_id globex = module.globex.bot_id diff --git a/examples/multi-tenant-agents-shared/modules/agent/main.tf b/examples/multi-tenant-agents-shared/modules/agent/main.tf index 1bda18e..2e66503 100644 --- a/examples/multi-tenant-agents-shared/modules/agent/main.tf +++ b/examples/multi-tenant-agents-shared/modules/agent/main.tf @@ -1,9 +1,9 @@ # The one canonical agent, shared by every customer. # # Instantiated once per customer by the per-tenant root (../../main.tf). The -# caller passes a chatbotkit provider authenticated with that customer's -# sub-account token, so every resource below is created inside the customer's -# isolated sub-account. Improve the agent here and it ships to all customers on +# caller passes a chatbotkit provider configured to run as that customer's +# child User, so every resource below is created inside the isolated User. +# Improve the agent here and it ships to all customers on # the next deploy. terraform { @@ -66,11 +66,11 @@ resource "chatbotkit_bot" "agent" { } output "bot_id" { - description = "The bot ID created in the customer's sub-account" + description = "The bot ID created in the customer's child User" value = chatbotkit_bot.agent.id } output "workspace_id" { - description = "The workspace ID created in the customer's sub-account" + description = "The workspace ID created in the customer's child User" value = chatbotkit_space.workspace.id } diff --git a/examples/multi-tenant-agents-shared/terraform.tfvars.example b/examples/multi-tenant-agents-shared/terraform.tfvars.example index b429b2b..a03e729 100644 --- a/examples/multi-tenant-agents-shared/terraform.tfvars.example +++ b/examples/multi-tenant-agents-shared/terraform.tfvars.example @@ -1,12 +1,12 @@ -# Sub-account (partner user) IDs — one per customer. +# Child User IDs, one per customer. # -# These select which sub-account each provider alias operates on via run_as. -# They are account IDs, not secrets. The single partner/master token is supplied -# separately via the CHATBOTKIT_API_KEY environment variable: +# These select which child User each provider alias operates on via run_as. +# They are User IDs, not secrets. The parent User API token is supplied +# separately via the CHATBOTKIT_API_TOKEN environment variable: # -# export CHATBOTKIT_API_KEY="sk-...your-partner-token..." +# export CHATBOTKIT_API_TOKEN="sk-...your-parent-user-api-token..." # -# Copy this file to terraform.tfvars and fill in the real account IDs. +# Copy this file to terraform.tfvars and fill in the real User IDs. -acme_account_id = "user_REPLACE-with-acme-sub-account-id" -globex_account_id = "user_REPLACE-with-globex-sub-account-id" +acme_account_id = "user_REPLACE-with-acme-user-id" +globex_account_id = "user_REPLACE-with-globex-user-id" diff --git a/examples/scheduled-task/README.md b/examples/scheduled-task/README.md index 91660b5..e66b1e2 100644 --- a/examples/scheduled-task/README.md +++ b/examples/scheduled-task/README.md @@ -21,7 +21,7 @@ automation along with the bot it drives. ## Usage ```bash -export CHATBOTKIT_API_KEY="your-api-key" +export CHATBOTKIT_API_TOKEN="your-api-token" terraform init terraform plan diff --git a/examples/scheduled-task/main.tf b/examples/scheduled-task/main.tf index 2ede196..fbc691c 100644 --- a/examples/scheduled-task/main.tf +++ b/examples/scheduled-task/main.tf @@ -7,7 +7,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set the CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set the CHATBOTKIT_API_TOKEN env var } # ---------------------------------------------------------------------------- diff --git a/examples/second-brain/main.tf b/examples/second-brain/main.tf index b5839ba..d128314 100644 --- a/examples/second-brain/main.tf +++ b/examples/second-brain/main.tf @@ -13,7 +13,7 @@ # - Telegram integration for mobile access # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable # - Configure Notion OAuth2 (platform secret: notion) # - Configure Google Calendar OAuth2 (platform secret: google/calendar) # - Configure Telegram bot token for the integration @@ -27,7 +27,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ @@ -47,7 +47,7 @@ resource "chatbotkit_space" "mind" { resource "chatbotkit_secret" "notion" { name = "Notion API Key" - description = "The API key for accessing Notion." + description = "The API token for accessing Notion." type = "template" kind = "personal" diff --git a/examples/simple-self-improving-agent/README.md b/examples/simple-self-improving-agent/README.md index c5450a7..4814312 100644 --- a/examples/simple-self-improving-agent/README.md +++ b/examples/simple-self-improving-agent/README.md @@ -61,9 +61,9 @@ The backstory file follows a comprehensive framework: ## Usage -1. Set your ChatBotKit API key: +1. Set your ChatBotKit API token: ```bash -export CHATBOTKIT_API_KEY="your-api-key" +export CHATBOTKIT_API_TOKEN="your-api-token" ``` 2. Initialize Terraform: diff --git a/examples/simple-self-improving-agent/main.tf b/examples/simple-self-improving-agent/main.tf index cf1c328..ccdd366 100644 --- a/examples/simple-self-improving-agent/main.tf +++ b/examples/simple-self-improving-agent/main.tf @@ -11,7 +11,7 @@ # - Continuous learning and adaptation # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable terraform { required_providers { @@ -22,7 +22,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/examples/skillset-based-dynamic-skill/README.md b/examples/skillset-based-dynamic-skill/README.md index e2e2921..b1702f0 100644 --- a/examples/skillset-based-dynamic-skill/README.md +++ b/examples/skillset-based-dynamic-skill/README.md @@ -68,10 +68,10 @@ This convention allows the agent to: ## Usage -1. Set your ChatBotKit API key: +1. Set your ChatBotKit API token: ```bash -export CHATBOTKIT_API_KEY="your-api-key" +export CHATBOTKIT_API_TOKEN="your-api-token" ``` 2. Initialize Terraform: diff --git a/examples/skillset-based-dynamic-skill/main.tf b/examples/skillset-based-dynamic-skill/main.tf index 5cb507b..5ded105 100644 --- a/examples/skillset-based-dynamic-skill/main.tf +++ b/examples/skillset-based-dynamic-skill/main.tf @@ -11,7 +11,7 @@ # - Dynamic skill loading at runtime # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable terraform { required_providers { @@ -22,7 +22,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/examples/soc-investigator/README.md b/examples/soc-investigator/README.md index 69f8378..625cb02 100644 --- a/examples/soc-investigator/README.md +++ b/examples/soc-investigator/README.md @@ -83,7 +83,7 @@ agent/skills/extract-knowledge/ SKILL.md (judgme ## Usage ```bash -export CHATBOTKIT_API_KEY="..." +export CHATBOTKIT_API_TOKEN="..." terraform init terraform apply ``` diff --git a/examples/soc-investigator/main.tf b/examples/soc-investigator/main.tf index 50ee7c2..46f85b0 100644 --- a/examples/soc-investigator/main.tf +++ b/examples/soc-investigator/main.tf @@ -24,7 +24,7 @@ # README (real SIEM, a SIRP case DB, a dataset-backed knowledge base). # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable terraform { required_providers { @@ -35,7 +35,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/examples/system-diagnostics-agent/main.tf b/examples/system-diagnostics-agent/main.tf index 2681569..c8136b3 100644 --- a/examples/system-diagnostics-agent/main.tf +++ b/examples/system-diagnostics-agent/main.tf @@ -12,7 +12,7 @@ # - Scheduled trigger for automated diagnostics # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable terraform { required_providers { @@ -23,7 +23,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/examples/two-sided-marketplace/README.md b/examples/two-sided-marketplace/README.md index 86ab5f0..16536fe 100644 --- a/examples/two-sided-marketplace/README.md +++ b/examples/two-sided-marketplace/README.md @@ -93,7 +93,7 @@ ability links only the shared token. ## Usage ```bash -export CHATBOTKIT_API_KEY="sk-...your-api-key..." +export CHATBOTKIT_API_TOKEN="sk-...your-api-token..." cp terraform.tfvars.example terraform.tfvars # fill in the secret values terraform init diff --git a/examples/two-sided-marketplace/main.tf b/examples/two-sided-marketplace/main.tf index c78f493..1f1cc58 100644 --- a/examples/two-sided-marketplace/main.tf +++ b/examples/two-sided-marketplace/main.tf @@ -57,7 +57,7 @@ # including its scheme (`Bearer `), so never add your own `Bearer ` prefix. # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable # - Provide the secret/OAuth values in terraform.tfvars (see the .example file) terraform { @@ -69,7 +69,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/examples/two-sided-marketplace/terraform.tfvars.example b/examples/two-sided-marketplace/terraform.tfvars.example index 659d81a..1a9f95f 100644 --- a/examples/two-sided-marketplace/terraform.tfvars.example +++ b/examples/two-sided-marketplace/terraform.tfvars.example @@ -1,8 +1,8 @@ # Secret values for the two-sided-marketplace example. # -# The ChatBotKit API key itself is supplied separately via the environment: +# The ChatBotKit API token itself is supplied separately via the environment: # -# export CHATBOTKIT_API_KEY="sk-...your-api-key..." +# export CHATBOTKIT_API_TOKEN="sk-...your-api-token..." # # Copy this file to terraform.tfvars and fill in the real values. Do not commit # terraform.tfvars — it contains secrets. diff --git a/examples/workflow-orchestrator/main.tf b/examples/workflow-orchestrator/main.tf index 0e22829..d8a729a 100644 --- a/examples/workflow-orchestrator/main.tf +++ b/examples/workflow-orchestrator/main.tf @@ -12,7 +12,7 @@ # - Trigger integration for workflow execution # # Prerequisites: -# - Set the CHATBOTKIT_API_KEY environment variable +# - Set the CHATBOTKIT_API_TOKEN environment variable terraform { required_providers { @@ -23,7 +23,7 @@ terraform { } provider "chatbotkit" { - # api_key = "..." # Or set CHATBOTKIT_API_KEY env var + # api_token = "..." # Or set CHATBOTKIT_API_TOKEN env var } # ============================================================================ diff --git a/go.mod b/go.mod index be7f96b..9dfbbcb 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/chatbotkit/terraform-sdk -go 1.24.11 +go 1.25.0 require ( github.com/hashicorp/terraform-plugin-framework v1.15.0 @@ -48,13 +48,13 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.17.0 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.39.0 // indirect + golang.org/x/tools v0.47.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect google.golang.org/grpc v1.75.1 // indirect diff --git a/go.sum b/go.sum index 4e9eb1d..941ec2f 100644 --- a/go.sum +++ b/go.sum @@ -173,22 +173,22 @@ go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mx go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -201,21 +201,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= diff --git a/internal/provider/client.go b/internal/provider/client.go index 23cb451..e3aad7b 100644 --- a/internal/provider/client.go +++ b/internal/provider/client.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "os" + "strings" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -22,13 +23,52 @@ type Client struct { HTTPClient *http.Client } +// envFirst returns the first non-empty value among the named environment +// variables. +func envFirst(names ...string) string { + for _, name := range names { + if value := os.Getenv(name); value != "" { + return value + } + } + + return "" +} + +// tokenFromEnv reads the API token from the environment. CHATBOTKIT_API_TOKEN +// is the name to use, or CBK_API_TOKEN for short. SECRET and KEY are older +// names that are still read so existing setups keep working. +func tokenFromEnv() string { + return envFirst( + "CHATBOTKIT_API_TOKEN", "CBK_API_TOKEN", + "CHATBOTKIT_API_SECRET", "CBK_API_SECRET", + "CHATBOTKIT_API_KEY", "CBK_API_KEY", + ) +} + +// runAsFromEnv reads the child User to operate on behalf of. The first pair are +// the names the CLI introduced and CHATBOTKIT_RUN_AS is this provider's +// original name; both tools read all of them, each with its CBK_ shorthand. +func runAsFromEnv() string { + return envFirst( + "CHATBOTKIT_API_RUNAS_USERID", "CBK_API_RUNAS_USERID", + "CHATBOTKIT_RUN_AS", "CBK_RUN_AS", + ) +} + // NewClient creates a new ChatBotKit API client. func NewClient(apiKey, baseURL string) *Client { if apiKey == "" { - apiKey = os.Getenv("CHATBOTKIT_API_KEY") + apiKey = tokenFromEnv() } if baseURL == "" { - baseURL = defaultBaseURL + // @note CHATBOTKIT_API_URL is the platform origin, as in the SDKs and the + // CLI; the GraphQL endpoint is derived from it + if origin := envFirst("CHATBOTKIT_API_URL", "CBK_API_URL"); origin != "" { + baseURL = strings.TrimRight(origin, "/") + "/api/v1/graphql" + } else { + baseURL = defaultBaseURL + } } return &Client{ APIKey: apiKey, @@ -71,8 +111,8 @@ func (c *Client) doRequest(ctx context.Context, query string, variables map[stri req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.APIKey) - // When set, act on behalf of a sub-account (partner user). This lets a single - // api_key manage many sub-accounts by selecting one per provider configuration. + // When set, act on behalf of a child User. This lets one API key manage many + // child Users by selecting one per provider configuration. if c.RunAs != "" { req.Header.Set("X-RunAs-UserId", c.RunAs) } @@ -116,6 +156,29 @@ func convertMapToInterface(ctx context.Context, m types.Map) map[string]interfac return result } +// maskedSecretValue is what the platform returns in place of a configured +// secret (access tokens, app secrets, ...) on read. +const maskedSecretValue = "********" + +// isMaskedSecret reports whether an API value is the platform's secret mask. +func isMaskedSecret(value string) bool { + if value == "" { + return false + } + return strings.Trim(value, "*") == "" +} + +// sensitiveStringFromAPI maps a sensitive API field onto state. The platform +// never echoes a configured secret back; it returns a mask instead. In that +// case (or when the field is absent) the value already in state is kept so +// the mask never overwrites the configured value and causes a perpetual diff. +func sensitiveStringFromAPI(current types.String, apiValue *string) types.String { + if apiValue == nil || isMaskedSecret(*apiValue) { + return current + } + return types.StringPointerValue(apiValue) +} + // CreateBlueprintInput represents the input for creating a blueprint. type CreateBlueprintInput struct { Alias *string `json:"alias,omitempty"` @@ -1828,6 +1891,7 @@ func (c *Client) GetGooglechatIntegration(ctx context.Context, id string) (*GetG // CreateInstagramIntegrationInput represents the input for creating a instagramintegration. type CreateInstagramIntegrationInput struct { AccessToken *string `json:"accessToken,omitempty"` + AppSecret *string `json:"appSecret,omitempty"` Alias *string `json:"alias,omitempty"` Attachments *bool `json:"attachments,omitempty"` BlueprintId *string `json:"blueprintId,omitempty"` @@ -1870,8 +1934,14 @@ func (c *Client) CreateInstagramIntegration(ctx context.Context, input CreateIns } // UpdateInstagramIntegrationInput represents the input for updating a instagramintegration. +// UpdateInstagramIntegrationInput is authoritative for the credentials: a nil +// AccessToken/AppSecret is serialised as an explicit JSON null so the platform +// clears the credential, rather than being omitted (which would keep it). This +// depends on the platform's updateInstagramIntegration mutation passing nulls +// through for those fields (being fixed platform-side in the same change). type UpdateInstagramIntegrationInput struct { - AccessToken *string `json:"accessToken,omitempty"` + AccessToken *string `json:"accessToken"` + AppSecret *string `json:"appSecret"` Alias *string `json:"alias,omitempty"` Attachments *bool `json:"attachments,omitempty"` BlueprintId *string `json:"blueprintId,omitempty"` @@ -1948,6 +2018,7 @@ func (c *Client) DeleteInstagramIntegration(ctx context.Context, id string) (*De type GetInstagramIntegrationResponse struct { ID *string `json:"id"` AccessToken *string `json:"accessToken,omitempty"` + AppSecret *string `json:"appSecret,omitempty"` Alias *string `json:"alias,omitempty"` Attachments *bool `json:"attachments,omitempty"` BlueprintId *string `json:"blueprintId,omitempty"` @@ -1971,6 +2042,7 @@ func (c *Client) GetInstagramIntegration(ctx context.Context, id string) (*GetIn node { id accessToken + appSecret alias attachments blueprintId @@ -2194,6 +2266,7 @@ func (c *Client) GetMcpserverIntegration(ctx context.Context, id string) (*GetMc // CreateMessengerIntegrationInput represents the input for creating a messengerintegration. type CreateMessengerIntegrationInput struct { AccessToken *string `json:"accessToken,omitempty"` + AppSecret *string `json:"appSecret,omitempty"` Alias *string `json:"alias,omitempty"` Attachments *bool `json:"attachments,omitempty"` BlueprintId *string `json:"blueprintId,omitempty"` @@ -2236,8 +2309,14 @@ func (c *Client) CreateMessengerIntegration(ctx context.Context, input CreateMes } // UpdateMessengerIntegrationInput represents the input for updating a messengerintegration. +// UpdateMessengerIntegrationInput is authoritative for the credentials: a nil +// AccessToken/AppSecret is serialised as an explicit JSON null so the platform +// clears the credential, rather than being omitted (which would keep it). This +// depends on the platform's updateMessengerIntegration mutation passing nulls +// through for those fields (being fixed platform-side in the same change). type UpdateMessengerIntegrationInput struct { - AccessToken *string `json:"accessToken,omitempty"` + AccessToken *string `json:"accessToken"` + AppSecret *string `json:"appSecret"` Alias *string `json:"alias,omitempty"` Attachments *bool `json:"attachments,omitempty"` BlueprintId *string `json:"blueprintId,omitempty"` @@ -2314,6 +2393,7 @@ func (c *Client) DeleteMessengerIntegration(ctx context.Context, id string) (*De type GetMessengerIntegrationResponse struct { ID *string `json:"id"` AccessToken *string `json:"accessToken,omitempty"` + AppSecret *string `json:"appSecret,omitempty"` Alias *string `json:"alias,omitempty"` Attachments *bool `json:"attachments,omitempty"` BlueprintId *string `json:"blueprintId,omitempty"` @@ -2337,6 +2417,7 @@ func (c *Client) GetMessengerIntegration(ctx context.Context, id string) (*GetMe node { id accessToken + appSecret alias attachments blueprintId @@ -3681,16 +3762,16 @@ func (c *Client) GetSkillserverIntegration(ctx context.Context, id string) (*Get // CreateSkillsetAbilityInput represents the input for creating a skillsetability. type CreateSkillsetAbilityInput struct { - BlueprintId *string `json:"blueprintId,omitempty"` - BotId *string `json:"botId,omitempty"` - Description *string `json:"description,omitempty"` - FileId *string `json:"fileId,omitempty"` - Instruction *string `json:"instruction,omitempty"` - Meta map[string]interface{} `json:"meta,omitempty"` - Name *string `json:"name,omitempty"` - SecretId *string `json:"secretId,omitempty"` - SpaceId *string `json:"spaceId,omitempty"` - State *string `json:"state,omitempty"` + BlueprintId *string `json:"blueprintId,omitempty"` + LinkedBotId *string `json:"linkedBotId,omitempty"` + Description *string `json:"description,omitempty"` + LinkedFileId *string `json:"linkedFileId,omitempty"` + Instruction *string `json:"instruction,omitempty"` + Meta map[string]interface{} `json:"meta,omitempty"` + Name *string `json:"name,omitempty"` + LinkedSecretId *string `json:"linkedSecretId,omitempty"` + LinkedSpaceId *string `json:"linkedSpaceId,omitempty"` + State *string `json:"state,omitempty"` } // CreateSkillsetAbilityResponse represents the response from creating a skillsetability. @@ -3725,17 +3806,22 @@ func (c *Client) CreateSkillsetAbility(ctx context.Context, skillsetId string, i } // UpdateSkillsetAbilityInput represents the input for updating a skillsetability. +// UpdateSkillsetAbilityInput is authoritative for the linked relations: a nil +// BlueprintId/Linked*Id is serialised as an explicit JSON null so the platform +// clears the link, rather than being omitted (which would keep it). This +// depends on the platform's updateSkillsetAbility mutation passing nulls +// through for those fields (being fixed platform-side in the same change). type UpdateSkillsetAbilityInput struct { - BlueprintId *string `json:"blueprintId,omitempty"` - BotId *string `json:"botId,omitempty"` - Description *string `json:"description,omitempty"` - FileId *string `json:"fileId,omitempty"` - Instruction *string `json:"instruction,omitempty"` - Meta map[string]interface{} `json:"meta,omitempty"` - Name *string `json:"name,omitempty"` - SecretId *string `json:"secretId,omitempty"` - SpaceId *string `json:"spaceId,omitempty"` - State *string `json:"state,omitempty"` + BlueprintId *string `json:"blueprintId"` + LinkedBotId *string `json:"linkedBotId"` + Description *string `json:"description,omitempty"` + LinkedFileId *string `json:"linkedFileId"` + Instruction *string `json:"instruction,omitempty"` + Meta map[string]interface{} `json:"meta,omitempty"` + Name *string `json:"name,omitempty"` + LinkedSecretId *string `json:"linkedSecretId"` + LinkedSpaceId *string `json:"linkedSpaceId"` + State *string `json:"state,omitempty"` } // UpdateSkillsetAbilityResponse represents the response from updating a skillsetability. @@ -3803,43 +3889,65 @@ func (c *Client) DeleteSkillsetAbility(ctx context.Context, skillsetId string, a // GetSkillsetAbilityResponse represents the response from fetching a skillsetability. type GetSkillsetAbilityResponse struct { - ID *string `json:"id"` - BlueprintId *string `json:"blueprintId,omitempty"` - BotId *string `json:"botId,omitempty"` - Description *string `json:"description,omitempty"` - FileId *string `json:"fileId,omitempty"` - Instruction *string `json:"instruction,omitempty"` - Meta map[string]interface{} `json:"meta,omitempty"` - Name *string `json:"name,omitempty"` - SecretId *string `json:"secretId,omitempty"` - SpaceId *string `json:"spaceId,omitempty"` - State *string `json:"state,omitempty"` - CreatedAt *string `json:"createdAt,omitempty"` - UpdatedAt *string `json:"updatedAt,omitempty"` -} + ID *string `json:"id"` + BlueprintId *string `json:"blueprintId,omitempty"` + LinkedBotId *string `json:"linkedBotId,omitempty"` + Description *string `json:"description,omitempty"` + LinkedFileId *string `json:"linkedFileId,omitempty"` + Instruction *string `json:"instruction,omitempty"` + Meta map[string]interface{} `json:"meta,omitempty"` + Name *string `json:"name,omitempty"` + LinkedSecretId *string `json:"linkedSecretId,omitempty"` + LinkedSpaceId *string `json:"linkedSpaceId,omitempty"` + State *string `json:"state,omitempty"` + CreatedAt *string `json:"createdAt,omitempty"` + UpdatedAt *string `json:"updatedAt,omitempty"` +} + +// graphqlRequester is the slice of *Client used by lookup helpers so they can +// be exercised against a fake transport in unit tests. +type graphqlRequester interface { + doRequest(ctx context.Context, query string, variables map[string]interface{}, result interface{}) error +} + +// skillsetAbilityPageSize is the number of abilities fetched per page while +// looking an ability up through its skillset connection. +const skillsetAbilityPageSize = 100 // GetSkillsetAbility fetches a skillsetability by ID. func (c *Client) GetSkillsetAbility(ctx context.Context, skillsetId string, id string) (*GetSkillsetAbilityResponse, error) { - // Query abilities through the skillset connection + return findSkillsetAbility(ctx, c, skillsetId, id) +} + +// findSkillsetAbility walks the skillset's abilities connection page by page +// until the ability with the given id is found or the connection is exhausted. +// +// The platform schema has no direct `ability(id:)` query and the skillset +// `abilities` connection takes no id filter, so the lookup has to paginate. +func findSkillsetAbility(ctx context.Context, requester graphqlRequester, skillsetId string, id string) (*GetSkillsetAbilityResponse, error) { query := ` - query GetSkillsetAbility($skillsetIds: [ID!]) { + query GetSkillsetAbility($skillsetIds: [ID!], $first: Int!, $after: String) { skillsets(first: 1, skillsetIds: $skillsetIds) { edges { node { id - abilities(first: 100) { + abilities(first: $first, after: $after) { + pageInfo { + hasNextPage + endCursor + } edges { node { id - blueprintId - botId + blueprint { id } + linkedBot { id } description - fileId + linkedFile { id } instruction meta name - secretId - spaceId + linkedSecret { id } + linkedSpace { id } state createdAt updatedAt @@ -3852,36 +3960,103 @@ func (c *Client) GetSkillsetAbility(ctx context.Context, skillsetId string, id s } ` - variables := map[string]interface{}{ - "skillsetIds": []string{skillsetId}, - } - - var response struct { - Skillsets struct { - Edges []struct { - Node struct { - ID string `json:"id"` - Abilities struct { - Edges []struct { - Node *GetSkillsetAbilityResponse `json:"node"` - } `json:"edges"` - } `json:"abilities"` - } `json:"node"` - } `json:"edges"` - } `json:"skillsets"` - } - - if err := c.doRequest(ctx, query, variables, &response); err != nil { - return nil, err - } - - // Find the ability with matching ID - for _, parentEdge := range response.Skillsets.Edges { - for _, abilityEdge := range parentEdge.Node.Abilities.Edges { - if abilityEdge.Node != nil && abilityEdge.Node.ID != nil && *abilityEdge.Node.ID == id { - return abilityEdge.Node, nil + // The Ability type exposes its links as relations (blueprint, linkedBot, + // linkedFile, linkedSecret, linkedSpace) rather than scalar IDs, so the + // node is decoded into an intermediate shape and flattened below. + type idRef struct { + ID *string `json:"id"` + } + type abilityNode struct { + ID *string `json:"id"` + Blueprint *idRef `json:"blueprint"` + LinkedBot *idRef `json:"linkedBot"` + Description *string `json:"description,omitempty"` + LinkedFile *idRef `json:"linkedFile"` + Instruction *string `json:"instruction,omitempty"` + Meta map[string]interface{} `json:"meta,omitempty"` + Name *string `json:"name,omitempty"` + LinkedSecret *idRef `json:"linkedSecret"` + LinkedSpace *idRef `json:"linkedSpace"` + State *string `json:"state,omitempty"` + CreatedAt *string `json:"createdAt,omitempty"` + UpdatedAt *string `json:"updatedAt,omitempty"` + } + + refId := func(ref *idRef) *string { + if ref == nil { + return nil + } + return ref.ID + } + + var after *string + seen := map[string]bool{} + + for { + variables := map[string]interface{}{ + "skillsetIds": []string{skillsetId}, + "first": skillsetAbilityPageSize, + "after": after, + } + + var response struct { + Skillsets struct { + Edges []struct { + Node struct { + ID string `json:"id"` + Abilities struct { + PageInfo struct { + HasNextPage bool `json:"hasNextPage"` + EndCursor *string `json:"endCursor"` + } `json:"pageInfo"` + Edges []struct { + Node *abilityNode `json:"node"` + } `json:"edges"` + } `json:"abilities"` + } `json:"node"` + } `json:"edges"` + } `json:"skillsets"` + } + + if err := requester.doRequest(ctx, query, variables, &response); err != nil { + return nil, err + } + + if len(response.Skillsets.Edges) == 0 { + return nil, fmt.Errorf("skillset with ID %s not found", skillsetId) + } + + abilities := response.Skillsets.Edges[0].Node.Abilities + + for _, abilityEdge := range abilities.Edges { + node := abilityEdge.Node + if node != nil && node.ID != nil && *node.ID == id { + return &GetSkillsetAbilityResponse{ + ID: node.ID, + BlueprintId: refId(node.Blueprint), + LinkedBotId: refId(node.LinkedBot), + Description: node.Description, + LinkedFileId: refId(node.LinkedFile), + Instruction: node.Instruction, + Meta: node.Meta, + Name: node.Name, + LinkedSecretId: refId(node.LinkedSecret), + LinkedSpaceId: refId(node.LinkedSpace), + State: node.State, + CreatedAt: node.CreatedAt, + UpdatedAt: node.UpdatedAt, + }, nil } } + + // Stop on the last page, a missing cursor, or a cursor we have already + // used (guards against a server that never advances). + cursor := abilities.PageInfo.EndCursor + if !abilities.PageInfo.HasNextPage || cursor == nil || *cursor == "" || seen[*cursor] { + break + } + seen[*cursor] = true + after = cursor } return nil, fmt.Errorf("skillsetability with ID %s not found in skillset %s", id, skillsetId) @@ -5606,6 +5781,7 @@ func (c *Client) GetTwilioIntegration(ctx context.Context, id string) (*GetTwili // CreateWhatsAppIntegrationInput represents the input for creating a whatsappintegration. type CreateWhatsAppIntegrationInput struct { AccessToken *string `json:"accessToken,omitempty"` + AppSecret *string `json:"appSecret,omitempty"` Alias *string `json:"alias,omitempty"` AllowFrom *string `json:"allowFrom,omitempty"` Attachments *bool `json:"attachments,omitempty"` @@ -5650,8 +5826,14 @@ func (c *Client) CreateWhatsAppIntegration(ctx context.Context, input CreateWhat } // UpdateWhatsAppIntegrationInput represents the input for updating a whatsappintegration. +// UpdateWhatsAppIntegrationInput is authoritative for the credentials: a nil +// AccessToken/AppSecret is serialised as an explicit JSON null so the platform +// clears the credential, rather than being omitted (which would keep it). This +// depends on the platform's updateWhatsAppIntegration mutation passing nulls +// through for those fields (being fixed platform-side in the same change). type UpdateWhatsAppIntegrationInput struct { - AccessToken *string `json:"accessToken,omitempty"` + AccessToken *string `json:"accessToken"` + AppSecret *string `json:"appSecret"` Alias *string `json:"alias,omitempty"` AllowFrom *string `json:"allowFrom,omitempty"` Attachments *bool `json:"attachments,omitempty"` @@ -5730,6 +5912,7 @@ func (c *Client) DeleteWhatsAppIntegration(ctx context.Context, id string) (*Del type GetWhatsAppIntegrationResponse struct { ID *string `json:"id"` AccessToken *string `json:"accessToken,omitempty"` + AppSecret *string `json:"appSecret,omitempty"` Alias *string `json:"alias,omitempty"` AllowFrom *string `json:"allowFrom,omitempty"` Attachments *bool `json:"attachments,omitempty"` @@ -5755,6 +5938,7 @@ func (c *Client) GetWhatsAppIntegration(ctx context.Context, id string) (*GetWha node { id accessToken + appSecret alias allowFrom attachments diff --git a/internal/provider/client_test.go b/internal/provider/client_test.go index 2cd5eed..eb945d3 100644 --- a/internal/provider/client_test.go +++ b/internal/provider/client_test.go @@ -3,8 +3,10 @@ package provider import ( "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" + "strings" "testing" ) @@ -26,12 +28,115 @@ func TestNewClient(t *testing.T) { }) t.Run("uses default base URL when empty", func(t *testing.T) { + t.Setenv("CHATBOTKIT_API_URL", "") + t.Setenv("CBK_API_URL", "") + client := NewClient("test-api-key", "") if client.BaseURL != defaultBaseURL { t.Errorf("expected BaseURL to be '%s', got '%s'", defaultBaseURL, client.BaseURL) } }) + + t.Run("derives the GraphQL endpoint from the platform origin in the environment", func(t *testing.T) { + for origin, want := range map[string]string{ + "http://localhost:3000": "http://localhost:3000/api/v1/graphql", + "http://127.0.0.1:4300/": "http://127.0.0.1:4300/api/v1/graphql", + "https://corp.example/cbk": "https://corp.example/cbk/api/v1/graphql", + "https://corp.example/cbk/": "https://corp.example/cbk/api/v1/graphql", + } { + t.Setenv("CHATBOTKIT_API_URL", origin) + + if client := NewClient("test-api-key", ""); client.BaseURL != want { + t.Errorf("origin %s: expected BaseURL '%s', got '%s'", origin, want, client.BaseURL) + } + } + }) + + t.Run("reads the token from the environment under every supported name", func(t *testing.T) { + names := []string{ + "CHATBOTKIT_API_TOKEN", "CBK_API_TOKEN", + "CHATBOTKIT_API_SECRET", "CBK_API_SECRET", + "CHATBOTKIT_API_KEY", "CBK_API_KEY", + } + + clear := func() { + for _, name := range names { + t.Setenv(name, "") + } + } + + for _, name := range names { + clear() + t.Setenv(name, "from-"+name) + + if client := NewClient("", ""); client.APIKey != "from-"+name { + t.Errorf("%s: expected the token to be read, got '%s'", name, client.APIKey) + } + } + + // @note names earlier in the list win: TOKEN over SECRET over KEY, and a + // long name over its CBK_ shorthand + for i := 0; i < len(names)-1; i++ { + clear() + t.Setenv(names[i], "winner") + t.Setenv(names[i+1], "loser") + + if client := NewClient("", ""); client.APIKey != "winner" { + t.Errorf("expected %s to win over %s, got '%s'", names[i], names[i+1], client.APIKey) + } + } + + clear() + t.Setenv("CHATBOTKIT_API_TOKEN", "from-env") + + if client := NewClient("configured", ""); client.APIKey != "configured" { + t.Errorf("expected the configured token to win over the environment, got '%s'", client.APIKey) + } + }) + + t.Run("reads the run-as user under the CLI names and the original one", func(t *testing.T) { + names := []string{"CHATBOTKIT_API_RUNAS_USERID", "CBK_API_RUNAS_USERID", "CHATBOTKIT_RUN_AS", "CBK_RUN_AS"} + + for i, name := range names { + for _, other := range names { + t.Setenv(other, "") + } + + t.Setenv(name, "user-"+name) + + if got := runAsFromEnv(); got != "user-"+name { + t.Errorf("%s: expected the run-as user to be read, got '%s'", name, got) + } + + if i+1 < len(names) { + t.Setenv(names[i+1], "loser") + + if got := runAsFromEnv(); got != "user-"+name { + t.Errorf("expected %s to win over %s, got '%s'", name, names[i+1], got) + } + } + } + }) + + t.Run("reads the platform origin from the CBK_API_URL shorthand", func(t *testing.T) { + t.Setenv("CHATBOTKIT_API_URL", "") + t.Setenv("CBK_API_URL", "http://localhost:3000") + + if client := NewClient("test-api-key", ""); client.BaseURL != "http://localhost:3000/api/v1/graphql" { + t.Errorf("expected the shorthand origin to be used, got '%s'", client.BaseURL) + } + }) + + t.Run("prefers the configured base URL over the environment", func(t *testing.T) { + t.Setenv("CHATBOTKIT_API_URL", "http://localhost:3000") + + client := NewClient("test-api-key", "http://localhost:9000/graphql") + + if client.BaseURL != "http://localhost:9000/graphql" { + t.Errorf("expected the configured BaseURL to win, got '%s'", client.BaseURL) + } + }) } func TestCreateBot(t *testing.T) { @@ -370,3 +475,124 @@ func TestDoRequest_HTTPError(t *testing.T) { } }) } + +// fakeGraphQLRequester serves canned JSON pages keyed by the `after` cursor +// and records the variables it was asked for. +type fakeGraphQLRequester struct { + pages map[string]string // key: after cursor ("" for the first page) + calls []map[string]interface{} +} + +func (f *fakeGraphQLRequester) doRequest(_ context.Context, _ string, variables map[string]interface{}, result interface{}) error { + f.calls = append(f.calls, variables) + + key := "" + if after, ok := variables["after"].(*string); ok && after != nil { + key = *after + } + + page, ok := f.pages[key] + if !ok { + return fmt.Errorf("no page for cursor %q", key) + } + + return json.Unmarshal([]byte(page), result) +} + +func abilityPage(hasNext bool, endCursor string, ids ...string) string { + edges := make([]string, 0, len(ids)) + for _, id := range ids { + edges = append(edges, fmt.Sprintf(`{"node":{"id":%q,"name":"n-%s","linkedSecret":{"id":"secret-%s"}}}`, id, id, id)) + } + cursor := "null" + if endCursor != "" { + cursor = fmt.Sprintf("%q", endCursor) + } + return fmt.Sprintf(`{"skillsets":{"edges":[{"node":{"id":"skillset-1","abilities":{"pageInfo":{"hasNextPage":%t,"endCursor":%s},"edges":[%s]}}}]}}`, + hasNext, cursor, strings.Join(edges, ",")) +} + +func TestFindSkillsetAbility(t *testing.T) { + ctx := context.Background() + + t.Run("finds an ability on the first page without paginating", func(t *testing.T) { + fake := &fakeGraphQLRequester{pages: map[string]string{ + "": abilityPage(true, "c1", "a1", "a2"), + }} + + got, err := findSkillsetAbility(ctx, fake, "skillset-1", "a2") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.ID == nil || *got.ID != "a2" || got.LinkedSecretId == nil || *got.LinkedSecretId != "secret-a2" { + t.Fatalf("unexpected result: %+v", got) + } + if len(fake.calls) != 1 { + t.Fatalf("expected a single request, got %d", len(fake.calls)) + } + if fake.calls[0]["first"] != skillsetAbilityPageSize { + t.Fatalf("expected page size %d, got %v", skillsetAbilityPageSize, fake.calls[0]["first"]) + } + }) + + t.Run("follows endCursor to later pages", func(t *testing.T) { + fake := &fakeGraphQLRequester{pages: map[string]string{ + "": abilityPage(true, "c1", "a1"), + "c1": abilityPage(true, "c2", "a2"), + "c2": abilityPage(false, "", "a3"), + }} + + got, err := findSkillsetAbility(ctx, fake, "skillset-1", "a3") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.ID == nil || *got.ID != "a3" { + t.Fatalf("unexpected result: %+v", got) + } + if len(fake.calls) != 3 { + t.Fatalf("expected three requests, got %d", len(fake.calls)) + } + if after, _ := fake.calls[2]["after"].(*string); after == nil || *after != "c2" { + t.Fatalf("expected third request to use cursor c2, got %v", fake.calls[2]["after"]) + } + }) + + t.Run("reports not found after exhausting the connection", func(t *testing.T) { + fake := &fakeGraphQLRequester{pages: map[string]string{ + "": abilityPage(true, "c1", "a1"), + "c1": abilityPage(false, "", "a2"), + }} + + _, err := findSkillsetAbility(ctx, fake, "skillset-1", "missing") + if err == nil || !strings.Contains(err.Error(), "not found") { + t.Fatalf("expected a not found error, got %v", err) + } + if len(fake.calls) != 2 { + t.Fatalf("expected two requests, got %d", len(fake.calls)) + } + }) + + t.Run("stops when the cursor does not advance", func(t *testing.T) { + fake := &fakeGraphQLRequester{pages: map[string]string{ + "": abilityPage(true, "c1", "a1"), + "c1": abilityPage(true, "c1", "a1"), + }} + + _, err := findSkillsetAbility(ctx, fake, "skillset-1", "missing") + if err == nil || !strings.Contains(err.Error(), "not found") { + t.Fatalf("expected a not found error, got %v", err) + } + if len(fake.calls) != 2 { + t.Fatalf("expected the loop to stop on a repeated cursor, got %d requests", len(fake.calls)) + } + }) + + t.Run("reports a missing skillset", func(t *testing.T) { + fake := &fakeGraphQLRequester{pages: map[string]string{"": `{"skillsets":{"edges":[]}}`}} + + _, err := findSkillsetAbility(ctx, fake, "skillset-x", "a1") + if err == nil || !strings.Contains(err.Error(), "skillset with ID skillset-x not found") { + t.Fatalf("expected a skillset not found error, got %v", err) + } + }) +} diff --git a/internal/provider/datasource_bot_test.go b/internal/provider/datasource_bot_test.go index 4593bd0..e7c0c8a 100644 --- a/internal/provider/datasource_bot_test.go +++ b/internal/provider/datasource_bot_test.go @@ -7,7 +7,7 @@ import ( ) // TestAccBotDataSource_basic tests reading an existing bot via data source. -// This test requires CHATBOTKIT_API_KEY to be set in the environment +// This test requires CHATBOTKIT_API_TOKEN to be set in the environment // and an existing bot to be available. func TestAccBotDataSource_basic(t *testing.T) { resource.Test(t, resource.TestCase{ diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 873b3d5..06bbdce 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -2,7 +2,6 @@ package provider import ( "context" - "os" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" @@ -21,9 +20,10 @@ type ChatBotKitProvider struct { // ChatBotKitProviderModel describes the provider data model. type ChatBotKitProviderModel struct { - APIKey types.String `tfsdk:"api_key"` - BaseURL types.String `tfsdk:"base_url"` - RunAs types.String `tfsdk:"run_as"` + APIToken types.String `tfsdk:"api_token"` + APIKey types.String `tfsdk:"api_key"` + BaseURL types.String `tfsdk:"base_url"` + RunAs types.String `tfsdk:"run_as"` } func (p *ChatBotKitProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { @@ -35,17 +35,23 @@ func (p *ChatBotKitProvider) Schema(ctx context.Context, req provider.SchemaRequ resp.Schema = schema.Schema{ MarkdownDescription: "ChatBotKit Terraform Provider for managing AI chatbot resources.", Attributes: map[string]schema.Attribute{ + "api_token": schema.StringAttribute{ + MarkdownDescription: "The API token for authenticating with the ChatBotKit API. Can also be set via the CHATBOTKIT_API_TOKEN environment variable, or CBK_API_TOKEN for short.", + Optional: true, + Sensitive: true, + }, "api_key": schema.StringAttribute{ - MarkdownDescription: "The API key for authenticating with ChatBotKit API. Can also be set via CHATBOTKIT_API_KEY environment variable.", + MarkdownDescription: "The former name of `api_token`. It is used when `api_token` is not set.", + DeprecationMessage: "Use api_token instead. api_key is still read when api_token is not set.", Optional: true, Sensitive: true, }, "base_url": schema.StringAttribute{ - MarkdownDescription: "The base URL for the ChatBotKit API. Defaults to https://api.chatbotkit.com/graphql", + MarkdownDescription: "The GraphQL endpoint URL. Defaults to https://api.chatbotkit.com/graphql. For a self-hosted platform use its GraphQL endpoint, e.g. http://localhost:3000/api/v1/graphql, or set the platform origin in the CHATBOTKIT_API_URL environment variable. Plain http works for local use.", Optional: true, }, "run_as": schema.StringAttribute{ - MarkdownDescription: "The ID of a sub-account (partner user) to operate on behalf of. When set, requests include the X-RunAs-UserId header, so a single api_key (a partner/master token) can manage many sub-accounts by configuring one provider alias per sub-account. Can also be set via the CHATBOTKIT_RUN_AS environment variable.", + MarkdownDescription: "The ID of a child User to operate on behalf of. When set, requests include the X-RunAs-UserId header, so one api_token belonging to the parent User can manage many child Users by configuring one provider alias per child User. Can also be set via the CHATBOTKIT_API_RUNAS_USERID environment variable (or CBK_API_RUNAS_USERID for short); CHATBOTKIT_RUN_AS and CBK_RUN_AS are still read.", Optional: true, }, }, @@ -61,16 +67,20 @@ func (p *ChatBotKitProvider) Configure(ctx context.Context, req provider.Configu return } - // Get API key from config or environment - apiKey := data.APIKey.ValueString() + // Get the API token from the config, then its deprecated name, then the + // environment + apiKey := data.APIToken.ValueString() + if apiKey == "" { + apiKey = data.APIKey.ValueString() + } if apiKey == "" { - apiKey = os.Getenv("CHATBOTKIT_API_KEY") + apiKey = tokenFromEnv() } if apiKey == "" { resp.Diagnostics.AddError( - "Missing API Key", - "The API key is required. Set it in the provider configuration or via the CHATBOTKIT_API_KEY environment variable.", + "Missing API Token", + "The API token is required. Set api_token in the provider configuration or the CHATBOTKIT_API_TOKEN environment variable.", ) return } @@ -81,10 +91,10 @@ func (p *ChatBotKitProvider) Configure(ctx context.Context, req provider.Configu // Create the API client client := NewClient(apiKey, baseURL) - // Optionally operate on behalf of a sub-account (partner user). + // Optionally operate on behalf of a child User. runAs := data.RunAs.ValueString() if runAs == "" { - runAs = os.Getenv("CHATBOTKIT_RUN_AS") + runAs = runAsFromEnv() } client.RunAs = runAs diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index a9544ba..ee58255 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -1,7 +1,6 @@ package provider import ( - "os" "testing" "github.com/hashicorp/terraform-plugin-framework/providerserver" @@ -16,11 +15,11 @@ var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServe "chatbotkit": providerserver.NewProtocol6WithError(New("test")()), } -// testAccPreCheck validates the necessary test API keys exist in the testing -// environment. +// testAccPreCheck validates the API token exists in the testing environment, +// under any of the names the provider reads. func testAccPreCheck(t *testing.T) { - if v := os.Getenv("CHATBOTKIT_API_KEY"); v == "" { - t.Fatal("CHATBOTKIT_API_KEY must be set for acceptance tests") + if tokenFromEnv() == "" { + t.Fatal("CHATBOTKIT_API_TOKEN must be set for acceptance tests") } } diff --git a/internal/provider/resource_bot_test.go b/internal/provider/resource_bot_test.go index 1ef6add..1b977af 100644 --- a/internal/provider/resource_bot_test.go +++ b/internal/provider/resource_bot_test.go @@ -8,7 +8,7 @@ import ( ) // TestAccBotResource_basic tests the basic lifecycle of a bot resource. -// This test requires CHATBOTKIT_API_KEY to be set in the environment. +// This test requires CHATBOTKIT_API_TOKEN to be set in the environment. // See the README.md for instructions on obtaining an API key. func TestAccBotResource_basic(t *testing.T) { resource.Test(t, resource.TestCase{ diff --git a/internal/provider/resource_instagram_integration.go b/internal/provider/resource_instagram_integration.go index 41f808c..ef11812 100644 --- a/internal/provider/resource_instagram_integration.go +++ b/internal/provider/resource_instagram_integration.go @@ -33,6 +33,7 @@ type InstagramIntegrationResourceModel struct { ID types.String `tfsdk:"id"` AccessToken types.String `tfsdk:"access_token"` + AppSecret types.String `tfsdk:"app_secret"` Alias types.String `tfsdk:"alias"` Attachments types.Bool `tfsdk:"attachments"` BlueprintId types.String `tfsdk:"blueprint_id"` @@ -69,6 +70,11 @@ func (r *InstagramIntegrationResource) Schema(ctx context.Context, req resource. Optional: true, Sensitive: true, }, + "app_secret": schema.StringAttribute{ + MarkdownDescription: "The Instagram app secret used to verify webhook signatures", + Optional: true, + Sensitive: true, + }, "alias": schema.StringAttribute{ MarkdownDescription: "The alias ID for the integration", Optional: true, @@ -151,6 +157,7 @@ func (r *InstagramIntegrationResource) Create(ctx context.Context, req resource. result, err := r.client.CreateInstagramIntegration(ctx, CreateInstagramIntegrationInput{ AccessToken: data.AccessToken.ValueStringPointer(), + AppSecret: data.AppSecret.ValueStringPointer(), Alias: data.Alias.ValueStringPointer(), Attachments: data.Attachments.ValueBoolPointer(), BlueprintId: data.BlueprintId.ValueStringPointer(), @@ -201,9 +208,9 @@ func (r *InstagramIntegrationResource) Read(ctx context.Context, req resource.Re // Update data model with response values - if result.AccessToken != nil { - data.AccessToken = types.StringPointerValue(result.AccessToken) - } + // Secrets come back masked once configured; keep the configured value. + data.AccessToken = sensitiveStringFromAPI(data.AccessToken, result.AccessToken) + data.AppSecret = sensitiveStringFromAPI(data.AppSecret, result.AppSecret) if result.Alias != nil { data.Alias = types.StringPointerValue(result.Alias) } @@ -259,6 +266,7 @@ func (r *InstagramIntegrationResource) Update(ctx context.Context, req resource. _, err := r.client.UpdateInstagramIntegration(ctx, data.ID.ValueString(), UpdateInstagramIntegrationInput{ AccessToken: data.AccessToken.ValueStringPointer(), + AppSecret: data.AppSecret.ValueStringPointer(), Alias: data.Alias.ValueStringPointer(), Attachments: data.Attachments.ValueBoolPointer(), BlueprintId: data.BlueprintId.ValueStringPointer(), diff --git a/internal/provider/resource_messenger_integration.go b/internal/provider/resource_messenger_integration.go index fb6cf71..de07f44 100644 --- a/internal/provider/resource_messenger_integration.go +++ b/internal/provider/resource_messenger_integration.go @@ -33,6 +33,7 @@ type MessengerIntegrationResourceModel struct { ID types.String `tfsdk:"id"` AccessToken types.String `tfsdk:"access_token"` + AppSecret types.String `tfsdk:"app_secret"` Alias types.String `tfsdk:"alias"` Attachments types.Bool `tfsdk:"attachments"` BlueprintId types.String `tfsdk:"blueprint_id"` @@ -69,6 +70,11 @@ func (r *MessengerIntegrationResource) Schema(ctx context.Context, req resource. Optional: true, Sensitive: true, }, + "app_secret": schema.StringAttribute{ + MarkdownDescription: "The Facebook Messenger app secret used to verify webhook signatures", + Optional: true, + Sensitive: true, + }, "alias": schema.StringAttribute{ MarkdownDescription: "The alias ID for the integration", Optional: true, @@ -151,6 +157,7 @@ func (r *MessengerIntegrationResource) Create(ctx context.Context, req resource. result, err := r.client.CreateMessengerIntegration(ctx, CreateMessengerIntegrationInput{ AccessToken: data.AccessToken.ValueStringPointer(), + AppSecret: data.AppSecret.ValueStringPointer(), Alias: data.Alias.ValueStringPointer(), Attachments: data.Attachments.ValueBoolPointer(), BlueprintId: data.BlueprintId.ValueStringPointer(), @@ -201,9 +208,9 @@ func (r *MessengerIntegrationResource) Read(ctx context.Context, req resource.Re // Update data model with response values - if result.AccessToken != nil { - data.AccessToken = types.StringPointerValue(result.AccessToken) - } + // Secrets come back masked once configured; keep the configured value. + data.AccessToken = sensitiveStringFromAPI(data.AccessToken, result.AccessToken) + data.AppSecret = sensitiveStringFromAPI(data.AppSecret, result.AppSecret) if result.Alias != nil { data.Alias = types.StringPointerValue(result.Alias) } @@ -259,6 +266,7 @@ func (r *MessengerIntegrationResource) Update(ctx context.Context, req resource. _, err := r.client.UpdateMessengerIntegration(ctx, data.ID.ValueString(), UpdateMessengerIntegrationInput{ AccessToken: data.AccessToken.ValueStringPointer(), + AppSecret: data.AppSecret.ValueStringPointer(), Alias: data.Alias.ValueStringPointer(), Attachments: data.Attachments.ValueBoolPointer(), BlueprintId: data.BlueprintId.ValueStringPointer(), diff --git a/internal/provider/resource_meta_integration_app_secret_test.go b/internal/provider/resource_meta_integration_app_secret_test.go new file mode 100644 index 0000000..f5ea576 --- /dev/null +++ b/internal/provider/resource_meta_integration_app_secret_test.go @@ -0,0 +1,148 @@ +package provider + +import ( + "context" + "encoding/json" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestSensitiveStringFromAPI(t *testing.T) { + configured := types.StringValue("configured-secret") + + t.Run("keeps state when the API returns the mask", func(t *testing.T) { + got := sensitiveStringFromAPI(configured, ptr(maskedSecretValue)) + if got.ValueString() != "configured-secret" { + t.Fatalf("expected configured value to be kept, got %q", got.ValueString()) + } + }) + + t.Run("keeps state when the API omits the field", func(t *testing.T) { + got := sensitiveStringFromAPI(configured, nil) + if got.ValueString() != "configured-secret" { + t.Fatalf("expected configured value to be kept, got %q", got.ValueString()) + } + }) + + t.Run("keeps a null state when the mask is returned for an unmanaged secret", func(t *testing.T) { + got := sensitiveStringFromAPI(types.StringNull(), ptr("****")) + if !got.IsNull() { + t.Fatalf("expected null state to be preserved, got %q", got.ValueString()) + } + }) + + t.Run("takes a real API value", func(t *testing.T) { + got := sensitiveStringFromAPI(configured, ptr("plain-value")) + if got.ValueString() != "plain-value" { + t.Fatalf("expected API value, got %q", got.ValueString()) + } + }) + + t.Run("empty string is not treated as a mask", func(t *testing.T) { + got := sensitiveStringFromAPI(configured, ptr("")) + if got.ValueString() != "" { + t.Fatalf("expected empty API value to be applied, got %q", got.ValueString()) + } + }) +} + +func TestMetaIntegrationAppSecretMapping(t *testing.T) { + assertJSONHas := func(t *testing.T, v interface{}, want string) { + t.Helper() + body, err := json.Marshal(v) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if !strings.Contains(string(body), want) { + t.Fatalf("expected %s in payload, got %s", want, body) + } + } + assertJSONLacks := func(t *testing.T, v interface{}, unwanted string) { + t.Helper() + body, err := json.Marshal(v) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if strings.Contains(string(body), unwanted) { + t.Fatalf("expected %s to be absent from payload, got %s", unwanted, body) + } + } + + t.Run("instagram create/update carry appSecret", func(t *testing.T) { + data := InstagramIntegrationResourceModel{AccessToken: types.StringValue("tok"), AppSecret: types.StringValue("sec")} + assertJSONHas(t, CreateInstagramIntegrationInput{AccessToken: data.AccessToken.ValueStringPointer(), AppSecret: data.AppSecret.ValueStringPointer()}, `"appSecret":"sec"`) + assertJSONHas(t, UpdateInstagramIntegrationInput{AppSecret: data.AppSecret.ValueStringPointer()}, `"appSecret":"sec"`) + assertJSONLacks(t, CreateInstagramIntegrationInput{AppSecret: types.StringNull().ValueStringPointer()}, `"appSecret"`) + assertJSONLacks(t, CreateInstagramIntegrationInput{AccessToken: types.StringNull().ValueStringPointer()}, `"accessToken"`) + assertJSONHas(t, UpdateInstagramIntegrationInput{}, `"appSecret":null`) + assertJSONHas(t, UpdateInstagramIntegrationInput{}, `"accessToken":null`) + }) + + t.Run("messenger create/update carry appSecret", func(t *testing.T) { + data := MessengerIntegrationResourceModel{AppSecret: types.StringValue("sec")} + assertJSONHas(t, CreateMessengerIntegrationInput{AppSecret: data.AppSecret.ValueStringPointer()}, `"appSecret":"sec"`) + assertJSONHas(t, UpdateMessengerIntegrationInput{AppSecret: data.AppSecret.ValueStringPointer()}, `"appSecret":"sec"`) + assertJSONLacks(t, CreateMessengerIntegrationInput{AppSecret: types.StringNull().ValueStringPointer()}, `"appSecret"`) + assertJSONLacks(t, CreateMessengerIntegrationInput{AccessToken: types.StringNull().ValueStringPointer()}, `"accessToken"`) + assertJSONHas(t, UpdateMessengerIntegrationInput{AppSecret: types.StringNull().ValueStringPointer()}, `"appSecret":null`) + assertJSONHas(t, UpdateMessengerIntegrationInput{AccessToken: types.StringNull().ValueStringPointer()}, `"accessToken":null`) + }) + + t.Run("whatsapp create/update carry appSecret", func(t *testing.T) { + data := WhatsAppIntegrationResourceModel{AppSecret: types.StringValue("sec")} + assertJSONHas(t, CreateWhatsAppIntegrationInput{AppSecret: data.AppSecret.ValueStringPointer()}, `"appSecret":"sec"`) + assertJSONHas(t, UpdateWhatsAppIntegrationInput{AppSecret: data.AppSecret.ValueStringPointer()}, `"appSecret":"sec"`) + assertJSONLacks(t, CreateWhatsAppIntegrationInput{AppSecret: types.StringNull().ValueStringPointer()}, `"appSecret"`) + assertJSONLacks(t, CreateWhatsAppIntegrationInput{AccessToken: types.StringNull().ValueStringPointer()}, `"accessToken"`) + assertJSONHas(t, UpdateWhatsAppIntegrationInput{}, `"appSecret":null`) + assertJSONHas(t, UpdateWhatsAppIntegrationInput{}, `"accessToken":null`) + }) + + t.Run("read responses decode appSecret and the mask is not applied to state", func(t *testing.T) { + raw := `{"id":"x","accessToken":"********","appSecret":"********","name":"n"}` + + var ig GetInstagramIntegrationResponse + var ms GetMessengerIntegrationResponse + var wa GetWhatsAppIntegrationResponse + for _, target := range []interface{}{&ig, &ms, &wa} { + if err := json.Unmarshal([]byte(raw), target); err != nil { + t.Fatalf("unmarshal: %v", err) + } + } + if ig.AppSecret == nil || ms.AppSecret == nil || wa.AppSecret == nil { + t.Fatalf("expected appSecret to decode on all three responses") + } + + state := InstagramIntegrationResourceModel{AccessToken: types.StringValue("tok"), AppSecret: types.StringValue("sec")} + state.AccessToken = sensitiveStringFromAPI(state.AccessToken, ig.AccessToken) + state.AppSecret = sensitiveStringFromAPI(state.AppSecret, ig.AppSecret) + if state.AccessToken.ValueString() != "tok" || state.AppSecret.ValueString() != "sec" { + t.Fatalf("expected masked values to keep state, got %+v", state) + } + }) + + t.Run("schemas declare app_secret as optional and sensitive", func(t *testing.T) { + for name, r := range map[string]resource.Resource{ + "instagram": NewInstagramIntegrationResource(), + "messenger": NewMessengerIntegrationResource(), + "whatsapp": NewWhatsAppIntegrationResource(), + } { + var resp resource.SchemaResponse + r.Schema(context.Background(), resource.SchemaRequest{}, &resp) + if resp.Diagnostics.HasError() { + t.Fatalf("%s: schema diagnostics: %v", name, resp.Diagnostics) + } + attr, ok := resp.Schema.Attributes["app_secret"].(schema.StringAttribute) + if !ok { + t.Fatalf("%s: expected app_secret string attribute, got %T", name, resp.Schema.Attributes["app_secret"]) + } + if !attr.Optional || !attr.Sensitive || attr.Required || attr.Computed { + t.Fatalf("%s: expected app_secret to be Optional+Sensitive, got %+v", name, attr) + } + } + }) +} diff --git a/internal/provider/resource_skillset_ability.go b/internal/provider/resource_skillset_ability.go index 0ae9c2f..a1dc42e 100644 --- a/internal/provider/resource_skillset_ability.go +++ b/internal/provider/resource_skillset_ability.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -157,18 +158,7 @@ func (r *SkillsetAbilityResource) Create(ctx context.Context, req resource.Creat // Call the ChatBotKit GraphQL API to create skillsetability - result, err := r.client.CreateSkillsetAbility(ctx, data.SkillsetId.ValueString(), CreateSkillsetAbilityInput{ - BlueprintId: data.BlueprintId.ValueStringPointer(), - BotId: data.BotId.ValueStringPointer(), - Description: data.Description.ValueStringPointer(), - FileId: data.FileId.ValueStringPointer(), - Instruction: data.Instruction.ValueStringPointer(), - Meta: convertMapToInterface(ctx, data.Meta), - Name: data.Name.ValueStringPointer(), - SecretId: data.SecretId.ValueStringPointer(), - SpaceId: data.SpaceId.ValueStringPointer(), - State: data.State.ValueStringPointer(), - }) + result, err := r.client.CreateSkillsetAbility(ctx, data.SkillsetId.ValueString(), skillsetAbilityCreateInputFromModel(ctx, data)) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create skillsetability: %s", err)) return @@ -209,44 +199,7 @@ func (r *SkillsetAbilityResource) Read(ctx context.Context, req resource.ReadReq // Update data model with response values - if result.BlueprintId != nil { - data.BlueprintId = types.StringPointerValue(result.BlueprintId) - } - if result.BotId != nil { - data.BotId = types.StringPointerValue(result.BotId) - } - if result.Description != nil { - data.Description = types.StringPointerValue(result.Description) - } - if result.FileId != nil { - data.FileId = types.StringPointerValue(result.FileId) - } - if result.Instruction != nil { - data.Instruction = types.StringPointerValue(result.Instruction) - } - if result.Meta != nil { - mapValue, diags := types.MapValueFrom(ctx, types.StringType, result.Meta) - resp.Diagnostics.Append(diags...) - data.Meta = mapValue - } - if result.Name != nil { - data.Name = types.StringPointerValue(result.Name) - } - if result.SecretId != nil { - data.SecretId = types.StringPointerValue(result.SecretId) - } - if result.SpaceId != nil { - data.SpaceId = types.StringPointerValue(result.SpaceId) - } - if result.State != nil { - data.State = types.StringPointerValue(result.State) - } - if result.CreatedAt != nil { - data.CreatedAt = types.StringPointerValue(result.CreatedAt) - } - if result.UpdatedAt != nil { - data.UpdatedAt = types.StringPointerValue(result.UpdatedAt) - } + resp.Diagnostics.Append(applySkillsetAbilityResultToModel(ctx, &data, result)...) // Save updated data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) @@ -265,18 +218,7 @@ func (r *SkillsetAbilityResource) Update(ctx context.Context, req resource.Updat // Call the ChatBotKit GraphQL API to update skillsetability - _, err := r.client.UpdateSkillsetAbility(ctx, data.SkillsetId.ValueString(), data.ID.ValueString(), UpdateSkillsetAbilityInput{ - BlueprintId: data.BlueprintId.ValueStringPointer(), - BotId: data.BotId.ValueStringPointer(), - Description: data.Description.ValueStringPointer(), - FileId: data.FileId.ValueStringPointer(), - Instruction: data.Instruction.ValueStringPointer(), - Meta: convertMapToInterface(ctx, data.Meta), - Name: data.Name.ValueStringPointer(), - SecretId: data.SecretId.ValueStringPointer(), - SpaceId: data.SpaceId.ValueStringPointer(), - State: data.State.ValueStringPointer(), - }) + _, err := r.client.UpdateSkillsetAbility(ctx, data.SkillsetId.ValueString(), data.ID.ValueString(), skillsetAbilityUpdateInputFromModel(ctx, data)) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update skillsetability: %s", err)) return @@ -308,5 +250,99 @@ func (r *SkillsetAbilityResource) Delete(ctx context.Context, req resource.Delet // ImportState imports the resource state from Terraform. func (r *SkillsetAbilityResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + skillsetId, abilityId, err := parseSkillsetAbilityImportID(req.ID) + if err != nil { + resp.Diagnostics.AddError("Invalid Import ID", err.Error()) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("skillset_id"), skillsetId)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), abilityId)...) +} + +// parseSkillsetAbilityImportID splits a `/` import +// ID into its parts. An ability can only be read through its skillset, so a +// bare ability ID is rejected with guidance on the expected format. +func parseSkillsetAbilityImportID(importID string) (skillsetId string, abilityId string, err error) { + parts := strings.Split(importID, "/") + if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" { + return "", "", fmt.Errorf("expected import ID in the format / (for example skillset_abc123/ability_def456), got %q", importID) + } + + return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), nil +} + +// skillsetAbilityCreateInputFromModel maps the HCL model to the GraphQL create +// input. The HCL attributes `secret_id`, `bot_id`, `file_id` and `space_id` +// correspond to the API's `linkedSecretId`, `linkedBotId`, `linkedFileId` and +// `linkedSpaceId` fields. +func skillsetAbilityCreateInputFromModel(ctx context.Context, data SkillsetAbilityResourceModel) CreateSkillsetAbilityInput { + return CreateSkillsetAbilityInput{ + BlueprintId: data.BlueprintId.ValueStringPointer(), + LinkedBotId: data.BotId.ValueStringPointer(), + Description: data.Description.ValueStringPointer(), + LinkedFileId: data.FileId.ValueStringPointer(), + Instruction: data.Instruction.ValueStringPointer(), + Meta: convertMapToInterface(ctx, data.Meta), + Name: data.Name.ValueStringPointer(), + LinkedSecretId: data.SecretId.ValueStringPointer(), + LinkedSpaceId: data.SpaceId.ValueStringPointer(), + State: data.State.ValueStringPointer(), + } +} + +// skillsetAbilityUpdateInputFromModel maps the HCL model to the GraphQL update +// input. See skillsetAbilityCreateInputFromModel for the linked field mapping. +func skillsetAbilityUpdateInputFromModel(ctx context.Context, data SkillsetAbilityResourceModel) UpdateSkillsetAbilityInput { + return UpdateSkillsetAbilityInput{ + BlueprintId: data.BlueprintId.ValueStringPointer(), + LinkedBotId: data.BotId.ValueStringPointer(), + Description: data.Description.ValueStringPointer(), + LinkedFileId: data.FileId.ValueStringPointer(), + Instruction: data.Instruction.ValueStringPointer(), + Meta: convertMapToInterface(ctx, data.Meta), + Name: data.Name.ValueStringPointer(), + LinkedSecretId: data.SecretId.ValueStringPointer(), + LinkedSpaceId: data.SpaceId.ValueStringPointer(), + State: data.State.ValueStringPointer(), + } +} + +// applySkillsetAbilityResultToModel copies an ability read from the API into +// the HCL model. Linked relations (`linkedBotId`, `linkedFileId`, +// `linkedSecretId`, `linkedSpaceId`) and `blueprintId` are always written so a +// link removed outside Terraform clears the state attribute to null. +func applySkillsetAbilityResultToModel(ctx context.Context, data *SkillsetAbilityResourceModel, result *GetSkillsetAbilityResponse) diag.Diagnostics { + var diagnostics diag.Diagnostics + + data.BlueprintId = types.StringPointerValue(result.BlueprintId) + data.BotId = types.StringPointerValue(result.LinkedBotId) + if result.Description != nil { + data.Description = types.StringPointerValue(result.Description) + } + data.FileId = types.StringPointerValue(result.LinkedFileId) + if result.Instruction != nil { + data.Instruction = types.StringPointerValue(result.Instruction) + } + if result.Meta != nil { + mapValue, diags := types.MapValueFrom(ctx, types.StringType, result.Meta) + diagnostics.Append(diags...) + data.Meta = mapValue + } + if result.Name != nil { + data.Name = types.StringPointerValue(result.Name) + } + data.SecretId = types.StringPointerValue(result.LinkedSecretId) + data.SpaceId = types.StringPointerValue(result.LinkedSpaceId) + if result.State != nil { + data.State = types.StringPointerValue(result.State) + } + if result.CreatedAt != nil { + data.CreatedAt = types.StringPointerValue(result.CreatedAt) + } + if result.UpdatedAt != nil { + data.UpdatedAt = types.StringPointerValue(result.UpdatedAt) + } + + return diagnostics } diff --git a/internal/provider/resource_skillset_ability_test.go b/internal/provider/resource_skillset_ability_test.go new file mode 100644 index 0000000..91f2504 --- /dev/null +++ b/internal/provider/resource_skillset_ability_test.go @@ -0,0 +1,243 @@ +package provider + +import ( + "context" + "encoding/json" + "strconv" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func testSkillsetAbilityModel() SkillsetAbilityResourceModel { + return SkillsetAbilityResourceModel{ + ID: types.StringValue("ability-1"), + SkillsetId: types.StringValue("skillset-1"), + BlueprintId: types.StringValue("blueprint-1"), + BotId: types.StringValue("bot-1"), + Description: types.StringValue("desc"), + FileId: types.StringValue("file-1"), + Instruction: types.StringValue("do the thing"), + Meta: types.MapNull(types.StringType), + Name: types.StringValue("lookup"), + SecretId: types.StringValue("secret-1"), + SpaceId: types.StringValue("space-1"), + State: types.StringValue("enabled"), + } +} + +func assertPtrEquals(t *testing.T, field string, got *string, want string) { + t.Helper() + if got == nil { + t.Fatalf("expected %s to be %q, got nil", field, want) + } + if *got != want { + t.Fatalf("expected %s to be %q, got %q", field, want, *got) + } +} + +func TestSkillsetAbilityCreateInputFromModel(t *testing.T) { + t.Run("carries all linked fields from the model", func(t *testing.T) { + input := skillsetAbilityCreateInputFromModel(context.Background(), testSkillsetAbilityModel()) + + assertPtrEquals(t, "LinkedSecretId", input.LinkedSecretId, "secret-1") + assertPtrEquals(t, "LinkedBotId", input.LinkedBotId, "bot-1") + assertPtrEquals(t, "LinkedFileId", input.LinkedFileId, "file-1") + assertPtrEquals(t, "LinkedSpaceId", input.LinkedSpaceId, "space-1") + assertPtrEquals(t, "BlueprintId", input.BlueprintId, "blueprint-1") + assertPtrEquals(t, "Name", input.Name, "lookup") + assertPtrEquals(t, "State", input.State, "enabled") + }) + + t.Run("sends explicit null for linked fields that are null in the model", func(t *testing.T) { + model := testSkillsetAbilityModel() + model.SecretId = types.StringNull() + model.BotId = types.StringNull() + model.FileId = types.StringNull() + model.SpaceId = types.StringNull() + model.BlueprintId = types.StringNull() + + input := skillsetAbilityUpdateInputFromModel(context.Background(), model) + + if input.LinkedSecretId != nil || input.LinkedBotId != nil || input.LinkedFileId != nil || input.LinkedSpaceId != nil || input.BlueprintId != nil { + t.Fatalf("expected null linked fields to map to nil, got %+v", input) + } + + // An update is authoritative for the config: a nil link must reach the + // platform as JSON null (clearing the link), not be omitted (keeping it). + body, err := json.Marshal(input) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + for _, want := range []string{ + `"linkedSecretId":null`, + `"linkedBotId":null`, + `"linkedFileId":null`, + `"linkedSpaceId":null`, + `"blueprintId":null`, + } { + if !strings.Contains(string(body), want) { + t.Fatalf("expected %s in update payload, got %s", want, body) + } + } + }) + + t.Run("create input still omits null linked fields", func(t *testing.T) { + model := testSkillsetAbilityModel() + model.SecretId = types.StringNull() + + body, err := json.Marshal(skillsetAbilityCreateInputFromModel(context.Background(), model)) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + if strings.Contains(string(body), `"linkedSecretId"`) { + t.Fatalf("expected create payload to omit null linkedSecretId, got %s", body) + } + }) +} + +func TestApplySkillsetAbilityResultToModel(t *testing.T) { + t.Run("sets state from all linked relations", func(t *testing.T) { + model := SkillsetAbilityResourceModel{ + ID: types.StringValue("ability-1"), + SkillsetId: types.StringValue("skillset-1"), + } + result := &GetSkillsetAbilityResponse{ + ID: ptr("ability-1"), + BlueprintId: ptr("blueprint-1"), + LinkedBotId: ptr("bot-1"), + Description: ptr("desc"), + LinkedFileId: ptr("file-1"), + Instruction: ptr("do the thing"), + Meta: map[string]interface{}{"kind": "search"}, + Name: ptr("lookup"), + LinkedSecretId: ptr("secret-1"), + LinkedSpaceId: ptr("space-1"), + State: ptr("enabled"), + CreatedAt: ptr("2026-01-01T00:00:00Z"), + UpdatedAt: ptr("2026-01-02T00:00:00Z"), + } + + diags := applySkillsetAbilityResultToModel(context.Background(), &model, result) + if diags.HasError() { + t.Fatalf("unexpected diagnostics: %v", diags) + } + + if model.SecretId.ValueString() != "secret-1" { + t.Fatalf("expected secret_id to be 'secret-1', got %q", model.SecretId.ValueString()) + } + if model.BotId.ValueString() != "bot-1" { + t.Fatalf("expected bot_id to be 'bot-1', got %q", model.BotId.ValueString()) + } + if model.FileId.ValueString() != "file-1" { + t.Fatalf("expected file_id to be 'file-1', got %q", model.FileId.ValueString()) + } + if model.SpaceId.ValueString() != "space-1" { + t.Fatalf("expected space_id to be 'space-1', got %q", model.SpaceId.ValueString()) + } + if model.BlueprintId.ValueString() != "blueprint-1" { + t.Fatalf("expected blueprint_id to be 'blueprint-1', got %q", model.BlueprintId.ValueString()) + } + if model.Name.ValueString() != "lookup" || model.Description.ValueString() != "desc" || model.Instruction.ValueString() != "do the thing" { + t.Fatalf("unexpected scalar fields: %+v", model) + } + if model.State.ValueString() != "enabled" { + t.Fatalf("expected state to be 'enabled', got %q", model.State.ValueString()) + } + if model.Meta.IsNull() || model.Meta.Elements()["kind"].(types.String).ValueString() != "search" { + t.Fatalf("unexpected meta: %v", model.Meta) + } + if model.CreatedAt.ValueString() != "2026-01-01T00:00:00Z" || model.UpdatedAt.ValueString() != "2026-01-02T00:00:00Z" { + t.Fatalf("unexpected timestamps: %+v", model) + } + }) + + t.Run("clears linked state fields to null when the relation is nil", func(t *testing.T) { + model := testSkillsetAbilityModel() + result := &GetSkillsetAbilityResponse{ + ID: ptr("ability-1"), + Name: ptr("lookup"), + } + + diags := applySkillsetAbilityResultToModel(context.Background(), &model, result) + if diags.HasError() { + t.Fatalf("unexpected diagnostics: %v", diags) + } + + if !model.SecretId.IsNull() { + t.Fatalf("expected secret_id to be null, got %q", model.SecretId.ValueString()) + } + if !model.BotId.IsNull() { + t.Fatalf("expected bot_id to be null, got %q", model.BotId.ValueString()) + } + if !model.FileId.IsNull() { + t.Fatalf("expected file_id to be null, got %q", model.FileId.ValueString()) + } + if !model.SpaceId.IsNull() { + t.Fatalf("expected space_id to be null, got %q", model.SpaceId.ValueString()) + } + if !model.BlueprintId.IsNull() { + t.Fatalf("expected blueprint_id to be null, got %q", model.BlueprintId.ValueString()) + } + // Non-link scalars keep prior state when absent from the response. + if model.Instruction.ValueString() != "do the thing" { + t.Fatalf("expected instruction to be preserved, got %q", model.Instruction.ValueString()) + } + }) + + t.Run("clears a single nil link while keeping the others", func(t *testing.T) { + model := testSkillsetAbilityModel() + result := &GetSkillsetAbilityResponse{ + ID: ptr("ability-1"), + LinkedBotId: ptr("bot-1"), + LinkedFileId: ptr("file-1"), + LinkedSecretId: nil, + LinkedSpaceId: ptr("space-1"), + } + + diags := applySkillsetAbilityResultToModel(context.Background(), &model, result) + if diags.HasError() { + t.Fatalf("unexpected diagnostics: %v", diags) + } + + if !model.SecretId.IsNull() { + t.Fatalf("expected secret_id to be null, got %q", model.SecretId.ValueString()) + } + if model.BotId.ValueString() != "bot-1" || model.FileId.ValueString() != "file-1" || model.SpaceId.ValueString() != "space-1" { + t.Fatalf("expected other links to be preserved, got %+v", model) + } + }) +} + +func TestParseSkillsetAbilityImportID(t *testing.T) { + t.Run("splits skillset and ability ids", func(t *testing.T) { + skillsetId, abilityId, err := parseSkillsetAbilityImportID("skillset_abc123/ability_def456") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if skillsetId != "skillset_abc123" || abilityId != "ability_def456" { + t.Fatalf("unexpected parts: %q / %q", skillsetId, abilityId) + } + }) + + t.Run("rejects a bare ability id with guidance", func(t *testing.T) { + _, _, err := parseSkillsetAbilityImportID("ability_def456") + if err == nil { + t.Fatal("expected an error for a bare ability id") + } + if !strings.Contains(err.Error(), "/") { + t.Fatalf("expected the error to explain the expected format, got %q", err.Error()) + } + }) + + for _, bad := range []string{"", "/", "skillset_abc123/", "/ability_def456", "a/b/c"} { + t.Run("rejects "+strconv.Quote(bad), func(t *testing.T) { + if _, _, err := parseSkillsetAbilityImportID(bad); err == nil { + t.Fatalf("expected %q to be rejected", bad) + } + }) + } +} diff --git a/internal/provider/resource_whatsapp_integration.go b/internal/provider/resource_whatsapp_integration.go index fe0b070..3c50f7a 100644 --- a/internal/provider/resource_whatsapp_integration.go +++ b/internal/provider/resource_whatsapp_integration.go @@ -33,6 +33,7 @@ type WhatsAppIntegrationResourceModel struct { ID types.String `tfsdk:"id"` AccessToken types.String `tfsdk:"access_token"` + AppSecret types.String `tfsdk:"app_secret"` Alias types.String `tfsdk:"alias"` AllowFrom types.String `tfsdk:"allow_from"` Attachments types.Bool `tfsdk:"attachments"` @@ -71,6 +72,11 @@ func (r *WhatsAppIntegrationResource) Schema(ctx context.Context, req resource.S Optional: true, Sensitive: true, }, + "app_secret": schema.StringAttribute{ + MarkdownDescription: "The WhatsApp Business app secret used to verify webhook signatures", + Optional: true, + Sensitive: true, + }, "alias": schema.StringAttribute{ MarkdownDescription: "The alias ID for the integration", Optional: true, @@ -161,6 +167,7 @@ func (r *WhatsAppIntegrationResource) Create(ctx context.Context, req resource.C result, err := r.client.CreateWhatsAppIntegration(ctx, CreateWhatsAppIntegrationInput{ AccessToken: data.AccessToken.ValueStringPointer(), + AppSecret: data.AppSecret.ValueStringPointer(), Alias: data.Alias.ValueStringPointer(), AllowFrom: data.AllowFrom.ValueStringPointer(), Attachments: data.Attachments.ValueBoolPointer(), @@ -213,9 +220,9 @@ func (r *WhatsAppIntegrationResource) Read(ctx context.Context, req resource.Rea // Update data model with response values - if result.AccessToken != nil { - data.AccessToken = types.StringPointerValue(result.AccessToken) - } + // Secrets come back masked once configured; keep the configured value. + data.AccessToken = sensitiveStringFromAPI(data.AccessToken, result.AccessToken) + data.AppSecret = sensitiveStringFromAPI(data.AppSecret, result.AppSecret) if result.Alias != nil { data.Alias = types.StringPointerValue(result.Alias) } @@ -277,6 +284,7 @@ func (r *WhatsAppIntegrationResource) Update(ctx context.Context, req resource.U _, err := r.client.UpdateWhatsAppIntegration(ctx, data.ID.ValueString(), UpdateWhatsAppIntegrationInput{ AccessToken: data.AccessToken.ValueStringPointer(), + AppSecret: data.AppSecret.ValueStringPointer(), Alias: data.Alias.ValueStringPointer(), AllowFrom: data.AllowFrom.ValueStringPointer(), Attachments: data.Attachments.ValueBoolPointer(), diff --git a/types/types.go b/types/types.go index dc29b68..a96cc55 100644 --- a/types/types.go +++ b/types/types.go @@ -7,18 +7,18 @@ package types type BlueprintVisibility string const ( - BlueprintVisibilityPrivate BlueprintVisibility = "private" + BlueprintVisibilityPrivate BlueprintVisibility = "private" BlueprintVisibilityProtected BlueprintVisibility = "protected" - BlueprintVisibilityPublic BlueprintVisibility = "public" + BlueprintVisibilityPublic BlueprintVisibility = "public" ) // BotVisibility Visibility options for bots type BotVisibility string const ( - BotVisibilityPrivate BotVisibility = "private" + BotVisibilityPrivate BotVisibility = "private" BotVisibilityProtected BotVisibility = "protected" - BotVisibilityPublic BotVisibility = "public" + BotVisibilityPublic BotVisibility = "public" ) // ContextBlueprintVisibility Visibility options for blueprints in the context of a user @@ -26,7 +26,7 @@ type ContextBlueprintVisibility string const ( ContextBlueprintVisibilityProtected ContextBlueprintVisibility = "protected" - ContextBlueprintVisibilityPublic ContextBlueprintVisibility = "public" + ContextBlueprintVisibilityPublic ContextBlueprintVisibility = "public" ) // ContextBotVisibility Visibility options for bots in the context of a user @@ -34,7 +34,7 @@ type ContextBotVisibility string const ( ContextBotVisibilityProtected ContextBotVisibility = "protected" - ContextBotVisibilityPublic ContextBotVisibility = "public" + ContextBotVisibilityPublic ContextBotVisibility = "public" ) // ContextDatasetVisibility Visibility options for datasets in the context of a user @@ -42,7 +42,7 @@ type ContextDatasetVisibility string const ( ContextDatasetVisibilityProtected ContextDatasetVisibility = "protected" - ContextDatasetVisibilityPublic ContextDatasetVisibility = "public" + ContextDatasetVisibilityPublic ContextDatasetVisibility = "public" ) // ContextFileVisibility Visibility options for files in the context of a user @@ -50,7 +50,7 @@ type ContextFileVisibility string const ( ContextFileVisibilityProtected ContextFileVisibility = "protected" - ContextFileVisibilityPublic ContextFileVisibility = "public" + ContextFileVisibilityPublic ContextFileVisibility = "public" ) // ContextSecretKind Kinds of secrets in the context of a user @@ -64,13 +64,13 @@ const ( type ContextSecretType string const ( - ContextSecretTypeBasic ContextSecretType = "basic" - ContextSecretTypeBearer ContextSecretType = "bearer" - ContextSecretTypeJwt ContextSecretType = "jwt" - ContextSecretTypeOauth ContextSecretType = "oauth" - ContextSecretTypePlain ContextSecretType = "plain" + ContextSecretTypeBasic ContextSecretType = "basic" + ContextSecretTypeBearer ContextSecretType = "bearer" + ContextSecretTypeJwt ContextSecretType = "jwt" + ContextSecretTypeOauth ContextSecretType = "oauth" + ContextSecretTypePlain ContextSecretType = "plain" ContextSecretTypeReference ContextSecretType = "reference" - ContextSecretTypeTemplate ContextSecretType = "template" + ContextSecretTypeTemplate ContextSecretType = "template" ) // ContextSecretVisibility Visibility options for secrets in the context of a user @@ -78,7 +78,7 @@ type ContextSecretVisibility string const ( ContextSecretVisibilityProtected ContextSecretVisibility = "protected" - ContextSecretVisibilityPublic ContextSecretVisibility = "public" + ContextSecretVisibilityPublic ContextSecretVisibility = "public" ) // ContextSkillsetVisibility Visibility options for skillsets in the context of a user @@ -86,39 +86,62 @@ type ContextSkillsetVisibility string const ( ContextSkillsetVisibilityProtected ContextSkillsetVisibility = "protected" - ContextSkillsetVisibilityPublic ContextSkillsetVisibility = "public" + ContextSkillsetVisibilityPublic ContextSkillsetVisibility = "public" ) // DatasetVisibility Visibility options for datasets type DatasetVisibility string const ( - DatasetVisibilityPrivate DatasetVisibility = "private" + DatasetVisibilityPrivate DatasetVisibility = "private" DatasetVisibilityProtected DatasetVisibility = "protected" - DatasetVisibilityPublic DatasetVisibility = "public" + DatasetVisibilityPublic DatasetVisibility = "public" ) // FileVisibility Visibility options for files type FileVisibility string const ( - FileVisibilityPrivate FileVisibility = "private" + FileVisibilityPrivate FileVisibility = "private" FileVisibilityProtected FileVisibility = "protected" - FileVisibilityPublic FileVisibility = "public" + FileVisibilityPublic FileVisibility = "public" +) + +// IntegrationVerificationActionType The type of action that can be performed for verification of the integration +type IntegrationVerificationActionType string + +const ( + IntegrationVerificationActionTypeInstall IntegrationVerificationActionType = "install" +) + +// IntegrationVerificationStatus The status of the verification for the integration +type IntegrationVerificationStatus string + +const ( + IntegrationVerificationStatusConfigured IntegrationVerificationStatus = "configured" + IntegrationVerificationStatusUnconfigured IntegrationVerificationStatus = "unconfigured" +) + +// ListOrder The order of items in a paginated list +type ListOrder string + +const ( + ListOrderAsc ListOrder = "asc" + ListOrderDesc ListOrder = "desc" ) // MessageType Types of messages in a conversation type MessageType string const ( - MessageTypeActivity MessageType = "activity" - MessageTypeBackstory MessageType = "backstory" - MessageTypeBot MessageType = "bot" - MessageTypeCheckpoint MessageType = "checkpoint" - MessageTypeContext MessageType = "context" + MessageTypeActivity MessageType = "activity" + MessageTypeBackstory MessageType = "backstory" + MessageTypeBot MessageType = "bot" + MessageTypeCheckpoint MessageType = "checkpoint" + MessageTypeContext MessageType = "context" MessageTypeInstruction MessageType = "instruction" - MessageTypeReasoning MessageType = "reasoning" - MessageTypeUser MessageType = "user" + MessageTypeReasoning MessageType = "reasoning" + MessageTypeUser MessageType = "user" ) // PolicyType Types of policies that can be used in the system @@ -126,7 +149,15 @@ type PolicyType string const ( PolicyTypeRetention PolicyType = "retention" - PolicyTypeUsage PolicyType = "usage" + PolicyTypeUsage PolicyType = "usage" +) + +// RatingSentiment The sentiment of a rating: upvote (value >= 0) or downvote (value < 0) +type RatingSentiment string + +const ( + RatingSentimentDownvote RatingSentiment = "downvote" + RatingSentimentUpvote RatingSentiment = "upvote" ) // ResourceState Lifecycle state for resources that can be toggled on/off without deletion @@ -134,23 +165,23 @@ type ResourceState string const ( ResourceStateDisabled ResourceState = "disabled" - ResourceStateEnabled ResourceState = "enabled" + ResourceStateEnabled ResourceState = "enabled" ) // Schedule Schedule options for trigger integrations type Schedule string const ( - ScheduleDaily Schedule = "daily" - ScheduleHalfhourly Schedule = "halfhourly" - ScheduleHourly Schedule = "hourly" - ScheduleMonthly Schedule = "monthly" - ScheduleNever Schedule = "never" + ScheduleDaily Schedule = "daily" + ScheduleHalfhourly Schedule = "halfhourly" + ScheduleHourly Schedule = "hourly" + ScheduleMonthly Schedule = "monthly" + ScheduleNever Schedule = "never" ScheduleQuarterhourly Schedule = "quarterhourly" - ScheduleTwicedaily Schedule = "twicedaily" - ScheduleTwicemonthly Schedule = "twicemonthly" - ScheduleTwiceweekly Schedule = "twiceweekly" - ScheduleWeekly Schedule = "weekly" + ScheduleTwicedaily Schedule = "twicedaily" + ScheduleTwicemonthly Schedule = "twicemonthly" + ScheduleTwiceweekly Schedule = "twiceweekly" + ScheduleWeekly Schedule = "weekly" ) // SecretContactVerificationActionType The type of action that can be performed for contact verification @@ -164,7 +195,7 @@ const ( type SecretContactVerificationStatus string const ( - SecretContactVerificationStatusAuthenticated SecretContactVerificationStatus = "authenticated" + SecretContactVerificationStatusAuthenticated SecretContactVerificationStatus = "authenticated" SecretContactVerificationStatusUnauthenticated SecretContactVerificationStatus = "unauthenticated" ) @@ -173,20 +204,20 @@ type SecretKind string const ( SecretKindPersonal SecretKind = "personal" - SecretKindShared SecretKind = "shared" + SecretKindShared SecretKind = "shared" ) // SecretType Types of secrets that can be used in the system type SecretType string const ( - SecretTypeBasic SecretType = "basic" - SecretTypeBearer SecretType = "bearer" - SecretTypeJwt SecretType = "jwt" - SecretTypeOauth SecretType = "oauth" - SecretTypePlain SecretType = "plain" + SecretTypeBasic SecretType = "basic" + SecretTypeBearer SecretType = "bearer" + SecretTypeJwt SecretType = "jwt" + SecretTypeOauth SecretType = "oauth" + SecretTypePlain SecretType = "plain" SecretTypeReference SecretType = "reference" - SecretTypeTemplate SecretType = "template" + SecretTypeTemplate SecretType = "template" ) // SecretVerificationActionType The type of action that can be performed for verification @@ -200,7 +231,7 @@ const ( type SecretVerificationStatus string const ( - SecretVerificationStatusAuthenticated SecretVerificationStatus = "authenticated" + SecretVerificationStatusAuthenticated SecretVerificationStatus = "authenticated" SecretVerificationStatusUnauthenticated SecretVerificationStatus = "unauthenticated" ) @@ -208,18 +239,18 @@ const ( type SecretVisibility string const ( - SecretVisibilityPrivate SecretVisibility = "private" + SecretVisibilityPrivate SecretVisibility = "private" SecretVisibilityProtected SecretVisibility = "protected" - SecretVisibilityPublic SecretVisibility = "public" + SecretVisibilityPublic SecretVisibility = "public" ) // SkillsetVisibility Visibility options for skillsets type SkillsetVisibility string const ( - SkillsetVisibilityPrivate SkillsetVisibility = "private" + SkillsetVisibilityPrivate SkillsetVisibility = "private" SkillsetVisibilityProtected SkillsetVisibility = "protected" - SkillsetVisibilityPublic SkillsetVisibility = "public" + SkillsetVisibilityPublic SkillsetVisibility = "public" ) // TaskOutcome Outcome of task execution @@ -236,41 +267,60 @@ type TaskStatus string const ( TaskStatusCanceled TaskStatus = "canceled" - TaskStatusIdle TaskStatus = "idle" - TaskStatusRunning TaskStatus = "running" + TaskStatusIdle TaskStatus = "idle" + TaskStatusRunning TaskStatus = "running" ) type Ability struct { // The blueprint associated with the ability Blueprint *Blueprint `json:"blueprint,omitempty"` - // The bot associated with the ability - Bot *Bot `json:"bot,omitempty"` // The date and time when the ability was created CreatedAt *string `json:"createdAt,omitempty"` // The description of the ability Description *string `json:"description,omitempty"` - // The file associated with the ability - File *File `json:"file,omitempty"` // The unique identifier of the ability ID *string `json:"id,omitempty"` // The instruction for the ability Instruction *string `json:"instruction,omitempty"` + // The bot the ability is linked to (the bot it acts on) + LinkedBot *Bot `json:"linkedBot,omitempty"` + // The file the ability is linked to (the file it acts on) + LinkedFile *File `json:"linkedFile,omitempty"` + // The secret the ability is linked to (the secret it acts with) + LinkedSecret *Secret `json:"linkedSecret,omitempty"` + // The space the ability is linked to (the space it acts on) + LinkedSpace *Space `json:"linkedSpace,omitempty"` // The metadata associated with the ability Meta map[string]interface{} `json:"meta,omitempty"` // The name of the ability Name *string `json:"name,omitempty"` - // The secret associated with the ability - Secret *Secret `json:"secret,omitempty"` // The skillset associated with the ability Skillset *Skillset `json:"skillset,omitempty"` - // The space associated with the ability - Space *Space `json:"space,omitempty"` // The lifecycle state of the ability (enabled/disabled) State *ResourceState `json:"state,omitempty"` // The date and time when the ability was last updated UpdatedAt *string `json:"updatedAt,omitempty"` } +type AnamIntegration struct { + // The blueprint associated with the anam integration + Blueprint *Blueprint `json:"blueprint,omitempty"` + // The bot associated with the anam integration + Bot *Bot `json:"bot,omitempty"` + // The date and time when the anam integration was created + CreatedAt *string `json:"createdAt,omitempty"` + // The description of the anam integration + Description *string `json:"description,omitempty"` + // The unique identifier of the anam integration + ID *string `json:"id,omitempty"` + // The metadata associated with the anam integration + Meta map[string]interface{} `json:"meta,omitempty"` + // The name of the anam integration + Name *string `json:"name,omitempty"` + // The date and time when the anam integration was last updated + UpdatedAt *string `json:"updatedAt,omitempty"` +} + type AuditLog struct { // The ID of the ability associated with this audit AbilityId *string `json:"abilityId,omitempty"` @@ -328,6 +378,25 @@ type AuditLog struct { WebhookId *string `json:"webhookId,omitempty"` } +type AvatarIntegration struct { + // The blueprint associated with the avatar integration + Blueprint *Blueprint `json:"blueprint,omitempty"` + // The bot associated with the avatar integration + Bot *Bot `json:"bot,omitempty"` + // The date and time when the avatar integration was created + CreatedAt *string `json:"createdAt,omitempty"` + // The description of the avatar integration + Description *string `json:"description,omitempty"` + // The unique identifier of the avatar integration + ID *string `json:"id,omitempty"` + // The metadata associated with the avatar integration + Meta map[string]interface{} `json:"meta,omitempty"` + // The name of the avatar integration + Name *string `json:"name,omitempty"` + // The date and time when the avatar integration was last updated + UpdatedAt *string `json:"updatedAt,omitempty"` +} + type Blueprint struct { // The abilities associated with the blueprint Abilities interface{} `json:"abilities,omitempty"` @@ -414,12 +483,16 @@ type Bot struct { Backstory *string `json:"backstory,omitempty"` // The blueprint associated with the bot Blueprint *Blueprint `json:"blueprint,omitempty"` + // The ID of the blueprint associated with the bot + BlueprintId *string `json:"blueprintId,omitempty"` // The conversations associated with the bot Conversations interface{} `json:"conversations,omitempty"` // The date and time when the bot was created CreatedAt *string `json:"createdAt,omitempty"` // The dataset associated with the bot Dataset *Dataset `json:"dataset,omitempty"` + // The ID of the dataset associated with the bot + DatasetId *string `json:"datasetId,omitempty"` // The description of the bot Description *string `json:"description,omitempty"` // The unique identifier of the bot @@ -440,6 +513,8 @@ type Bot struct { Ratings interface{} `json:"ratings,omitempty"` // The skillset associated with the bot Skillset *Skillset `json:"skillset,omitempty"` + // The ID of the skillset associated with the bot + SkillsetId *string `json:"skillsetId,omitempty"` // The tasks associated with the bot Task interface{} `json:"task,omitempty"` // The date and time when the bot was last updated @@ -738,6 +813,8 @@ type Conversation struct { CreatedAt *string `json:"createdAt,omitempty"` // The description of the conversation Description *string `json:"description,omitempty"` + // The date and time when the conversation expires + ExpiresAt *string `json:"expiresAt,omitempty"` // The unique identifier of the conversation ID *string `json:"id,omitempty"` // The messages in the conversation @@ -759,6 +836,8 @@ type Conversation struct { type Dataset struct { // The blueprint associated with the dataset Blueprint *Blueprint `json:"blueprint,omitempty"` + // The ID of the blueprint associated with the dataset + BlueprintId *string `json:"blueprintId,omitempty"` // The bots associated with the dataset Bots interface{} `json:"bots,omitempty"` // The date and time when the dataset was created @@ -803,8 +882,6 @@ type DatasetCreateRequest struct { SearchMinScore *float64 `json:"searchMinScore,omitempty"` // The separators for chunking text Separators *string `json:"separators,omitempty"` - // The storage backend to use - Store *string `json:"store,omitempty"` // The visibility level of the dataset Visibility *DatasetVisibility `json:"visibility,omitempty"` } @@ -884,6 +961,8 @@ type DiscordIntegration struct { SessionDuration *float64 `json:"sessionDuration,omitempty"` // The date and time when the discord integration was last updated UpdatedAt *string `json:"updatedAt,omitempty"` + // Whether the integration holds every credential it needs to carry traffic, and how to install it if not + Verification IntegrationVerification `json:"verification"` } // DiscordIntegrationCreateRequest Input parameters for creating a new Discord integration @@ -989,6 +1068,8 @@ type EmailIntegration struct { SessionDuration *float64 `json:"sessionDuration,omitempty"` // The date and time when the email integration was last updated UpdatedAt *string `json:"updatedAt,omitempty"` + // Whether the integration holds every credential it needs to carry traffic, and how to install it if not + Verification IntegrationVerification `json:"verification"` } // EmailIntegrationCreateRequest Input parameters for creating a new Email integration @@ -1184,6 +1265,8 @@ type ExtractIntegrationUpdateResponse struct { type File struct { // The blueprint associated with the file Blueprint *Blueprint `json:"blueprint,omitempty"` + // The ID of the blueprint associated with the file + BlueprintId *string `json:"blueprintId,omitempty"` // The date and time when the file was created CreatedAt *string `json:"createdAt,omitempty"` // The description of the file @@ -1248,6 +1331,25 @@ type FileUpdateResponse struct { ID *string `json:"id,omitempty"` } +type GithubIntegration struct { + // The blueprint associated with the github integration + Blueprint *Blueprint `json:"blueprint,omitempty"` + // The bot associated with the github integration + Bot *Bot `json:"bot,omitempty"` + // The date and time when the github integration was created + CreatedAt *string `json:"createdAt,omitempty"` + // The description of the github integration + Description *string `json:"description,omitempty"` + // The unique identifier of the github integration + ID *string `json:"id,omitempty"` + // The metadata associated with the github integration + Meta map[string]interface{} `json:"meta,omitempty"` + // The name of the github integration + Name *string `json:"name,omitempty"` + // The date and time when the github integration was last updated + UpdatedAt *string `json:"updatedAt,omitempty"` +} + type GooglechatIntegration struct { // The allowed senders for the Google Chat integration AllowFrom *string `json:"allowFrom,omitempty"` @@ -1273,6 +1375,8 @@ type GooglechatIntegration struct { SessionDuration *float64 `json:"sessionDuration,omitempty"` // The date and time when the Google Chat integration was last updated UpdatedAt *string `json:"updatedAt,omitempty"` + // Whether the integration holds every credential it needs to carry traffic, and how to install it if not + Verification IntegrationVerification `json:"verification"` } // GooglechatIntegrationCreateRequest Input parameters for creating a new Google Chat integration @@ -1422,6 +1526,8 @@ type InstagramIntegration struct { SessionDuration *float64 `json:"sessionDuration,omitempty"` // The date and time when the instagram integration was last updated UpdatedAt *string `json:"updatedAt,omitempty"` + // Whether the integration holds every credential it needs to carry traffic, and how to install it if not + Verification IntegrationVerification `json:"verification"` } // InstagramIntegrationCreateRequest Input parameters for creating a new Instagram integration @@ -1430,6 +1536,8 @@ type InstagramIntegrationCreateRequest struct { AccessToken *string `json:"accessToken,omitempty"` // The alias ID for the integration Alias *string `json:"alias,omitempty"` + // The Meta app secret used to validate webhook signatures + AppSecret *string `json:"appSecret,omitempty"` // Whether to enable file attachments Attachments *bool `json:"attachments,omitempty"` // The ID of the blueprint to use @@ -1466,6 +1574,8 @@ type InstagramIntegrationUpdateRequest struct { AccessToken *string `json:"accessToken,omitempty"` // The alias ID for the integration Alias *string `json:"alias,omitempty"` + // The Meta app secret used to validate webhook signatures + AppSecret *string `json:"appSecret,omitempty"` // Whether to enable file attachments Attachments *bool `json:"attachments,omitempty"` // The ID of the blueprint to use @@ -1490,6 +1600,20 @@ type InstagramIntegrationUpdateResponse struct { ID *string `json:"id,omitempty"` } +type IntegrationVerification struct { + // The actions available for the verification + Action *IntegrationVerificationAction `json:"action,omitempty"` + // The verification status of the integration + Status IntegrationVerificationStatus `json:"status"` +} + +type IntegrationVerificationAction struct { + // The type of action that can be performed for verification + Type IntegrationVerificationActionType `json:"type"` + // The URL to perform the action for verification + URL *string `json:"url,omitempty"` +} + type McpserverIntegration struct { // The blueprint associated with the MCP server integration Blueprint *Blueprint `json:"blueprint,omitempty"` @@ -1564,10 +1688,16 @@ type McpserverIntegrationUpdateResponse struct { } type Memory struct { + // The ID of the bot the memory is scoped to + BotId *string `json:"botId,omitempty"` + // The ID of the contact the memory is scoped to + ContactId *string `json:"contactId,omitempty"` // The date and time when the memory was created CreatedAt *string `json:"createdAt,omitempty"` // The description of the memory Description *string `json:"description,omitempty"` + // The date and time when the memory expires + ExpiresAt *string `json:"expiresAt,omitempty"` // The unique identifier of the memory ID *string `json:"id,omitempty"` // The metadata associated with the memory @@ -1628,6 +1758,8 @@ type MessengerIntegration struct { SessionDuration *float64 `json:"sessionDuration,omitempty"` // The date and time when the messenger integration was last updated UpdatedAt *string `json:"updatedAt,omitempty"` + // Whether the integration holds every credential it needs to carry traffic, and how to install it if not + Verification IntegrationVerification `json:"verification"` } // MessengerIntegrationCreateRequest Input parameters for creating a new Messenger integration @@ -1636,6 +1768,8 @@ type MessengerIntegrationCreateRequest struct { AccessToken *string `json:"accessToken,omitempty"` // The alias ID for the integration Alias *string `json:"alias,omitempty"` + // The Meta app secret used to validate webhook signatures + AppSecret *string `json:"appSecret,omitempty"` // Whether to enable file attachments Attachments *bool `json:"attachments,omitempty"` // The ID of the blueprint to use @@ -1672,6 +1806,8 @@ type MessengerIntegrationUpdateRequest struct { AccessToken *string `json:"accessToken,omitempty"` // The alias ID for the integration Alias *string `json:"alias,omitempty"` + // The Meta app secret used to validate webhook signatures + AppSecret *string `json:"appSecret,omitempty"` // Whether to enable file attachments Attachments *bool `json:"attachments,omitempty"` // The ID of the blueprint to use @@ -1721,6 +1857,8 @@ type MicrosoftteamsIntegration struct { SessionDuration *float64 `json:"sessionDuration,omitempty"` // The date and time when the Microsoft Teams integration was last updated UpdatedAt *string `json:"updatedAt,omitempty"` + // Whether the integration holds every credential it needs to carry traffic, and how to install it if not + Verification IntegrationVerification `json:"verification"` } // MicrosoftteamsIntegrationCreateRequest Input parameters for creating a new Microsoft Teams integration @@ -1798,101 +1936,101 @@ type MicrosoftteamsIntegrationUpdateResponse struct { } type Mutation struct { - ClonePlatformExample *ClonePlatformExampleResult `json:"clonePlatformExample,omitempty"` - CreateBlueprint *BlueprintCreateResponse `json:"createBlueprint,omitempty"` - CreateBot *BotCreateResponse `json:"createBot,omitempty"` - CreateContext *ContextCreateResponse `json:"createContext,omitempty"` - CreateDataset *DatasetCreateResponse `json:"createDataset,omitempty"` - CreateDiscordIntegration *DiscordIntegrationCreateResponse `json:"createDiscordIntegration,omitempty"` - CreateEmailIntegration *EmailIntegrationCreateResponse `json:"createEmailIntegration,omitempty"` - CreateExtractIntegration *ExtractIntegrationCreateResponse `json:"createExtractIntegration,omitempty"` - CreateFile *FileCreateResponse `json:"createFile,omitempty"` - CreateGooglechatIntegration *GooglechatIntegrationCreateResponse `json:"createGooglechatIntegration,omitempty"` - CreateInstagramIntegration *InstagramIntegrationCreateResponse `json:"createInstagramIntegration,omitempty"` - CreateMcpserverIntegration *McpserverIntegrationCreateResponse `json:"createMcpserverIntegration,omitempty"` - CreateMessengerIntegration *MessengerIntegrationCreateResponse `json:"createMessengerIntegration,omitempty"` + ClonePlatformExample *ClonePlatformExampleResult `json:"clonePlatformExample,omitempty"` + CreateBlueprint *BlueprintCreateResponse `json:"createBlueprint,omitempty"` + CreateBot *BotCreateResponse `json:"createBot,omitempty"` + CreateContext *ContextCreateResponse `json:"createContext,omitempty"` + CreateDataset *DatasetCreateResponse `json:"createDataset,omitempty"` + CreateDiscordIntegration *DiscordIntegrationCreateResponse `json:"createDiscordIntegration,omitempty"` + CreateEmailIntegration *EmailIntegrationCreateResponse `json:"createEmailIntegration,omitempty"` + CreateExtractIntegration *ExtractIntegrationCreateResponse `json:"createExtractIntegration,omitempty"` + CreateFile *FileCreateResponse `json:"createFile,omitempty"` + CreateGooglechatIntegration *GooglechatIntegrationCreateResponse `json:"createGooglechatIntegration,omitempty"` + CreateInstagramIntegration *InstagramIntegrationCreateResponse `json:"createInstagramIntegration,omitempty"` + CreateMcpserverIntegration *McpserverIntegrationCreateResponse `json:"createMcpserverIntegration,omitempty"` + CreateMessengerIntegration *MessengerIntegrationCreateResponse `json:"createMessengerIntegration,omitempty"` CreateMicrosoftteamsIntegration *MicrosoftteamsIntegrationCreateResponse `json:"createMicrosoftteamsIntegration,omitempty"` - CreateNotionIntegration *NotionIntegrationCreateResponse `json:"createNotionIntegration,omitempty"` - CreatePolicy *PolicyCreateResponse `json:"createPolicy,omitempty"` - CreatePortal *PortalCreateResponse `json:"createPortal,omitempty"` - CreateSecret *SecretCreateResponse `json:"createSecret,omitempty"` - CreateSitemapIntegration *SitemapIntegrationCreateResponse `json:"createSitemapIntegration,omitempty"` - CreateSkillserverIntegration *SkillserverIntegrationCreateResponse `json:"createSkillserverIntegration,omitempty"` - CreateSkillset *SkillsetCreateResponse `json:"createSkillset,omitempty"` - CreateSkillsetAbility *SkillsetAbilityCreateResponse `json:"createSkillsetAbility,omitempty"` - CreateSlackIntegration *SlackIntegrationCreateResponse `json:"createSlackIntegration,omitempty"` - CreateSpace *SpaceCreateResponse `json:"createSpace,omitempty"` - CreateSpaceSite *SpaceSiteCreateResponse `json:"createSpaceSite,omitempty"` - CreateSupportIntegration *SupportIntegrationCreateResponse `json:"createSupportIntegration,omitempty"` - CreateTask *TaskCreateResponse `json:"createTask,omitempty"` - CreateTelegramIntegration *TelegramIntegrationCreateResponse `json:"createTelegramIntegration,omitempty"` - CreateTriggerIntegration *TriggerIntegrationCreateResponse `json:"createTriggerIntegration,omitempty"` - CreateTwilioIntegration *TwilioIntegrationCreateResponse `json:"createTwilioIntegration,omitempty"` - CreateWhatsAppIntegration *WhatsAppIntegrationCreateResponse `json:"createWhatsAppIntegration,omitempty"` - CreateWidgetIntegration *WidgetIntegrationCreateResponse `json:"createWidgetIntegration,omitempty"` - DeleteBlueprint *BlueprintDeleteResponse `json:"deleteBlueprint,omitempty"` - DeleteBot *BotDeleteResponse `json:"deleteBot,omitempty"` - DeleteContext *ContextDeleteResponse `json:"deleteContext,omitempty"` - DeleteDataset *DatasetDeleteResponse `json:"deleteDataset,omitempty"` - DeleteDiscordIntegration *DiscordIntegrationDeleteResponse `json:"deleteDiscordIntegration,omitempty"` - DeleteEmailIntegration *EmailIntegrationDeleteResponse `json:"deleteEmailIntegration,omitempty"` - DeleteExtractIntegration *ExtractIntegrationDeleteResponse `json:"deleteExtractIntegration,omitempty"` - DeleteFile *FileDeleteResponse `json:"deleteFile,omitempty"` - DeleteGooglechatIntegration *GooglechatIntegrationDeleteResponse `json:"deleteGooglechatIntegration,omitempty"` - DeleteInstagramIntegration *InstagramIntegrationDeleteResponse `json:"deleteInstagramIntegration,omitempty"` - DeleteMcpserverIntegration *McpserverIntegrationDeleteResponse `json:"deleteMcpserverIntegration,omitempty"` - DeleteMessengerIntegration *MessengerIntegrationDeleteResponse `json:"deleteMessengerIntegration,omitempty"` + CreateNotionIntegration *NotionIntegrationCreateResponse `json:"createNotionIntegration,omitempty"` + CreatePolicy *PolicyCreateResponse `json:"createPolicy,omitempty"` + CreatePortal *PortalCreateResponse `json:"createPortal,omitempty"` + CreateSecret *SecretCreateResponse `json:"createSecret,omitempty"` + CreateSitemapIntegration *SitemapIntegrationCreateResponse `json:"createSitemapIntegration,omitempty"` + CreateSkillserverIntegration *SkillserverIntegrationCreateResponse `json:"createSkillserverIntegration,omitempty"` + CreateSkillset *SkillsetCreateResponse `json:"createSkillset,omitempty"` + CreateSkillsetAbility *SkillsetAbilityCreateResponse `json:"createSkillsetAbility,omitempty"` + CreateSlackIntegration *SlackIntegrationCreateResponse `json:"createSlackIntegration,omitempty"` + CreateSpace *SpaceCreateResponse `json:"createSpace,omitempty"` + CreateSpaceSite *SpaceSiteCreateResponse `json:"createSpaceSite,omitempty"` + CreateSupportIntegration *SupportIntegrationCreateResponse `json:"createSupportIntegration,omitempty"` + CreateTask *TaskCreateResponse `json:"createTask,omitempty"` + CreateTelegramIntegration *TelegramIntegrationCreateResponse `json:"createTelegramIntegration,omitempty"` + CreateTriggerIntegration *TriggerIntegrationCreateResponse `json:"createTriggerIntegration,omitempty"` + CreateTwilioIntegration *TwilioIntegrationCreateResponse `json:"createTwilioIntegration,omitempty"` + CreateWhatsAppIntegration *WhatsAppIntegrationCreateResponse `json:"createWhatsAppIntegration,omitempty"` + CreateWidgetIntegration *WidgetIntegrationCreateResponse `json:"createWidgetIntegration,omitempty"` + DeleteBlueprint *BlueprintDeleteResponse `json:"deleteBlueprint,omitempty"` + DeleteBot *BotDeleteResponse `json:"deleteBot,omitempty"` + DeleteContext *ContextDeleteResponse `json:"deleteContext,omitempty"` + DeleteDataset *DatasetDeleteResponse `json:"deleteDataset,omitempty"` + DeleteDiscordIntegration *DiscordIntegrationDeleteResponse `json:"deleteDiscordIntegration,omitempty"` + DeleteEmailIntegration *EmailIntegrationDeleteResponse `json:"deleteEmailIntegration,omitempty"` + DeleteExtractIntegration *ExtractIntegrationDeleteResponse `json:"deleteExtractIntegration,omitempty"` + DeleteFile *FileDeleteResponse `json:"deleteFile,omitempty"` + DeleteGooglechatIntegration *GooglechatIntegrationDeleteResponse `json:"deleteGooglechatIntegration,omitempty"` + DeleteInstagramIntegration *InstagramIntegrationDeleteResponse `json:"deleteInstagramIntegration,omitempty"` + DeleteMcpserverIntegration *McpserverIntegrationDeleteResponse `json:"deleteMcpserverIntegration,omitempty"` + DeleteMessengerIntegration *MessengerIntegrationDeleteResponse `json:"deleteMessengerIntegration,omitempty"` DeleteMicrosoftteamsIntegration *MicrosoftteamsIntegrationDeleteResponse `json:"deleteMicrosoftteamsIntegration,omitempty"` - DeleteNotionIntegration *NotionIntegrationDeleteResponse `json:"deleteNotionIntegration,omitempty"` - DeletePolicy *PolicyDeleteResponse `json:"deletePolicy,omitempty"` - DeletePortal *PortalDeleteResponse `json:"deletePortal,omitempty"` - DeleteSecret *SecretDeleteResponse `json:"deleteSecret,omitempty"` - DeleteSitemapIntegration *SitemapIntegrationDeleteResponse `json:"deleteSitemapIntegration,omitempty"` - DeleteSkillserverIntegration *SkillserverIntegrationDeleteResponse `json:"deleteSkillserverIntegration,omitempty"` - DeleteSkillset *SkillsetDeleteResponse `json:"deleteSkillset,omitempty"` - DeleteSkillsetAbility *SkillsetAbilityDeleteResponse `json:"deleteSkillsetAbility,omitempty"` - DeleteSlackIntegration *SlackIntegrationDeleteResponse `json:"deleteSlackIntegration,omitempty"` - DeleteSpace *SpaceDeleteResponse `json:"deleteSpace,omitempty"` - DeleteSpaceSite *SpaceSiteDeleteResponse `json:"deleteSpaceSite,omitempty"` - DeleteSupportIntegration *SupportIntegrationDeleteResponse `json:"deleteSupportIntegration,omitempty"` - DeleteTask *TaskDeleteResponse `json:"deleteTask,omitempty"` - DeleteTelegramIntegration *TelegramIntegrationDeleteResponse `json:"deleteTelegramIntegration,omitempty"` - DeleteTriggerIntegration *TriggerIntegrationDeleteResponse `json:"deleteTriggerIntegration,omitempty"` - DeleteTwilioIntegration *TwilioIntegrationDeleteResponse `json:"deleteTwilioIntegration,omitempty"` - DeleteWhatsAppIntegration *WhatsAppIntegrationDeleteResponse `json:"deleteWhatsAppIntegration,omitempty"` - DeleteWidgetIntegration *WidgetIntegrationDeleteResponse `json:"deleteWidgetIntegration,omitempty"` - RevokeSecret *SecretRevokeResponse `json:"revokeSecret,omitempty"` - UpdateBlueprint *BlueprintUpdateResponse `json:"updateBlueprint,omitempty"` - UpdateBot *BotUpdateResponse `json:"updateBot,omitempty"` - UpdateContext *ContextUpdateResponse `json:"updateContext,omitempty"` - UpdateDataset *DatasetUpdateResponse `json:"updateDataset,omitempty"` - UpdateDiscordIntegration *DiscordIntegrationUpdateResponse `json:"updateDiscordIntegration,omitempty"` - UpdateEmailIntegration *EmailIntegrationUpdateResponse `json:"updateEmailIntegration,omitempty"` - UpdateExtractIntegration *ExtractIntegrationUpdateResponse `json:"updateExtractIntegration,omitempty"` - UpdateFile *FileUpdateResponse `json:"updateFile,omitempty"` - UpdateGooglechatIntegration *GooglechatIntegrationUpdateResponse `json:"updateGooglechatIntegration,omitempty"` - UpdateInstagramIntegration *InstagramIntegrationUpdateResponse `json:"updateInstagramIntegration,omitempty"` - UpdateMcpserverIntegration *McpserverIntegrationUpdateResponse `json:"updateMcpserverIntegration,omitempty"` - UpdateMessengerIntegration *MessengerIntegrationUpdateResponse `json:"updateMessengerIntegration,omitempty"` + DeleteNotionIntegration *NotionIntegrationDeleteResponse `json:"deleteNotionIntegration,omitempty"` + DeletePolicy *PolicyDeleteResponse `json:"deletePolicy,omitempty"` + DeletePortal *PortalDeleteResponse `json:"deletePortal,omitempty"` + DeleteSecret *SecretDeleteResponse `json:"deleteSecret,omitempty"` + DeleteSitemapIntegration *SitemapIntegrationDeleteResponse `json:"deleteSitemapIntegration,omitempty"` + DeleteSkillserverIntegration *SkillserverIntegrationDeleteResponse `json:"deleteSkillserverIntegration,omitempty"` + DeleteSkillset *SkillsetDeleteResponse `json:"deleteSkillset,omitempty"` + DeleteSkillsetAbility *SkillsetAbilityDeleteResponse `json:"deleteSkillsetAbility,omitempty"` + DeleteSlackIntegration *SlackIntegrationDeleteResponse `json:"deleteSlackIntegration,omitempty"` + DeleteSpace *SpaceDeleteResponse `json:"deleteSpace,omitempty"` + DeleteSpaceSite *SpaceSiteDeleteResponse `json:"deleteSpaceSite,omitempty"` + DeleteSupportIntegration *SupportIntegrationDeleteResponse `json:"deleteSupportIntegration,omitempty"` + DeleteTask *TaskDeleteResponse `json:"deleteTask,omitempty"` + DeleteTelegramIntegration *TelegramIntegrationDeleteResponse `json:"deleteTelegramIntegration,omitempty"` + DeleteTriggerIntegration *TriggerIntegrationDeleteResponse `json:"deleteTriggerIntegration,omitempty"` + DeleteTwilioIntegration *TwilioIntegrationDeleteResponse `json:"deleteTwilioIntegration,omitempty"` + DeleteWhatsAppIntegration *WhatsAppIntegrationDeleteResponse `json:"deleteWhatsAppIntegration,omitempty"` + DeleteWidgetIntegration *WidgetIntegrationDeleteResponse `json:"deleteWidgetIntegration,omitempty"` + RevokeSecret *SecretRevokeResponse `json:"revokeSecret,omitempty"` + UpdateBlueprint *BlueprintUpdateResponse `json:"updateBlueprint,omitempty"` + UpdateBot *BotUpdateResponse `json:"updateBot,omitempty"` + UpdateContext *ContextUpdateResponse `json:"updateContext,omitempty"` + UpdateDataset *DatasetUpdateResponse `json:"updateDataset,omitempty"` + UpdateDiscordIntegration *DiscordIntegrationUpdateResponse `json:"updateDiscordIntegration,omitempty"` + UpdateEmailIntegration *EmailIntegrationUpdateResponse `json:"updateEmailIntegration,omitempty"` + UpdateExtractIntegration *ExtractIntegrationUpdateResponse `json:"updateExtractIntegration,omitempty"` + UpdateFile *FileUpdateResponse `json:"updateFile,omitempty"` + UpdateGooglechatIntegration *GooglechatIntegrationUpdateResponse `json:"updateGooglechatIntegration,omitempty"` + UpdateInstagramIntegration *InstagramIntegrationUpdateResponse `json:"updateInstagramIntegration,omitempty"` + UpdateMcpserverIntegration *McpserverIntegrationUpdateResponse `json:"updateMcpserverIntegration,omitempty"` + UpdateMessengerIntegration *MessengerIntegrationUpdateResponse `json:"updateMessengerIntegration,omitempty"` UpdateMicrosoftteamsIntegration *MicrosoftteamsIntegrationUpdateResponse `json:"updateMicrosoftteamsIntegration,omitempty"` - UpdateNotionIntegration *NotionIntegrationUpdateResponse `json:"updateNotionIntegration,omitempty"` - UpdatePolicy *PolicyUpdateResponse `json:"updatePolicy,omitempty"` - UpdatePortal *PortalUpdateResponse `json:"updatePortal,omitempty"` - UpdateSecret *SecretUpdateResponse `json:"updateSecret,omitempty"` - UpdateSitemapIntegration *SitemapIntegrationUpdateResponse `json:"updateSitemapIntegration,omitempty"` - UpdateSkillserverIntegration *SkillserverIntegrationUpdateResponse `json:"updateSkillserverIntegration,omitempty"` - UpdateSkillset *SkillsetUpdateResponse `json:"updateSkillset,omitempty"` - UpdateSkillsetAbility *SkillsetAbilityUpdateResponse `json:"updateSkillsetAbility,omitempty"` - UpdateSlackIntegration *SlackIntegrationUpdateResponse `json:"updateSlackIntegration,omitempty"` - UpdateSpace *SpaceUpdateResponse `json:"updateSpace,omitempty"` - UpdateSpaceSite *SpaceSiteUpdateResponse `json:"updateSpaceSite,omitempty"` - UpdateSupportIntegration *SupportIntegrationUpdateResponse `json:"updateSupportIntegration,omitempty"` - UpdateTask *TaskUpdateResponse `json:"updateTask,omitempty"` - UpdateTelegramIntegration *TelegramIntegrationUpdateResponse `json:"updateTelegramIntegration,omitempty"` - UpdateTriggerIntegration *TriggerIntegrationUpdateResponse `json:"updateTriggerIntegration,omitempty"` - UpdateTwilioIntegration *TwilioIntegrationUpdateResponse `json:"updateTwilioIntegration,omitempty"` - UpdateWhatsAppIntegration *WhatsAppIntegrationUpdateResponse `json:"updateWhatsAppIntegration,omitempty"` - UpdateWidgetIntegration *WidgetIntegrationUpdateResponse `json:"updateWidgetIntegration,omitempty"` + UpdateNotionIntegration *NotionIntegrationUpdateResponse `json:"updateNotionIntegration,omitempty"` + UpdatePolicy *PolicyUpdateResponse `json:"updatePolicy,omitempty"` + UpdatePortal *PortalUpdateResponse `json:"updatePortal,omitempty"` + UpdateSecret *SecretUpdateResponse `json:"updateSecret,omitempty"` + UpdateSitemapIntegration *SitemapIntegrationUpdateResponse `json:"updateSitemapIntegration,omitempty"` + UpdateSkillserverIntegration *SkillserverIntegrationUpdateResponse `json:"updateSkillserverIntegration,omitempty"` + UpdateSkillset *SkillsetUpdateResponse `json:"updateSkillset,omitempty"` + UpdateSkillsetAbility *SkillsetAbilityUpdateResponse `json:"updateSkillsetAbility,omitempty"` + UpdateSlackIntegration *SlackIntegrationUpdateResponse `json:"updateSlackIntegration,omitempty"` + UpdateSpace *SpaceUpdateResponse `json:"updateSpace,omitempty"` + UpdateSpaceSite *SpaceSiteUpdateResponse `json:"updateSpaceSite,omitempty"` + UpdateSupportIntegration *SupportIntegrationUpdateResponse `json:"updateSupportIntegration,omitempty"` + UpdateTask *TaskUpdateResponse `json:"updateTask,omitempty"` + UpdateTelegramIntegration *TelegramIntegrationUpdateResponse `json:"updateTelegramIntegration,omitempty"` + UpdateTriggerIntegration *TriggerIntegrationUpdateResponse `json:"updateTriggerIntegration,omitempty"` + UpdateTwilioIntegration *TwilioIntegrationUpdateResponse `json:"updateTwilioIntegration,omitempty"` + UpdateWhatsAppIntegration *WhatsAppIntegrationUpdateResponse `json:"updateWhatsAppIntegration,omitempty"` + UpdateWidgetIntegration *WidgetIntegrationUpdateResponse `json:"updateWidgetIntegration,omitempty"` } type NotionIntegration struct { @@ -2044,33 +2182,6 @@ type PlatformAction struct { UpdatedAt *string `json:"updatedAt,omitempty"` } -type PlatformDoc struct { - // The category of the platform content doc - Category *string `json:"category,omitempty"` - // The content of the platform content doc. Fetches full content from API when requested. - Content *string `json:"content,omitempty"` - // The date and time when the platform content doc was created - CreatedAt *string `json:"createdAt,omitempty"` - // The description of the platform content doc - Description *string `json:"description,omitempty"` - // The excerpt of the platform content doc - Excerpt *string `json:"excerpt,omitempty"` - // The unique identifier of the platform content doc - ID *string `json:"id,omitempty"` - // The index of the platform content doc - Index *int64 `json:"index,omitempty"` - // The URL of the platform content doc - Link *string `json:"link,omitempty"` - // The metadata associated with the platform content doc - Meta map[string]interface{} `json:"meta,omitempty"` - // The name of the platform content doc - Name *string `json:"name,omitempty"` - // The tags associated with the platform content doc - Tags []string `json:"tags,omitempty"` - // The date and time when the platform content doc was last updated - UpdatedAt *string `json:"updatedAt,omitempty"` -} - type PlatformExample struct { // The configuration of the platform example. Fetches full config from API when requested. Config map[string]interface{} `json:"config,omitempty"` @@ -2094,33 +2205,6 @@ type PlatformExample struct { UpdatedAt *string `json:"updatedAt,omitempty"` } -type PlatformManual struct { - // The category of the platform content manual - Category *string `json:"category,omitempty"` - // The content of the platform content manual. Fetches full content from API when requested. - Content *string `json:"content,omitempty"` - // The date and time when the platform content manual was created - CreatedAt *string `json:"createdAt,omitempty"` - // The description of the platform content manual - Description *string `json:"description,omitempty"` - // The excerpt of the platform content manual - Excerpt *string `json:"excerpt,omitempty"` - // The unique identifier of the platform content manual - ID *string `json:"id,omitempty"` - // The index of the platform content manual - Index *int64 `json:"index,omitempty"` - // The URL of the platform content manual - Link *string `json:"link,omitempty"` - // The metadata associated with the platform content manual - Meta map[string]interface{} `json:"meta,omitempty"` - // The name of the platform content manual - Name *string `json:"name,omitempty"` - // The tags associated with the platform content manual - Tags []string `json:"tags,omitempty"` - // The date and time when the platform content manual was last updated - UpdatedAt *string `json:"updatedAt,omitempty"` -} - type PlatformModel struct { // The date and time when the platform model was created CreatedAt *string `json:"createdAt,omitempty"` @@ -2200,27 +2284,6 @@ type PlatformSecret struct { UpdatedAt *string `json:"updatedAt,omitempty"` } -type PlatformTutorial struct { - // The category of the platform content tutorial - Category *string `json:"category,omitempty"` - // The content of the platform content tutorial. Fetches full content from API when requested. - Content *string `json:"content,omitempty"` - // The description of the platform content tutorial - Description *string `json:"description,omitempty"` - // The excerpt of the platform content tutorial - Excerpt *string `json:"excerpt,omitempty"` - // The unique identifier of the platform content tutorial - ID *string `json:"id,omitempty"` - // The index of the platform content tutorial - Index *int64 `json:"index,omitempty"` - // The URL of the platform content tutorial - Link *string `json:"link,omitempty"` - // The name of the platform content tutorial - Name *string `json:"name,omitempty"` - // The tags associated with the platform content tutorial - Tags []string `json:"tags,omitempty"` -} - type Policy struct { // The alias ID for the policy Alias *string `json:"alias,omitempty"` @@ -2240,6 +2303,8 @@ type Policy struct { Meta map[string]interface{} `json:"meta,omitempty"` // The name of the policy Name *string `json:"name,omitempty"` + // The lifecycle state of the policy (enabled/disabled) + State *ResourceState `json:"state,omitempty"` // The type of the policy Type *PolicyType `json:"type,omitempty"` // The date and time when the policy was last updated @@ -2262,6 +2327,8 @@ type PolicyCreateRequest struct { Meta map[string]interface{} `json:"meta,omitempty"` // The name of the policy Name *string `json:"name,omitempty"` + // The lifecycle state of the policy (enabled/disabled) + State *ResourceState `json:"state,omitempty"` // The type of the policy Type PolicyType `json:"type"` } @@ -2294,6 +2361,8 @@ type PolicyUpdateRequest struct { Meta map[string]interface{} `json:"meta,omitempty"` // The name of the policy Name *string `json:"name,omitempty"` + // The lifecycle state of the policy (enabled/disabled) + State *ResourceState `json:"state,omitempty"` // The type of the policy Type *PolicyType `json:"type,omitempty"` } @@ -2307,6 +2376,8 @@ type PolicyUpdateResponse struct { type Portal struct { // The blueprint associated with the portal Blueprint *Blueprint `json:"blueprint,omitempty"` + // The ID of the blueprint associated with the portal + BlueprintId *string `json:"blueprintId,omitempty"` // The configuration of the portal Config map[string]interface{} `json:"config,omitempty"` // The date and time when the portal was created @@ -2323,6 +2394,8 @@ type Portal struct { Slug *string `json:"slug,omitempty"` // The date and time when the portal was last updated UpdatedAt *string `json:"updatedAt,omitempty"` + // The URL the portal is served at on this deployment, derived from its slug and the deployment portal topology + URL *string `json:"url,omitempty"` } // PortalCreateRequest Input parameters for creating a new portal @@ -2406,12 +2479,33 @@ type Rating struct { Value *int64 `json:"value,omitempty"` } +type RecallIntegration struct { + // The blueprint associated with the recall integration + Blueprint *Blueprint `json:"blueprint,omitempty"` + // The bot associated with the recall integration + Bot *Bot `json:"bot,omitempty"` + // The date and time when the recall integration was created + CreatedAt *string `json:"createdAt,omitempty"` + // The description of the recall integration + Description *string `json:"description,omitempty"` + // The unique identifier of the recall integration + ID *string `json:"id,omitempty"` + // The metadata associated with the recall integration + Meta map[string]interface{} `json:"meta,omitempty"` + // The name of the recall integration + Name *string `json:"name,omitempty"` + // The date and time when the recall integration was last updated + UpdatedAt *string `json:"updatedAt,omitempty"` +} + type Secret struct { // The abilities associated with the secret Abilities interface{} `json:"abilities,omitempty"` // The blueprint associated with the secret Blueprint *Blueprint `json:"blueprint,omitempty"` - // The configuration of the secret + // The ID of the blueprint associated with the secret + BlueprintId *string `json:"blueprintId,omitempty"` + // The configuration of the secret (config.clientSecret is returned as '********' if configured, null otherwise) Config map[string]interface{} `json:"config,omitempty"` // The contacts associated with the secret Contacts []SecretContact `json:"contacts,omitempty"` @@ -2725,6 +2819,8 @@ type Skillset struct { Abilities interface{} `json:"abilities,omitempty"` // The blueprint associated with the skillset Blueprint *Blueprint `json:"blueprint,omitempty"` + // The ID of the blueprint associated with the skillset + BlueprintId *string `json:"blueprintId,omitempty"` // The bots associated with the skillset Bots interface{} `json:"bots,omitempty"` // The date and time when the skillset was created @@ -2745,24 +2841,26 @@ type Skillset struct { // SkillsetAbilityCreateRequest Input parameters for creating a new skillset ability type SkillsetAbilityCreateRequest struct { + // The alias ID for the ability + Alias *string `json:"alias,omitempty"` // The ID of the blueprint to use BlueprintId *string `json:"blueprintId,omitempty"` - // The ID of the bot to use - BotId *string `json:"botId,omitempty"` // The description of the ability Description *string `json:"description,omitempty"` - // The ID of the file to use - FileId *string `json:"fileId,omitempty"` // The instruction for the ability Instruction *string `json:"instruction,omitempty"` + // The ID of the bot the ability is linked to + LinkedBotId *string `json:"linkedBotId,omitempty"` + // The ID of the file the ability is linked to + LinkedFileId *string `json:"linkedFileId,omitempty"` + // The ID of the secret the ability is linked to + LinkedSecretId *string `json:"linkedSecretId,omitempty"` + // The ID of the space the ability is linked to + LinkedSpaceId *string `json:"linkedSpaceId,omitempty"` // Additional metadata for the ability Meta map[string]interface{} `json:"meta,omitempty"` // The name of the ability Name *string `json:"name,omitempty"` - // The ID of the secret to use for authentication - SecretId *string `json:"secretId,omitempty"` - // The ID of the space to use - SpaceId *string `json:"spaceId,omitempty"` // The lifecycle state of the ability (enabled/disabled) State *ResourceState `json:"state,omitempty"` } @@ -2781,24 +2879,26 @@ type SkillsetAbilityDeleteResponse struct { // SkillsetAbilityUpdateRequest Input parameters for updating an existing skillset ability type SkillsetAbilityUpdateRequest struct { + // The alias ID for the ability + Alias *string `json:"alias,omitempty"` // The ID of the blueprint to use BlueprintId *string `json:"blueprintId,omitempty"` - // The ID of the bot to use - BotId *string `json:"botId,omitempty"` // The description of the ability Description *string `json:"description,omitempty"` - // The ID of the file to use - FileId *string `json:"fileId,omitempty"` // The instruction for the ability Instruction *string `json:"instruction,omitempty"` + // The ID of the bot the ability is linked to + LinkedBotId *string `json:"linkedBotId,omitempty"` + // The ID of the file the ability is linked to + LinkedFileId *string `json:"linkedFileId,omitempty"` + // The ID of the secret the ability is linked to + LinkedSecretId *string `json:"linkedSecretId,omitempty"` + // The ID of the space the ability is linked to + LinkedSpaceId *string `json:"linkedSpaceId,omitempty"` // Additional metadata for the ability Meta map[string]interface{} `json:"meta,omitempty"` // The name of the ability Name *string `json:"name,omitempty"` - // The ID of the secret to use for authentication - SecretId *string `json:"secretId,omitempty"` - // The ID of the space to use - SpaceId *string `json:"spaceId,omitempty"` // The lifecycle state of the ability (enabled/disabled) State *ResourceState `json:"state,omitempty"` } @@ -2894,6 +2994,8 @@ type SlackIntegration struct { SessionDuration *float64 `json:"sessionDuration,omitempty"` // The date and time when the slack integration was last updated UpdatedAt *string `json:"updatedAt,omitempty"` + // Whether the integration holds every credential it needs to carry traffic, and how to install it if not + Verification IntegrationVerification `json:"verification"` // The number of visible messages outside of the new thread VisibleMessages *int64 `json:"visibleMessages,omitempty"` } @@ -3054,8 +3156,6 @@ type SpaceSite struct { CreatedAt *string `json:"createdAt,omitempty"` // The description of the space site Description *string `json:"description,omitempty"` - // The host the site is served at (a