feat(site): built-in SDK integrations and maintainer tiers in the catalog (#3766)

Co-authored-by: strandly-the-agent <strandly-the-agent@users.noreply.github.com>
This commit is contained in:
Jonathan Segev
2026-08-14 11:42:07 -04:00
committed by GitHub
co-authored by strandly-the-agent
parent d2567ce5ec
commit 766e4f4e3d
70 changed files with 836 additions and 77 deletions
+36 -6
View File
@@ -188,10 +188,15 @@ jobs:
// parses yes/on as strings, which the schema's z.boolean()
// rejects at the build gate — only true booleans can reach main.
function editorialState(data) {
if (data === null) return { featured: true, badges: true };
if (data === null) return { featured: true, badges: true, teamTier: true, partnerTier: true };
const tier = typeof data.maintainedBy === 'string' ? data.maintainedBy : 'community';
return {
featured: data.featured === true,
badges: Array.isArray(data.badges) && data.badges.length > 0,
// `strands` and `aws` are team-granted; `partner` needs a
// maintainer to verify the vendor relationship.
teamTier: tier === 'strands' || tier === 'aws',
partnerTier: tier === 'partner',
};
}
@@ -215,6 +220,7 @@ jobs:
// entry is flagged whenever its head state differs from its
// base state (a brand-new file compares against empty).
const flagged = [];
const partnerFlagged = [];
const changedPackages = [];
for (const file of catalogFiles) {
// A renamed file lives at previous_filename on the base ref;
@@ -228,9 +234,16 @@ jobs:
const headData = parseEntry(headText);
const head = editorialState(headData);
const base = editorialState(parseEntry(baseText));
if (head.featured !== base.featured || head.badges !== base.badges) {
if (
head.featured !== base.featured ||
head.badges !== base.badges ||
head.teamTier !== base.teamTier
) {
flagged.push(file.filename);
}
if (head.partnerTier !== base.partnerTier) {
partnerFlagged.push(file.filename);
}
if (headText) {
const { github: entryGithub, pkgs } = extractEntryData(headData);
for (const p of pkgs) {
@@ -239,7 +252,7 @@ jobs:
}
}
if (flagged.length === 0 && changedPackages.length === 0) {
if (flagged.length === 0 && partnerFlagged.length === 0 && changedPackages.length === 0) {
console.log('No editorial changes or packages in touched entries — nothing to flag.');
await clearStaleFlag();
return;
@@ -325,7 +338,7 @@ jobs:
if (status !== 'MATCH') provenance.push({ ...p, status, detail });
}
if (flagged.length === 0 && provenance.length === 0) {
if (flagged.length === 0 && partnerFlagged.length === 0 && provenance.length === 0) {
console.log('Provenance verified and no editorial changes — nothing to flag.');
await clearStaleFlag();
return;
@@ -334,6 +347,9 @@ jobs:
if (flagged.length > 0) {
console.log(`Editorial fields changed by non-maintainer "${author}" in: ${flagged.join(', ')}`);
}
if (partnerFlagged.length > 0) {
console.log(`Partner tier changed by non-maintainer "${author}" in: ${partnerFlagged.join(', ')}`);
}
if (!(await headStillCurrent())) return;
@@ -343,7 +359,7 @@ jobs:
// exist in the repo, so create it on first use. A label failure
// must not prevent the sticky comment below — the comment is the
// primary signal.
if (flagged.length > 0) {
if (flagged.length > 0 || partnerFlagged.length > 0) {
try {
try {
await github.rest.issues.getLabel({
@@ -383,7 +399,7 @@ jobs:
'',
'#### Editorial catalog fields detected',
'',
`This PR changes \`featured\` and/or \`badges\` on: ${flagged.map(f => `\`${f}\``).join(', ')}.`,
`This PR changes \`featured\`, \`badges\`, and/or a team-granted \`maintainedBy\` tier (\`strands\`/\`aws\`) on: ${flagged.map(f => `\`${f}\``).join(', ')}.`,
'',
'Those fields are granted by the Strands team, not set by submitters',
'(see the [submission guide](https://strandsagents.com/docs/integrations/get-featured/)).',
@@ -392,6 +408,20 @@ jobs:
'submitter to revert it** before merging.'
);
}
if (partnerFlagged.length > 0) {
sections.push(
'',
'#### Partner maintainer tier claimed',
'',
`This PR adds or removes \`maintainedBy: partner\` on: ${partnerFlagged.map(f => `\`${f}\``).join(', ')}.`,
'',
'That tier means the integration is vended and supported by the company',
"behind it. Before merging, a maintainer must verify the entry's `github`",
'org is the official org of that company (and its packages are published',
'by them) — an integration published from a personal account stays',
'`community` regardless of what it integrates.'
);
}
if (provenance.length > 0) {
sections.push(
'',
+46 -16
View File
@@ -12,8 +12,8 @@
*/
import type { CatalogCardModel } from '../../util/catalog'
import { pathWithBase } from '../../util/links'
import { DOWNLOAD_ICON, GITHUB_ICON, NPM_ICON, PYPI_ICON, STAR_ICON } from './icons'
import { TYPE_ICONS, TYPE_LABELS, VERIFIED_ICON } from './types'
import { AWS_ICON, DOWNLOAD_ICON, GITHUB_ICON, NPM_ICON, PYPI_ICON, STAR_ICON } from './icons'
import { AWS_MARK_VIEWBOX, MAINTAINED_BY_FLAGS, MAINTAINED_BY_TITLES, TYPE_ICONS, TYPE_LABELS, VERIFIED_ICON } from './types'
interface Props {
card: CatalogCardModel
@@ -32,13 +32,26 @@ const href = card.external ? card.href : pathWithBase(card.href)
---
<article
class:list={['cat-card', { 'cat-card--featured': card.featured }]}
class:list={['cat-card', { 'cat-card--featured': card.featured, 'cat-card--builtin': card.maintainedBy === 'strands' }]}
data-type={card.integrationType}
data-langs={card.languages.join(' ')}
data-badges={[...card.badges, ...(card.featured ? ['featured'] : [])].join(' ')}
data-maintained={card.maintainedBy}
data-sdk={card.sdk}
data-search={`${card.name} ${card.description} by ${card.maintainer}`.toLowerCase()}
>
{
(card.featured || card.badges.includes('new')) && (
<span class="cat-corner">
{card.featured && (
<span class="cat-tab cat-tab--featured" title="Featured by the Strands team">
Featured
</span>
)}
{card.badges.includes('new') && <span class="cat-tab cat-tab--new">New</span>}
</span>
)
}
<span class="cat-tile" aria-hidden="true">
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={TYPE_ICONS[card.integrationType]} />
@@ -58,20 +71,8 @@ const href = card.external ? card.href : pathWithBase(card.href)
title={card.name}
>
{card.name}
{card.external && <span class="cat-external" aria-hidden="true">↗</span>}
</a>
{card.external && <span class="cat-external" aria-hidden="true">↗</span>}
{card.featured && <span class="cat-flag cat-flag--featured" title="Featured by the Strands team">Featured</span>}
{
card.badges.includes('verified') && (
<span class="cat-flag cat-flag--verified" title="Verified by the Strands team">
<svg width="12" height="12" viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={VERIFIED_ICON} />
</svg>
Verified
</span>
)
}
{card.badges.includes('new') && <span class="cat-flag cat-flag--new">New</span>}
<span class="cat-card-stats">
{
card.stars !== undefined && (
@@ -94,6 +95,35 @@ const href = card.external ? card.href : pathWithBase(card.href)
)
}
</span>
{/* Badges hold the fixed right slot: every non-community card has one,
while stats are sparse and flow after the title instead. */}
{(card.badges.includes('verified') || card.maintainedBy !== 'community') && (
<span class="cat-card-flags">
{card.badges.includes('verified') && (
<span class="cat-flag cat-flag--verified" title="Verified by the Strands team">
<svg width="12" height="12" viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={VERIFIED_ICON} />
</svg>
Verified
</span>
)}
{card.maintainedBy !== 'community' && (
<span
class:list={['cat-flag', `cat-flag--${card.maintainedBy}`]}
title={MAINTAINED_BY_TITLES[card.maintainedBy]}
aria-label={MAINTAINED_BY_FLAGS[card.maintainedBy]}
>
{card.maintainedBy === 'aws' ? (
<svg width="31" height="19" viewBox={AWS_MARK_VIEWBOX} aria-hidden="true">
<path fill="currentColor" d={AWS_ICON} />
</svg>
) : (
MAINTAINED_BY_FLAGS[card.maintainedBy]
)}
</span>
)}
</span>
)}
</div>
<p class="cat-card-desc">{card.description}</p>
<div class="cat-card-foot">
@@ -6,17 +6,18 @@
* segments and the left rail's type/badge chips.
*/
import CatalogSegments from './CatalogSegments.astro'
import { TYPE_ICONS, VERIFIED_ICON, type FacetOption } from './types'
import { AWS_MARK_VIEWBOX, MAINTAINED_BY_ICONS, TYPE_ICONS, VERIFIED_ICON, type FacetOption } from './types'
interface Props {
types: FacetOption[]
maintainedBy: FacetOption[]
showSdkFacet: boolean
hasVerified: boolean
hasFeatured: boolean
hasNew: boolean
}
const { types, showSdkFacet, hasVerified, hasFeatured, hasNew } = Astro.props
const { types, maintainedBy, showSdkFacet, hasVerified, hasFeatured, hasNew } = Astro.props
---
<div class="cat-facet-row">
@@ -46,6 +47,22 @@ const { types, showSdkFacet, hasVerified, hasFeatured, hasNew } = Astro.props
)
}
</div>
<div class="cat-type-rail" role="group" aria-label="Maintained by">
{
maintainedBy.map((t) => (
<button class="cat-chip" type="button" data-cat-maintained={t.value} aria-label={t.label} aria-pressed="false">
{MAINTAINED_BY_ICONS[t.value] ? (
<svg width="29" height="18" viewBox={AWS_MARK_VIEWBOX} aria-hidden="true">
<path fill="currentColor" d={MAINTAINED_BY_ICONS[t.value]} />
</svg>
) : (
t.label
)}
<span class="cat-count">{t.count}</span>
</button>
))
}
</div>
<div class="cat-type-rail" role="group" aria-label="Type">
{
types.map((t) => (
+4
View File
@@ -25,3 +25,7 @@ export const PYPI_ICON: readonly string[] = ICONS.python
export const NPM_ICON: readonly string[] = [
'M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z',
]
/** AWS smile mark (24x24, fill=currentColor) for aws-maintained entries. */
export const AWS_ICON =
'M6.763 10.036c0 .296.032.535.088.71.064.176.144.368.256.576.04.063.056.127.056.183 0 .08-.048.16-.152.24l-.503.335a.383.383 0 0 1-.208.072c-.08 0-.16-.04-.239-.112a2.47 2.47 0 0 1-.287-.375 6.18 6.18 0 0 1-.248-.471c-.622.734-1.405 1.101-2.347 1.101-.67 0-1.205-.191-1.596-.574-.391-.384-.59-.894-.59-1.533 0-.678.239-1.23.726-1.644.487-.415 1.133-.623 1.955-.623.272 0 .551.024.846.064.296.04.6.104.918.176v-.583c0-.607-.127-1.03-.375-1.277-.255-.248-.686-.367-1.3-.367-.28 0-.568.031-.863.103-.295.072-.583.16-.862.272a2.287 2.287 0 0 1-.28.104.488.488 0 0 1-.127.023c-.112 0-.168-.08-.168-.247v-.391c0-.128.016-.224.056-.28a.597.597 0 0 1 .224-.167c.279-.144.614-.264 1.005-.36a4.84 4.84 0 0 1 1.246-.151c.95 0 1.644.216 2.091.647.439.43.662 1.085.662 1.963v2.586zm-3.24 1.214c.263 0 .534-.048.822-.144.287-.096.543-.271.758-.51.128-.152.224-.32.272-.512.047-.191.08-.423.08-.694v-.335a6.66 6.66 0 0 0-.735-.136 6.02 6.02 0 0 0-.75-.048c-.535 0-.926.104-1.19.32-.263.215-.39.518-.39.917 0 .375.095.655.295.846.191.2.47.296.838.296zm6.41.862c-.144 0-.24-.024-.304-.08-.064-.048-.12-.16-.168-.311L7.586 5.55a1.398 1.398 0 0 1-.072-.32c0-.128.064-.2.191-.2h.783c.151 0 .255.025.31.08.065.048.113.16.16.312l1.342 5.284 1.245-5.284c.04-.16.088-.264.151-.312a.549.549 0 0 1 .32-.08h.638c.152 0 .256.025.32.08.063.048.12.16.151.312l1.261 5.348 1.381-5.348c.048-.16.104-.264.16-.312a.52.52 0 0 1 .311-.08h.743c.127 0 .2.065.2.2 0 .04-.009.08-.017.128a1.137 1.137 0 0 1-.056.2l-1.923 6.17c-.048.16-.104.263-.168.311a.51.51 0 0 1-.303.08h-.687c-.151 0-.255-.024-.32-.08-.063-.056-.119-.16-.15-.32l-1.238-5.148-1.23 5.14c-.04.16-.087.264-.15.32-.065.056-.177.08-.32.08zm10.256.215c-.415 0-.83-.048-1.229-.143-.399-.096-.71-.2-.918-.32-.128-.071-.215-.151-.247-.223a.563.563 0 0 1-.048-.224v-.407c0-.167.064-.247.183-.247.048 0 .096.008.144.024.048.016.12.048.2.08.271.12.566.215.878.279.319.064.63.096.95.096.502 0 .894-.088 1.165-.264a.86.86 0 0 0 .415-.758.777.777 0 0 0-.215-.559c-.144-.151-.416-.287-.807-.415l-1.157-.36c-.583-.183-1.014-.454-1.277-.813a1.902 1.902 0 0 1-.4-1.158c0-.335.073-.63.216-.886.144-.255.335-.479.575-.654.24-.184.51-.32.83-.415.32-.096.655-.136 1.006-.136.175 0 .359.008.535.032.183.024.35.056.518.088.16.04.312.08.455.127.144.048.256.096.336.144a.69.69 0 0 1 .24.2.43.43 0 0 1 .071.263v.375c0 .168-.064.256-.184.256a.83.83 0 0 1-.303-.096 3.652 3.652 0 0 0-1.532-.311c-.455 0-.815.071-1.062.223-.248.152-.375.383-.375.71 0 .224.08.416.24.567.159.152.454.304.877.44l1.134.358c.574.184.99.44 1.237.767.247.327.367.702.367 1.117 0 .343-.072.655-.207.926-.144.272-.336.511-.583.703-.248.2-.543.343-.886.447-.36.111-.734.167-1.142.167zM21.698 16.207c-2.626 1.94-6.442 2.969-9.722 2.969-4.598 0-8.74-1.7-11.87-4.526-.247-.223-.024-.527.272-.351 3.384 1.963 7.559 3.153 11.877 3.153 2.914 0 6.114-.607 9.06-1.852.439-.2.814.287.383.607zM22.792 14.961c-.336-.43-2.22-.207-3.074-.103-.255.032-.295-.192-.063-.36 1.5-1.053 3.967-.75 4.254-.399.287.36-.08 2.826-1.485 4.007-.215.184-.423.088-.327-.151.32-.79 1.03-2.57.695-2.994z'
+25
View File
@@ -1,3 +1,5 @@
import { AWS_ICON } from './icons'
// Canonical registry of catalog integration types: filter value, display
// labels, and a 24x24 single-path glyph (fill=currentColor) rendered in cards
// and facet chips. The zod enum in src/content.config.ts lists the same
@@ -72,6 +74,29 @@ export const CATALOG_TYPES = [
export const TYPE_LABELS: Record<string, string> = Object.fromEntries(CATALOG_TYPES.map((t) => [t.value, t.label]))
export const TYPE_ICONS: Record<string, string> = Object.fromEntries(CATALOG_TYPES.map((t) => [t.value, t.icon]))
// Maintainer tiers, highest commitment first: who stands behind an
// integration and what support users can infer. `label` names the facet chip;
// `flag` names the card badge (an SDK entry's badge reads "Built-in", not the
// team name). Values match the zod enum in src/content.config.ts — keep the
// two in sync.
export const MAINTAINED_BY_TIERS = [
{ value: 'strands', label: 'Strands', flag: 'Built-in', title: 'Built into the Strands SDK' },
{ value: 'aws', label: 'AWS', flag: 'AWS', title: 'Maintained by AWS' },
{ value: 'partner', label: 'Partner', flag: 'Partner', title: 'Maintained by the partner behind the integration' },
{ value: 'community', label: 'Community', flag: 'Community', title: 'Maintained by a community developer' },
] as const
export const MAINTAINED_BY_FLAGS: Record<string, string> = Object.fromEntries(
MAINTAINED_BY_TIERS.map((t) => [t.value, t.flag])
)
export const MAINTAINED_BY_TITLES: Record<string, string> = Object.fromEntries(
MAINTAINED_BY_TIERS.map((t) => [t.value, t.title])
)
/** Tier glyphs for facet chips; only AWS carries a brand mark. */
export const MAINTAINED_BY_ICONS: Record<string, string> = { aws: AWS_ICON }
/** Crops AWS_ICON's square canvas to the mark's measured bounds (y 4.8-19.2). */
export const AWS_MARK_VIEWBOX = '0 4.6 24 14.8'
/** Shield-check glyph for the verified badge (cards and the verified facet). */
export const VERIFIED_ICON =
'M12 1 3 5v6c0 5.6 3.8 10.7 9 12 5.2-1.3 9-6.4 9-12V5l-9-4zm-1.5 15L7 12.5 8.4 11l2.1 2.1 5.1-5.1L17 9.4 10.5 16z'
+5
View File
@@ -150,6 +150,11 @@ export const catalogEntrySchema = z
// on-site docsPage, the card's primary link prefers this over the bare
// GitHub repo so users land on usage instructions.
docsUrl: z.string().url().startsWith('https://', 'docsUrl must be https').optional(),
// Who stands behind the integration, shown and filtered on the catalog.
// `strands` (SDK built-ins) and `aws` are granted by the Strands team;
// `partner` requires a maintainer to verify the entry is vended by the
// integrator's official org; submitters leave it unset (community).
maintainedBy: z.enum(['strands', 'aws', 'partner', 'community']).default('community'),
// Editorial fields — maintainer-granted only; submitters leave them unset.
featured: z.boolean().default(false),
badges: z.array(z.enum(['verified'])).default([]),
+1
View File
@@ -1,6 +1,7 @@
name: ag-ui
description: Connect agents to rich clients like CopilotKit through the AG-UI protocol, with streaming chat and tool-based generative UI
integrationType: integration
maintainedBy: partner
languages:
python:
package: ag-ui-strands
@@ -1,6 +1,7 @@
name: agent-control
description: Runtime control plane that evaluates every agent input and output against configurable policies to block prompt injection, PII leakage, and rogue tool calls.
integrationType: plugin
maintainedBy: partner
languages:
python:
package: agent-control-sdk[strands-agents]
@@ -0,0 +1,10 @@
name: Agent Skills
description: Give agents on-demand access to specialized instructions with progressive disclosure that keeps the system prompt lean.
integrationType: plugin
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_plugins/skills
docsPage: docs/user-guide/concepts/plugins/skills
addedDate: 2025-12-01
@@ -1,6 +1,7 @@
name: AgentCore Memory Store
description: Amazon Bedrock AgentCore Memory as a Strands MemoryStore, with long-term recall and server-side memory extraction from conversation turns.
integrationType: memory-store
maintainedBy: aws
languages:
typescript:
package: bedrock-agentcore
@@ -1,6 +1,7 @@
name: agentcore-memory
description: Amazon Bedrock AgentCore Memory session manager providing short-term conversation persistence and long-term memory strategies for Strands Agents.
integrationType: session-manager
maintainedBy: aws
languages:
python:
package: bedrock-agentcore[strands-agents]
@@ -1,6 +1,7 @@
name: agentcore-payments
description: Amazon Bedrock AgentCore plugin that intercepts HTTP 402 responses and processes x402 micropayments to access paid APIs and MCP servers.
integrationType: plugin
maintainedBy: aws
languages:
python:
package: bedrock-agentcore[strands-agents]
@@ -1,6 +1,7 @@
name: agentcore-rl-toolkit
description: AWS SDK for reinforcement learning training loops on Amazon Bedrock AgentCore, with built-in Strands Agents support via a decorator swap.
integrationType: integration
maintainedBy: aws
languages:
python:
package: agentcore-rl-toolkit
@@ -1,6 +1,7 @@
name: agentcore-tool-search
description: Amazon Bedrock AgentCore Gateway plugin that loads only the tools relevant to each invocation via semantic search over hundreds of registered tools.
integrationType: plugin
maintainedBy: aws
languages:
python:
package: bedrock-agentcore[strands-agents]
@@ -0,0 +1,10 @@
name: Amazon Bedrock
description: Run Strands agents on Amazon Bedrock models, with guardrails, prompt caching, and multi-modal input.
integrationType: model-provider
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/bedrock.py
docsPage: docs/user-guide/concepts/model-providers/amazon-bedrock
addedDate: 2025-12-01
@@ -0,0 +1,9 @@
name: Amazon SageMaker
description: Run Strands agents on models deployed to Amazon SageMaker inference endpoints, including JumpStart and fine-tuned models.
integrationType: model-provider
maintainedBy: strands
languages:
python: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/sagemaker.py
docsPage: docs/user-guide/concepts/model-providers/sagemaker
addedDate: 2025-12-01
+10
View File
@@ -0,0 +1,10 @@
name: Anthropic
description: Run Strands agents on Claude models through the Anthropic API, with structured output and prompt caching.
integrationType: model-provider
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/anthropic.py
docsPage: docs/user-guide/concepts/model-providers/anthropic
addedDate: 2025-12-01
+10
View File
@@ -0,0 +1,10 @@
name: Bash
description: Execute shell commands, with persistent bash sessions in TypeScript and sandboxed stateless shells in Python.
integrationType: tool
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_tools/shell
docsPage: docs/user-guide/concepts/tools/vended-tools
addedDate: 2025-12-01
@@ -0,0 +1,10 @@
name: Bedrock Knowledge Base
description: 'Back agent memory with Amazon Bedrock Knowledge Bases: semantic search over a managed vector store with document ingestion.'
integrationType: memory-store
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_memory_stores/bedrock_knowledge_base
docsPage: docs/user-guide/concepts/memory/bedrock-knowledge-base
addedDate: 2025-12-01
@@ -0,0 +1,10 @@
name: Cedar Authorization
description: 'Control which tools an agent can invoke with Cedar policies: identity-aware access control, role-based permissions, and rate limits.'
integrationType: intervention
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_interventions/cedar
docsPage: docs/user-guide/concepts/agents/interventions/cedar-authorization
addedDate: 2025-12-01
@@ -1,6 +1,7 @@
name: coinbase-agentkit-strands-agents
description: Coinbase AgentKit extension for Strands Agents that provides onchain action tools for interacting with blockchain networks and wallets.
integrationType: tool
maintainedBy: partner
languages:
python:
package: coinbase-agentkit-strands-agents
@@ -0,0 +1,10 @@
name: Context Injector
description: 'Fold real-time text into the model input before each call: a clock, environment facts, or a lookup, without storing it in history.'
integrationType: plugin
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_plugins/context_injector
docsPage: docs/user-guide/concepts/plugins/context-injector
addedDate: 2025-12-01
@@ -0,0 +1,10 @@
name: Context Offloader
description: Keep large tool results from consuming the context window by storing them externally and leaving retrievable references.
integrationType: plugin
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_plugins/context_offloader
docsPage: docs/user-guide/concepts/plugins/context-offloader
addedDate: 2025-12-01
@@ -1,6 +1,7 @@
name: datadog-ai-guard
description: Datadog AI Guard plugin that inspects and blocks prompt injection, jailbreaking, data exfiltration, and destructive tool calls in real time.
integrationType: plugin
maintainedBy: partner
languages:
python:
package: ddtrace
+10
View File
@@ -0,0 +1,10 @@
name: File Editor
description: View, create, and edit files in the workspace from agent tool calls.
integrationType: tool
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_tools/file_editor
docsPage: docs/user-guide/concepts/tools/vended-tools
addedDate: 2025-12-01
@@ -0,0 +1,10 @@
name: File Session Manager
description: Persist agent conversations, tool state, and context to the local filesystem across sessions.
integrationType: session-manager
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/session/file_session_manager.py
docsPage: docs/user-guide/concepts/agents/session-management
addedDate: 2025-12-01
+10
View File
@@ -0,0 +1,10 @@
name: GoalLoop
description: Validate agent responses against a goal and loop with feedback until it is satisfied.
integrationType: plugin
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_plugins/goal
docsPage: docs/user-guide/concepts/plugins/goal-loop
addedDate: 2025-12-01
@@ -0,0 +1,10 @@
name: Google Gemini
description: Run Strands agents on Google Gemini models, with built-in tools, structured output, and image and video input.
integrationType: model-provider
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/gemini.py
docsPage: docs/user-guide/concepts/model-providers/google
addedDate: 2025-12-01
+1
View File
@@ -1,6 +1,7 @@
name: headroom-ai
description: Framework-agnostic context compression library that cuts LLM token usage 60–95% on JSON and 15–20% on code, with Strands support built in.
integrationType: plugin
maintainedBy: partner
languages:
python:
package: headroom-ai[strands]
@@ -1,6 +1,7 @@
name: hiddenlayer-strands
description: HiddenLayer AI security integration for Strands Agents that detects and blocks threats such as prompt injection and model abuse in real time.
integrationType: intervention
maintainedBy: partner
languages:
python:
package: hiddenlayer-strands
@@ -1,6 +1,7 @@
name: hindsight-strands
description: Persistent long-term memory and semantic search tools for Strands agents, powered by Vectorize's Hindsight memory service.
integrationType: memory-store
maintainedBy: partner
languages:
python:
package: hindsight-strands
@@ -0,0 +1,10 @@
name: HTTP Request
description: Make HTTP requests to external APIs, with configurable clients for authentication, timeouts, and proxies.
integrationType: tool
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_tools/http_request
docsPage: docs/user-guide/concepts/tools/vended-tools
addedDate: 2025-12-01
@@ -0,0 +1,10 @@
name: Human in the Loop
description: Gate agent tool execution behind human approval, with modes for CLI, web, and custom UIs.
integrationType: intervention
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_interventions/hitl
docsPage: docs/user-guide/concepts/agents/interventions/human-in-the-loop
addedDate: 2025-12-01
+1
View File
@@ -1,6 +1,7 @@
name: langfuse
description: Langfuse LLM observability for Strands agents — traces, metrics, and evaluations via the SDK's built-in OpenTelemetry export (strands-agents[otel]).
integrationType: plugin
maintainedBy: partner
languages:
python: {}
github: https://github.com/langfuse/langfuse
+9
View File
@@ -0,0 +1,9 @@
name: LiteLLM
description: Run Strands agents on models from OpenAI, Anthropic, Amazon, and more through LiteLLM's unified API.
integrationType: model-provider
maintainedBy: strands
languages:
python: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/litellm.py
docsPage: docs/user-guide/concepts/model-providers/litellm
addedDate: 2025-12-01
+9
View File
@@ -0,0 +1,9 @@
name: Llama API
description: Run Strands agents on Meta-hosted Llama models without managing your own inference infrastructure.
integrationType: model-provider
maintainedBy: strands
languages:
python: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/llamaapi.py
docsPage: docs/user-guide/concepts/model-providers/llamaapi
addedDate: 2025-12-01
+9
View File
@@ -0,0 +1,9 @@
name: llama.cpp
description: Run Strands agents on local quantized models served by llama.cpp, with grammar constraints and advanced sampling.
integrationType: model-provider
maintainedBy: strands
languages:
python: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/llamacpp.py
docsPage: docs/user-guide/concepts/model-providers/llamacpp
addedDate: 2025-12-01
@@ -1,6 +1,7 @@
name: llm-tracekit-strands
description: OpenTelemetry instrumentation for Strands Agents that captures spans, traces, and metrics compatible with Coralogix and other OTel backends.
integrationType: plugin
maintainedBy: partner
languages:
python:
package: llm-tracekit-strands
+9
View File
@@ -0,0 +1,9 @@
name: Mistral AI
description: Run Strands agents on Mistral AI models, with tool calling and streaming.
integrationType: model-provider
maintainedBy: strands
languages:
python: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/mistral.py
docsPage: docs/user-guide/concepts/model-providers/mistral
addedDate: 2025-12-01
+9
View File
@@ -0,0 +1,9 @@
name: Notebook
description: Manage persistent text notebooks that carry plans, notes, and scratch space across agent turns.
integrationType: tool
maintainedBy: strands
languages:
typescript: {}
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-ts/src/vended-tools/notebook
docsPage: docs/user-guide/concepts/tools/vended-tools
addedDate: 2025-12-01
@@ -1,6 +1,7 @@
name: nvidia-nat-strands
description: NVIDIA NeMo Agent Toolkit integration adding enterprise-grade instrumentation, observability, and optimization to Strands Agents.
integrationType: integration
maintainedBy: partner
languages:
python:
package: nvidia-nat-strands
+9
View File
@@ -0,0 +1,9 @@
name: Ollama
description: 'Run Strands agents on local models with Ollama: text generation, image understanding, tool calling, and streaming.'
integrationType: model-provider
maintainedBy: strands
languages:
python: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/ollama.py
docsPage: docs/user-guide/concepts/model-providers/ollama
addedDate: 2025-12-01
@@ -0,0 +1,10 @@
name: OpenAI Responses
description: Run Strands agents on the OpenAI Responses API, with built-in tools like web search and server-side conversation state.
integrationType: model-provider
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/openai_responses.py
docsPage: docs/user-guide/concepts/model-providers/openai-responses
addedDate: 2025-12-01
+10
View File
@@ -0,0 +1,10 @@
name: OpenAI
description: Run Strands agents on OpenAI models or any OpenAI-compatible API, with vision and structured output.
integrationType: model-provider
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/openai.py
docsPage: docs/user-guide/concepts/model-providers/openai
addedDate: 2025-12-01
@@ -1,6 +1,7 @@
name: openinference-instrumentation-strands-agents
description: Arize AI OpenInference span processor for Strands Agents — transforms native OTel spans to OpenInference format for Arize AX and Phoenix observability.
integrationType: plugin
maintainedBy: partner
languages:
python:
package: openinference-instrumentation-strands-agents
+1
View File
@@ -1,6 +1,7 @@
name: portkey
description: Routes Strands model calls through Portkey's AI gateway via the OpenAI-compatible endpoint, adding guardrails, fallbacks, and observability.
integrationType: model-provider
maintainedBy: partner
languages:
python: {}
github: https://github.com/Portkey-AI/gateway
+1
View File
@@ -1,6 +1,7 @@
name: "respan"
description: "OpenTelemetry instrumentation plugin that normalizes Strands Agents TS spans into the Respan observability platform."
integrationType: plugin
maintainedBy: partner
languages:
typescript:
package: "@respan/instrumentation-strands-agents"
@@ -0,0 +1,10 @@
name: S3 Session Manager
description: Persist agent conversations, tool state, and context to Amazon S3 across sessions.
integrationType: session-manager
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/session/s3_session_manager.py
docsPage: docs/user-guide/concepts/agents/session-management
addedDate: 2025-12-01
@@ -1,6 +1,7 @@
name: s3-vectors-memory
description: Long-term semantic memory for Strands Agents via Amazon S3 Vectors, storing conversation summaries as vectors and recalling them on later runs.
integrationType: plugin
maintainedBy: aws
languages:
python:
package: strands-s3-vectors-memory
@@ -1,6 +1,7 @@
name: sigil-sdk-strands
description: Grafana Cloud AI Observability (Sigil) hooks for Strands Agents — sends agent, model, and tool lifecycle events to Grafana Sigil via SigilStrandsHookProvider.
integrationType: plugin
maintainedBy: partner
languages:
python:
package: sigil-sdk-strands
+10
View File
@@ -0,0 +1,10 @@
name: Sleep
description: Pause agent execution for a bounded, cancellable duration.
integrationType: tool
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_tools/sleep
docsPage: docs/user-guide/concepts/tools/vended-tools
addedDate: 2025-12-01
+10
View File
@@ -0,0 +1,10 @@
name: Steering
description: Guide agents with just-in-time contextual feedback at key moments instead of front-loaded monolithic prompts.
integrationType: intervention
maintainedBy: strands
languages:
python: {}
typescript: {}
github: https://github.com/strands-agents/harness-sdk/tree/main/strands-py/src/strands/vended_plugins/steering
docsPage: docs/user-guide/concepts/agents/interventions/steering
addedDate: 2025-12-01
@@ -1,6 +1,7 @@
name: strands-apify
description: Apify platform integration giving Strands Agents access to thousands of pre-built Actors for web scraping, search, and e-commerce crawling.
integrationType: tool
maintainedBy: partner
languages:
python:
package: strands-apify
@@ -1,6 +1,7 @@
name: strands-code-agent
description: A coding agent that replaces tool-calling with code generation in a persistent Python REPL
integrationType: agent-extension
maintainedBy: aws
languages:
python:
package: strands-code-agent
@@ -1,6 +1,7 @@
name: strands-dakera
description: Persistent, decay-weighted MemoryStore backed by a self-hosted Dakera server, plus a full-CRUD memory tool sharing the same backend.
integrationType: memory-store
maintainedBy: partner
languages:
python:
package: strands-dakera
@@ -1,6 +1,7 @@
name: strands-token-telemetry
description: Emits Strands agent token usage as CloudWatch EMF metrics for cost visibility and capacity planning.
integrationType: plugin
maintainedBy: partner
languages:
python:
package: strands-token-telemetry
+1
View File
@@ -1,6 +1,7 @@
name: temporal
description: Durable execution for Strands agents via Temporal workflows, so every model call and tool invocation survives crashes and restarts.
integrationType: integration
maintainedBy: partner
languages:
python:
package: temporalio[strands-agents]
+1
View File
@@ -1,6 +1,7 @@
name: "traceai"
description: "OpenTelemetry instrumentation for the Strands Agents TypeScript SDK, part of the Future AGI traceAI observability framework."
integrationType: plugin
maintainedBy: partner
languages:
typescript:
package: "@traceai/strands"
+1
View File
@@ -1,6 +1,7 @@
name: utcp
description: Universal Tool Calling Protocol adapter enabling tool discovery and invocation across HTTP, WebSocket, and other transports without wrapper servers.
integrationType: tool
maintainedBy: partner
languages:
python:
package: strands-utcp
@@ -1,6 +1,7 @@
name: valyu-agentcore
description: Valyu search tools for AWS Bedrock AgentCore providing web, financial, academic, biomedical, patent, and regulatory data search.
integrationType: tool
maintainedBy: partner
languages:
python:
package: valyu-agentcore
+9
View File
@@ -0,0 +1,9 @@
name: Vercel AI SDK
description: Run Strands agents on any Vercel AI SDK provider through the VercelModel adapter, using @ai-sdk packages.
integrationType: model-provider
maintainedBy: strands
languages:
typescript: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-ts/src/models/vercel.ts
docsPage: docs/user-guide/concepts/model-providers/vercel
addedDate: 2025-12-01
+9
View File
@@ -0,0 +1,9 @@
name: Writer
description: Run Strands agents on Writer Palmyra models, with tool calling, structured output, and enterprise tasks.
integrationType: model-provider
maintainedBy: strands
languages:
python: {}
github: https://github.com/strands-agents/harness-sdk/blob/main/strands-py/src/strands/models/writer.py
docsPage: docs/user-guide/concepts/model-providers/writer
addedDate: 2025-12-01
@@ -59,6 +59,7 @@ addedDate: 2026-07-31 # the date you open the PR
| `addedDate` | ✅ | Date of submission (drives the "New" badge) |
| `docsUrl` | Recommended | Link to your own docs page about the Strands integration — your card's primary link. Must start with `https://`. |
| `docsPage` | Legacy | On-site docs page reference — existing entries only; new submissions link their own docs via `docsUrl` |
| `maintainedBy` | Optional | Who stands behind the integration, shown and filtered on the catalog. Defaults to `community` (maintained by an individual developer). Set `partner` only if you represent the company behind the integration and the entry's `github` org is that company's official org — a maintainer verifies this before merging. `strands` and `aws` are granted by the Strands team. |
The maintainer shown on your card and the PyPI/npm links are derived automatically — the
maintainer from your `github` URL's owner, the registry links from your `package` names.
+113 -30
View File
@@ -1,16 +1,17 @@
---
/**
* Community integration catalog: searchable, filterable grid of community
* packages. Entries come from the `catalog` content collection; popularity
* stats from the bot-maintained src/data/catalog-stats.json. Everything is
* rendered statically; the inline script only toggles card visibility.
* Integration catalog: searchable, filterable grid of SDK built-ins and
* community packages. Entries come from the `catalog` content collection;
* popularity stats from the bot-maintained src/data/catalog-stats.json.
* Everything is rendered statically; the inline script only toggles card
* visibility.
*/
import { getCollection } from 'astro:content'
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'
import CatalogCard from '../components/catalog/CatalogCard.astro'
import CatalogFilterBar from '../components/catalog/CatalogFilterBar.astro'
import CatalogFacets from '../components/catalog/CatalogFacets.astro'
import { CATALOG_TYPES, TYPE_ICONS } from '../components/catalog/types'
import { CATALOG_TYPES, AWS_MARK_VIEWBOX, MAINTAINED_BY_ICONS, MAINTAINED_BY_TIERS, TYPE_ICONS } from '../components/catalog/types'
import { toCardModel, sortEntries, type CatalogStatsFile } from '../util/catalog'
import { pathWithBase } from '../util/links'
import statsFile from '../data/catalog-stats.json'
@@ -28,16 +29,22 @@ const hasVerified = cards.some((c) => c.badges.includes('verified'))
const hasFeatured = cards.some((c) => c.featured)
const hasNew = cards.some((c) => c.badges.includes('new'))
// Counts span both SDKs while filtering is per-SDK; with no evals entries the
// two coincide, so if/when evals entries land these need client-side recompute.
// Build-time counts are the initial paint only; apply() recomputes every
// chip count client-side against the active filters.
const typeFacets = CATALOG_TYPES.map(({ value, labelPlural }) => ({
value,
label: labelPlural,
count: cards.filter((c) => c.integrationType === value).length,
})).filter((t) => t.count > 0)
const maintainedByFacets = MAINTAINED_BY_TIERS.map(({ value, label }) => ({
value,
label,
count: cards.filter((c) => c.maintainedBy === value).length,
})).filter((t) => t.count > 0)
const title = 'Integrations'
const description = 'Community-built tools, model providers, plugins, and integrations for Strands Agents.'
const description = 'Built-in and community tools, model providers, plugins, and integrations for Strands Agents.'
---
<StarlightPage frontmatter={{ title, description, template: 'splash', pagefind: false, hero: { actions: [] } }} hasSidebar={false}>
@@ -45,7 +52,7 @@ const description = 'Community-built tools, model providers, plugins, and integr
<div class="cat-head">
<div>
<h1 class="cat-title">Integrations</h1>
<p class="cat-subtitle">Tools, model providers, and integrations built by the Strands community</p>
<p class="cat-subtitle">Tools, model providers, and integrations, built into the SDK and by the Strands community</p>
</div>
<div class="cat-head-actions">
<a class="cat-cta-btn" href={pathWithBase('/docs/integrations/get-featured/')}>Add your integration</a>
@@ -56,22 +63,6 @@ const description = 'Community-built tools, model providers, plugins, and integr
<CatalogFilterBar showSdkFacet={showSdkFacet} />
<div class="cat-body">
<nav class="cat-rail" aria-label="Filter catalog">
<p class="cat-rail-label" id="cat-type-label">Type</p>
<ul class="cat-rail-list" aria-labelledby="cat-type-label">
{
typeFacets.map((t) => (
<li class="cat-rail-item">
<button class="cat-chip" type="button" data-cat-type={t.value} aria-pressed="false">
<svg width="13" height="13" viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={TYPE_ICONS[t.value]} />
</svg>
{t.label}
<span class="cat-count">{t.count}</span>
</button>
</li>
))
}
</ul>
{
(hasFeatured || hasNew || hasVerified) && (
<>
@@ -105,11 +96,54 @@ const description = 'Community-built tools, model providers, plugins, and integr
</>
)
}
<p class="cat-rail-label" id="cat-type-label">Type</p>
<ul class="cat-rail-list" aria-labelledby="cat-type-label">
{
typeFacets.map((t) => (
<li class="cat-rail-item">
<button class="cat-chip" type="button" data-cat-type={t.value} aria-pressed="false">
<svg width="13" height="13" viewBox="0 0 24 24" aria-hidden="true">
<path fill="currentColor" d={TYPE_ICONS[t.value]} />
</svg>
{t.label}
<span class="cat-count">{t.count}</span>
</button>
</li>
))
}
</ul>
<p class="cat-rail-label" id="cat-maintained-label">Maintained by</p>
<ul class="cat-rail-list" aria-labelledby="cat-maintained-label">
{
maintainedByFacets.map((t) => (
<li class="cat-rail-item">
<button class="cat-chip" type="button" data-cat-maintained={t.value} aria-label={t.label} aria-pressed="false">
{MAINTAINED_BY_ICONS[t.value] ? (
<svg width="29" height="18" viewBox={AWS_MARK_VIEWBOX} aria-hidden="true">
<path fill="currentColor" d={MAINTAINED_BY_ICONS[t.value]} />
</svg>
) : (
t.label
)}
<span class="cat-count">{t.count}</span>
</button>
</li>
))
}
</ul>
</nav>
<div class="cat-grid" data-cat-grid>
{cards.map((card) => <CatalogCard card={card} />)}
{/* Not a .cat-card: the filter script must never hide this tile. */}
<div class="cat-cta">
<div class="cat-empty" data-cat-empty hidden>
<p class="cat-empty-title">No integrations match</p>
<p class="cat-empty-body">Try different filters, or build the missing piece and list it here.</p>
<div class="cat-empty-actions">
<button class="cat-btn-ghost" type="button" data-cat-clear>Clear filters</button>
<a class="cat-cta-btn" href={pathWithBase('/docs/integrations/get-featured/')}>Add your integration</a>
</div>
</div>
{/* Not a .cat-card: hidden only when the empty panel takes over. */}
<div class="cat-cta" data-cat-cta>
<h2 class="cat-cta-title">Add your integration</h2>
<p class="cat-cta-body">Built a tool, model provider, or plugin for Strands? List it here.</p>
<a class="cat-cta-btn" href={pathWithBase('/docs/integrations/get-featured/')}>Add your integration</a>
@@ -137,6 +171,7 @@ const description = 'Community-built tools, model providers, plugins, and integr
<div class="cat-dialog-facets">
<CatalogFacets
types={typeFacets}
maintainedBy={maintainedByFacets}
showSdkFacet={showSdkFacet}
hasVerified={hasVerified}
hasFeatured={hasFeatured}
@@ -150,6 +185,7 @@ const description = 'Community-built tools, model providers, plugins, and integr
data-type={card.integrationType}
data-langs={card.languages.join(' ')}
data-badges={[...card.badges, ...(card.featured ? ['featured'] : [])].join(' ')}
data-maintained={card.maintainedBy}
data-sdk={card.sdk}
data-search={`${card.name} ${card.description} by ${card.maintainer}`.toLowerCase()}
>
@@ -179,7 +215,6 @@ const description = 'Community-built tools, model providers, plugins, and integr
<iframe class="cat-drawer-frame" data-cat-drawer-frame title="Integration documentation" src="about:blank"
></iframe>
</dialog>
<p class="cat-empty" data-cat-empty hidden>No integrations match the current filters.</p>
<p class="cat-disclaimer">Community packages are maintained by their authors — review before production use.</p>
</div>
@@ -189,15 +224,24 @@ const description = 'Community-built tools, model providers, plugins, and integr
// Client-side faceted filtering over the statically-rendered cards. The
// matching + URL-state logic lives in util/catalog-filter (unit-tested);
// this script only reads the DOM and toggles [hidden].
import { matchesFilters, stateToQuery, queryToState, type CardFilterData } from '../util/catalog-filter'
import {
countIfSelected,
matchesFilters,
selectOnly,
stateToQuery,
queryToState,
type CardFilterData,
} from '../util/catalog-filter'
const cards = Array.from(document.querySelectorAll<HTMLElement>('[data-cat-grid] .cat-card'))
const searchInput = document.querySelector<HTMLInputElement>('[data-cat-search]')
const typeBtns = Array.from(document.querySelectorAll<HTMLElement>('[data-cat-type]'))
const langBtns = Array.from(document.querySelectorAll<HTMLElement>('[data-cat-lang]'))
const badgeBtns = Array.from(document.querySelectorAll<HTMLElement>('[data-cat-badge]'))
const maintainedBtns = Array.from(document.querySelectorAll<HTMLElement>('[data-cat-maintained]'))
const sdkBtns = Array.from(document.querySelectorAll<HTMLElement>('[data-cat-sdk]'))
const emptyEl = document.querySelector<HTMLElement>('[data-cat-empty]')
const ctaEl = document.querySelector<HTMLElement>('[data-cat-cta]')
const countEl = document.querySelector<HTMLElement>('[data-cat-count]')
const dialog = document.querySelector<HTMLDialogElement>('[data-cat-dialog]')
const dialogItems = Array.from(document.querySelectorAll<HTMLElement>('[data-cat-dialog-list] .cat-dialog-item'))
@@ -214,6 +258,8 @@ const description = 'Community-built tools, model providers, plugins, and integr
for (const badge of state.badges) if (!renderedBadges.has(badge)) state.badges.delete(badge)
const renderedTypes = new Set(typeBtns.map((btn) => btn.dataset.catType))
for (const t of state.types) if (!renderedTypes.has(t)) state.types.delete(t)
const renderedMaintained = new Set(maintainedBtns.map((btn) => btn.dataset.catMaintained))
for (const t of state.maintainedBy) if (!renderedMaintained.has(t)) state.maintainedBy.delete(t)
function cardData(el: HTMLElement): CardFilterData {
return {
@@ -221,10 +267,22 @@ const description = 'Community-built tools, model providers, plugins, and integr
type: el.dataset.type || '',
languages: (el.dataset.langs || '').split(' ').filter(Boolean),
badges: (el.dataset.badges || '').split(' ').filter(Boolean),
maintainedBy: el.dataset.maintained || 'community',
sdk: el.dataset.sdk || 'agents',
}
}
// Facet data never changes after render; cache it for the count updates.
const facetData = cards.map(cardData)
function updateChipCount(btn: HTMLElement, facet: 'types' | 'badges' | 'maintainedBy', value: string) {
const el = btn.querySelector('.cat-count')
if (!el) return
const count = countIfSelected(facetData, state, facet, value)
el.textContent = String(count)
btn.classList.toggle('cat-chip--zero', count === 0)
}
function apply() {
let visible = 0
for (const el of cards) {
@@ -233,6 +291,7 @@ const description = 'Community-built tools, model providers, plugins, and integr
if (show) visible++
}
if (emptyEl) emptyEl.hidden = visible > 0
if (ctaEl) ctaEl.hidden = visible === 0
if (countEl) countEl.textContent = String(visible)
if (searchLabel) searchLabel.textContent = state.search.trim() || 'Search integrations…'
if (searchOpenBtn) searchOpenBtn.classList.toggle('cat-search-trigger--active', Boolean(state.search.trim()))
@@ -250,6 +309,12 @@ const description = 'Community-built tools, model providers, plugins, and integr
for (const btn of typeBtns) btn.setAttribute('aria-pressed', String(state.types.has(btn.dataset.catType || '')))
for (const btn of langBtns) btn.setAttribute('aria-pressed', String(state.languages.has(btn.dataset.catLang || '')))
for (const btn of badgeBtns) btn.setAttribute('aria-pressed', String(state.badges.has(btn.dataset.catBadge || '')))
for (const btn of maintainedBtns)
btn.setAttribute('aria-pressed', String(state.maintainedBy.has(btn.dataset.catMaintained || '')))
for (const btn of typeBtns) updateChipCount(btn, 'types', btn.dataset.catType || '')
for (const btn of badgeBtns) updateChipCount(btn, 'badges', btn.dataset.catBadge || '')
for (const btn of maintainedBtns) updateChipCount(btn, 'maintainedBy', btn.dataset.catMaintained || '')
for (const btn of sdkBtns) btn.setAttribute('aria-checked', String(state.sdk === btn.dataset.catSdk))
// `entry` (the open docs drawer) is not filter state; carry the current
@@ -302,7 +367,7 @@ const description = 'Community-built tools, model providers, plugins, and integr
})
for (const btn of typeBtns)
btn.addEventListener('click', () => {
toggle(state.types, btn.dataset.catType || '')
selectOnly(state.types, btn.dataset.catType || '')
apply()
})
for (const btn of langBtns)
@@ -310,11 +375,29 @@ const description = 'Community-built tools, model providers, plugins, and integr
toggle(state.languages, btn.dataset.catLang || '')
apply()
})
// Highlights are cross-cutting flags (an entry can be Featured and New), so
// they multi-select; Type and Maintained-by hold one value per entry, so
// selecting within them replaces the previous choice.
for (const btn of badgeBtns)
btn.addEventListener('click', () => {
toggle(state.badges, btn.dataset.catBadge || '')
apply()
})
for (const btn of maintainedBtns)
btn.addEventListener('click', () => {
selectOnly(state.maintainedBy, btn.dataset.catMaintained || '')
apply()
})
document.querySelector('[data-cat-clear]')?.addEventListener('click', () => {
state.search = ''
if (searchInput) searchInput.value = ''
state.types.clear()
state.languages.clear()
state.badges.clear()
state.maintainedBy.clear()
state.sdk = 'agents'
apply()
})
for (const btn of sdkBtns)
btn.addEventListener('click', () => {
state.sdk = btn.dataset.catSdk || 'agents'
+153 -22
View File
@@ -108,9 +108,9 @@
margin: 0 0 0.25rem;
padding-left: 0.65rem;
}
/* Space between rail sections (Type, Highlights). */
/* Space between rail sections (Highlights, Type, Maintained by). */
.cat-rail-list + .cat-rail-label {
margin-top: 1.1rem;
margin-top: 0.7rem;
}
.cat-rail-list {
list-style: none;
@@ -118,7 +118,7 @@
padding: 0;
display: flex;
flex-direction: column;
gap: 0;
gap: 0.25rem;
}
.cat-rail-item {
margin: 0;
@@ -127,11 +127,18 @@
.cat-rail .cat-chip {
width: 100%;
text-align: left;
padding-top: 0.22rem;
padding-bottom: 0.22rem;
/* Starlight's content styles give buttons a bottom margin the :where reset
(margin-top only) doesn't touch; without this the rail rows drift apart. */
margin: 0;
padding-top: 0.14rem;
padding-bottom: 0.14rem;
}
.cat-rail .cat-count {
margin-left: auto;
margin-left: 0.1rem;
}
/* A chip whose selection would match nothing stays clickable but reads muted. */
.cat-chip--zero {
opacity: 0.4;
}
.cat-grid {
@@ -151,13 +158,16 @@
position: relative;
display: flex;
gap: 0.85rem;
align-items: flex-start;
/* Stretch so the content column fills the uniform card height and the
footer's margin-top auto can push it to the bottom edge. */
align-items: stretch;
/* Grid items default to min-width auto; without this a card's nowrap
footer would widen its track past the container on narrow screens. */
min-width: 0;
border: 1px solid var(--sl-color-hairline, var(--sl-color-gray-5));
border-radius: var(--cat-radius-card);
padding: 1.4rem;
/* Lighter bottom padding so the footer line hugs the card's lower edge. */
padding: 1.4rem 1.4rem 0.95rem;
background: var(--sl-color-bg-nav);
transition: border-color 0.15s ease, background 0.15s ease;
}
@@ -169,12 +179,77 @@
display: none;
}
/* The CTA tile sets its own display, which outranks the [hidden] UA style
(same trap as .cat-card[hidden]). */
.cat-cta[hidden] {
display: none;
}
/* Zero-results panel: spans the grid, offers a reset and the submission CTA. */
.cat-empty {
grid-column: 1 / -1;
text-align: center;
border: 1px dashed color-mix(in srgb, var(--sl-color-gray-4) 60%, transparent);
border-radius: var(--cat-radius-card);
padding: 3rem 1.5rem;
}
.cat-empty[hidden] {
display: none;
}
.cat-empty-title {
font-weight: 600;
font-size: 1.15rem;
color: var(--sl-color-white);
margin-bottom: 0.35rem;
}
.cat-empty-body {
color: var(--sl-color-gray-3);
margin-bottom: 1.2rem;
}
.cat-empty-actions {
display: flex;
gap: 0.6rem;
justify-content: center;
flex-wrap: wrap;
}
.cat-btn-ghost {
font-size: 0.92rem;
padding: 0.45rem 1rem;
border: 1px solid var(--sl-color-gray-4);
border-radius: 999px;
background: transparent;
color: var(--sl-color-gray-2);
cursor: pointer;
}
.cat-btn-ghost:hover {
border-color: var(--sl-color-gray-3);
color: var(--sl-color-white);
}
/* Featured entries live in the same grid (sorted first) and are marked by a
Featured chip in the card head plus a light accent border tint. */
.cat-card--featured {
border-color: color-mix(in srgb, var(--sl-color-accent) 25%, var(--sl-color-gray-5));
}
/* Built-in (SDK) cards carry the badge's note-blue on the border and type
tile so first-party entries scan apart from the rest of the catalog. */
.cat-card--builtin {
border-color: color-mix(in srgb, var(--sl-badge-note-border) 45%, var(--sl-color-gray-5));
}
.cat-card--builtin:hover {
border-color: color-mix(in srgb, var(--sl-badge-note-border) 75%, var(--sl-color-gray-5));
background: color-mix(in srgb, var(--sl-badge-note-bg) 12%, var(--sl-color-bg-nav));
}
.cat-card--builtin .cat-tile {
color: var(--sl-badge-note-text);
background: color-mix(in srgb, var(--sl-badge-note-bg) 30%, transparent);
border-color: color-mix(in srgb, var(--sl-badge-note-border) 35%, transparent);
}
.cat-card--builtin:hover .cat-card-title {
color: var(--sl-badge-note-text);
}
/* Whole-card click: the title link stretches over the card; secondary links
re-raise themselves above it. */
.cat-card-title::after {
@@ -236,17 +311,24 @@
.cat-card-stats {
display: inline-flex;
gap: 0.55rem;
margin-left: auto;
flex-shrink: 0;
font-size: var(--cat-text-meta);
color: var(--sl-color-gray-3);
}
.cat-card-flags {
display: inline-flex;
align-items: center;
gap: 0.45rem;
margin-left: auto;
flex-shrink: 0;
}
.cat-card:hover .cat-card-title {
color: var(--cat-accent-text);
}
.cat-external {
font-size: 0.7rem;
color: var(--sl-color-gray-3);
margin-left: 0.25rem;
}
/* Card flags mirror Starlight's sidebar .sl-badge treatment (see Badge.astro):
@@ -264,28 +346,77 @@
border: 1px solid;
border-radius: 0.25rem;
}
/* Variant colors follow the docs sidebar badges but lightened: outline-only
(no fill) so the flags read as quiet metadata rather than solid chips. */
.cat-flag--new {
color: var(--sl-badge-tip-text);
background: color-mix(in srgb, var(--sl-badge-tip-bg) 35%, transparent);
border-color: color-mix(in srgb, var(--sl-badge-tip-border) 55%, transparent);
}
/* Featured/Verified are maintainer-granted: brand-green outline keeps them
distinct from the informational New without a solid fill. */
.cat-flag--verified,
.cat-flag--featured {
/* Verified is maintainer-granted: brand-green outline, no solid fill. */
.cat-flag--verified {
color: var(--cat-accent-text);
background: color-mix(in srgb, var(--sl-color-accent) 7%, transparent);
border-color: color-mix(in srgb, var(--sl-color-accent) 40%, transparent);
}
/* Featured/New render as tabs seated on the card's top-right border rather
than chips in the title row, keeping the head to name + tier. The fill
matches the featured border tint so tab and border read as one shape. */
.cat-corner {
position: absolute;
top: -1px;
right: 14px;
display: flex;
gap: 4px;
z-index: 1;
/* Sits above the stretched title link; without this the tabs would be a
click-dead zone on the card. */
pointer-events: none;
}
.cat-tab {
font-family: var(--sl-font-system-mono);
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
line-height: 1;
padding: 0.22rem 0.5rem;
border-radius: 0 0 5px 5px;
color: var(--sl-color-black);
}
.cat-tab--featured {
background: var(--sl-color-accent);
}
.cat-tab--new {
background: var(--sl-badge-tip-border);
}
/* Maintainer-tier flags. Built-in (Strands) is note-blue so it reads apart
from the maintainer-granted green (Verified/Featured) and the informational
New; AWS is caution-orange; Partner stays a neutral outline. Community
renders no flag: it is the default tier. */
.cat-flag--strands {
color: var(--sl-badge-note-text);
background: color-mix(in srgb, var(--sl-badge-note-bg) 35%, transparent);
border-color: color-mix(in srgb, var(--sl-badge-note-border) 55%, transparent);
}
/* Inline svg baseline-aligns and rides high; block lets the flag's flex
centering align marks with the neighbor chips' text. */
.cat-flag svg {
display: block;
}
/* The AWS flag is the bare mark, no chip box; --sl-color-white flips with
the theme so the logo stays full-contrast in light and dark. */
.cat-flag--aws {
border: none;
background: transparent;
padding: 0;
color: var(--sl-color-white);
}
.cat-flag--partner {
color: var(--sl-color-gray-2);
background: color-mix(in srgb, var(--sl-color-gray-4) 20%, transparent);
border-color: color-mix(in srgb, var(--sl-color-gray-4) 70%, transparent);
}
.cat-card-desc {
color: var(--sl-color-gray-2);
font-size: 0.92rem;
line-height: 1.6;
margin: 0;
flex-grow: 1;
/* Uniform card heights: every description occupies exactly two lines —
longer text clamps, shorter text still reserves the space. */
display: -webkit-box;
@@ -298,9 +429,9 @@
.cat-card-foot {
display: flex;
align-items: center;
margin-top: auto;
justify-content: space-between;
gap: 0.7rem;
margin-top: 0.15rem;
font-size: var(--cat-text-meta);
color: var(--sl-color-gray-3);
min-width: 0;
+35
View File
@@ -10,6 +10,7 @@ export interface CatalogFilterState {
types: Set<string>
languages: Set<string>
badges: Set<string>
maintainedBy: Set<string>
sdk: string
}
@@ -18,12 +19,14 @@ export interface CardFilterData {
type: string
languages: string[]
badges: string[]
maintainedBy: string
sdk: string
}
const KNOWN_TYPES = new Set<string>(CATALOG_TYPES.map((t) => t.value))
const KNOWN_LANGUAGES = new Set(['python', 'typescript'])
const KNOWN_BADGES = new Set(['verified', 'featured', 'new'])
export const KNOWN_MAINTAINED_BY = new Set(['strands', 'aws', 'partner', 'community'])
const KNOWN_SDKS = new Set(['agents', 'evals'])
const DEFAULT_SDK = 'agents'
@@ -35,9 +38,39 @@ export function matchesFilters(card: CardFilterData, state: CatalogFilterState):
if (state.types.size > 0 && !state.types.has(card.type)) return false
if (state.languages.size > 0 && !card.languages.some((l) => state.languages.has(l))) return false
if (state.badges.size > 0 && !card.badges.some((b) => state.badges.has(b))) return false
if (state.maintainedBy.size > 0 && !state.maintainedBy.has(card.maintainedBy)) return false
return true
}
/**
* Cards a facet value would match if it were the sole selection in its facet,
* holding every other facet at the current state. Drives the live chip counts
* so no chip invites a zero-result click.
*/
export function countIfSelected(
cards: CardFilterData[],
state: CatalogFilterState,
facet: 'types' | 'languages' | 'badges' | 'maintainedBy',
value: string
): number {
const probe: CatalogFilterState = {
...state,
types: new Set(state.types),
languages: new Set(state.languages),
badges: new Set(state.badges),
maintainedBy: new Set(state.maintainedBy),
}
probe[facet] = new Set([value])
return cards.filter((card) => matchesFilters(card, probe)).length
}
/** Exclusive facet selection: picking a value replaces the set; re-picking clears it. */
export function selectOnly(set: Set<string>, value: string): void {
const had = set.has(value)
set.clear()
if (!had) set.add(value)
}
/** Serialize non-default state to a query string ('' when everything is default). */
export function stateToQuery(state: CatalogFilterState): string {
const params = new URLSearchParams()
@@ -45,6 +78,7 @@ export function stateToQuery(state: CatalogFilterState): string {
if (state.types.size > 0) params.set('type', [...state.types].sort().join(','))
if (state.languages.size > 0) params.set('lang', [...state.languages].sort().join(','))
if (state.badges.size > 0) params.set('badge', [...state.badges].sort().join(','))
if (state.maintainedBy.size > 0) params.set('by', [...state.maintainedBy].sort().join(','))
if (state.sdk !== DEFAULT_SDK) params.set('sdk', state.sdk)
return params.toString()
}
@@ -60,6 +94,7 @@ export function queryToState(query: string): CatalogFilterState {
types: pick('type', KNOWN_TYPES),
languages: pick('lang', KNOWN_LANGUAGES),
badges: pick('badge', KNOWN_BADGES),
maintainedBy: pick('by', KNOWN_MAINTAINED_BY),
sdk: KNOWN_SDKS.has(sdkParam) ? sdkParam : DEFAULT_SDK,
}
}
+2
View File
@@ -20,6 +20,7 @@ export interface CatalogCardModel {
name: string
description: string
integrationType: CatalogEntryData['integrationType']
maintainedBy: CatalogEntryData['maintainedBy']
sdk: 'agents' | 'evals'
languages: ('python' | 'typescript')[]
href: string
@@ -81,6 +82,7 @@ export function toCardModel(
name: data.name,
description: data.description,
integrationType: data.integrationType,
maintainedBy: data.maintainedBy,
sdk: data.sdk,
languages,
href: docsHref ?? data.docsUrl ?? data.github,
+52
View File
@@ -1,3 +1,5 @@
import { existsSync } from 'node:fs'
import { join } from 'node:path'
import { describe, it, expect } from 'vitest'
import { getCollection } from 'astro:content'
import { catalogEntrySchema } from '../src/content.config'
@@ -69,6 +71,24 @@ describe('catalog content collection', () => {
).toBe(false)
})
it('accepts every maintainer tier and defaults to community', () => {
const base = {
name: 'tiered-entry',
description: 'maintainer tier probe',
integrationType: 'model-provider',
languages: { python: {} },
github: 'https://github.com/strands-agents/harness-sdk',
addedDate: '2025-12-01',
}
for (const maintainedBy of ['strands', 'aws', 'partner', 'community']) {
expect(catalogEntrySchema.safeParse({ ...base, maintainedBy }).success, maintainedBy).toBe(true)
}
const parsed = catalogEntrySchema.safeParse(base)
expect(parsed.success).toBe(true)
if (parsed.success) expect(parsed.data.maintainedBy).toBe('community')
expect(catalogEntrySchema.safeParse({ ...base, maintainedBy: 'official' }).success).toBe(false)
})
it('rejects an unknown language key', () => {
// The languages container is .strict() so a misspelled key
// (`typeScript:`) fails the build instead of silently dropping the
@@ -144,6 +164,38 @@ describe('catalog content collection', () => {
}
})
it('loads built-in SDK entries', async () => {
const entries = await getCollection('catalog')
const anthropic = entries.find((e) => e.id === 'anthropic')
expect(anthropic).toBeDefined()
expect(anthropic!.data.maintainedBy).toBe('strands')
expect(anthropic!.data.docsPage).toBe('docs/user-guide/concepts/model-providers/anthropic')
// The content layer silently drops entries whose YAML fails to parse;
// pinned counts catch that. Update them when granting or removing tiers.
const byTier = Map.groupBy(entries, (e) => e.data.maintainedBy)
expect(byTier.get('strands')?.length).toBe(28)
expect(byTier.get('aws')?.length).toBe(7)
expect(byTier.get('partner')?.length).toBe(21)
// Built-ins point at their source path in the SDK monorepo and always
// have on-site docs.
for (const e of byTier.get('strands') ?? []) {
expect(e.data.github, e.id).toMatch(/^https:\/\/github\.com\/strands-agents\/harness-sdk\//)
expect(e.data.docsPage, e.id).toBeDefined()
}
})
it('built-in github links resolve to real monorepo paths', async () => {
// Built-ins deep-link their source (blob/tree under main); unlike docsPage
// nothing else validates them, so a repo restructure would rot the links
// silently without this.
const entries = await getCollection('catalog')
for (const e of entries.filter((x) => x.data.maintainedBy === 'strands')) {
const m = /^https:\/\/github\.com\/strands-agents\/harness-sdk\/(?:blob|tree)\/main\/(.+)$/.exec(e.data.github)
expect(m, `${e.id}: ${e.data.github}`).not.toBeNull()
expect(existsSync(join('..', m![1])), `${e.id}: ${m![1]} missing from the repo`).toBe(true)
}
})
it('every docsPage points at a real docs collection entry', async () => {
const [entries, docs] = await Promise.all([getCollection('catalog'), getCollection('docs')])
const docIds = new Set(docs.map((d) => d.id))
+45 -1
View File
@@ -1,6 +1,8 @@
import { describe, it, expect } from 'vitest'
import {
countIfSelected,
matchesFilters,
selectOnly,
stateToQuery,
queryToState,
type CatalogFilterState,
@@ -8,7 +10,15 @@ import {
} from '../src/util/catalog-filter'
function state(overrides: Partial<CatalogFilterState> = {}): CatalogFilterState {
return { search: '', types: new Set(), languages: new Set(), badges: new Set(), sdk: 'agents', ...overrides }
return {
search: '',
types: new Set(),
languages: new Set(),
badges: new Set(),
maintainedBy: new Set(),
sdk: 'agents',
...overrides,
}
}
const card: CardFilterData = {
@@ -16,6 +26,7 @@ const card: CardFilterData = {
type: 'tool',
languages: ['python'],
badges: ['verified'],
maintainedBy: 'community',
sdk: 'agents',
}
@@ -69,6 +80,7 @@ describe('URL state round-trip', () => {
types: new Set(['tool', 'plugin']),
languages: new Set(['python']),
badges: new Set(['verified']),
maintainedBy: new Set(['partner']),
sdk: 'evals',
})
const back = queryToState(stateToQuery(s))
@@ -76,9 +88,41 @@ describe('URL state round-trip', () => {
expect(back.types).toEqual(new Set(['tool', 'plugin']))
expect(back.languages).toEqual(new Set(['python']))
expect(back.badges).toEqual(new Set(['verified']))
expect(back.maintainedBy).toEqual(new Set(['partner']))
expect(back.sdk).toBe('evals')
})
it('filters by maintainer tier', () => {
expect(matchesFilters(card, state({ maintainedBy: new Set(['community', 'aws']) }))).toBe(true)
expect(matchesFilters(card, state({ maintainedBy: new Set(['strands']) }))).toBe(false)
})
it('parses maintainer tiers from a deep link', () => {
expect(queryToState('by=strands,aws').maintainedBy).toEqual(new Set(['strands', 'aws']))
expect(queryToState('by=official').maintainedBy).toEqual(new Set())
})
it('countIfSelected holds other facets and swaps in the probed value', () => {
const cards = [
card,
{ ...card, type: 'plugin', maintainedBy: 'aws' },
{ ...card, type: 'tool', maintainedBy: 'aws' },
]
const s = state({ maintainedBy: new Set(['aws']) })
expect(countIfSelected(cards, s, 'types', 'tool')).toBe(1)
expect(countIfSelected(cards, s, 'types', 'session-manager')).toBe(0)
// probing within the same facet replaces the current selection
expect(countIfSelected(cards, s, 'maintainedBy', 'community')).toBe(1)
})
it('selectOnly replaces the selection and re-picking clears it', () => {
const set = new Set(['model-provider'])
selectOnly(set, 'tool')
expect(set).toEqual(new Set(['tool']))
selectOnly(set, 'tool')
expect(set).toEqual(new Set())
})
it('drops unknown values when parsing', () => {
const back = queryToState('type=widget&lang=rust&sdk=nope')
expect(back.types.size).toBe(0)
+1
View File
@@ -11,6 +11,7 @@ function entry(overrides: Partial<CatalogEntryData> = {}): CatalogEntryData {
description: 'Example integration',
integrationType: 'tool',
sdk: 'agents',
maintainedBy: 'community',
languages: { python: { package: 'strands-example' } },
github: 'https://github.com/example/strands-example',
featured: false,