Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/contributing/architecture/primitives.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -211,14 +211,16 @@ primitives:
deployment; inert forks, one-click install (UI-only fork + publish checks
+ immediate activation, with an acknowledgement gate for untrusted
listings), ratings, user reports, admin moderation at
/admin/community-reports, commit-pinned admin trust marks, /community
routes, and lazily derived package icons that track the owner package's
current published commit.
/admin/community-reports, commit-pinned admin trust marks, admin featured
marks that surface trusted listings as onboarding starter packages,
/community routes, and lazily derived package icons that track the owner
package's current published commit.
code:
- packages/worker/src/community/
- packages/worker/src/mcp/capabilities/community/
- packages/worker/migrations/0045-community-listings.sql
- packages/worker/migrations/0059-community-trusted-listings.sql
- packages/worker/migrations/0060-community-featured-listings.sql
- packages/worker/src/app/handlers/community*
- packages/worker/src/app/handlers/admin-community-reports*
- packages/worker/client/routes/community*
Expand Down
14 changes: 14 additions & 0 deletions docs/contributing/community-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ mark; delisted listings cannot be trusted. Surfaces: the `Trusted` badge on
`community_set_trusted` capability. `community_search` and `community_get`
expose the effective `trusted` flag.

Admin **featured** marks live in `featured_at` (migration
`0060-community-featured-listings.sql`) and highlight onboarding starter
packages. `setCommunityListingFeatured` in `service.ts` requires the listing to
be effectively trusted before featuring; the effective `featured` flag
(`featured_at IS NOT NULL AND trusted`) is computed in `repo.ts`, so an owner
republish that drops trust also pulls the listing from onboarding while keeping
the stored mark. `listFeaturedCommunityListings` feeds the onboarding page (slim
`OnboardingFeaturedListing` shapes, capped at 6). Surfaces: the `Featured` badge
on the detail page, the admin-only toggle
(`POST /community/:listingId/feature.json`, audited), the admin-only
`community_set_featured` capability, and the onboarding "Install a starter
package" step. `community_get` exposes the effective `featured` flag.

Reports survive listing deletion via denormalized listing name and owner on the
report row.

Expand Down Expand Up @@ -155,6 +168,7 @@ Capabilities:
- `community_rate`
- `community_report`
- `community_set_trusted` (admin-only via `requiredRole`)
- `community_set_featured` (admin-only via `requiredRole`)

Register the domain in `builtinDomains` and `capabilityDomainNames` like other
builtin domains (see [Adding capabilities](./adding-kody.md)). Do not surface
Expand Down
15 changes: 15 additions & 0 deletions docs/use/community-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,19 @@ publishing it into your account.
Admins toggle trust from the listing detail page or with the
`community_set_trusted` capability.

## Featured listings

Admins can additionally mark trusted listings as **featured**. Featured listings
appear on the onboarding page (`/onboarding`) as starter packages new users can
one-click install. Only trusted listings can be featured, and a listing is only
_shown_ in onboarding while it remains effectively trusted: when the owner
republishes, it silently drops out of onboarding until an admin re-trusts the
new version (the featured mark itself is kept, so re-trusting restores it).

Featured listings show a **Featured** badge on their detail page. Admins toggle
featuring from the listing detail page or with the `community_set_featured`
capability.

## Ratings

After forking, your agent can call `community_rate` with:
Expand Down Expand Up @@ -191,6 +204,8 @@ Use the MCP `community` domain:
- `community_report` — report a listing (requires login)
- `community_set_trusted` — admin-only: mark or unmark a listing as trusted at
its current pinned commit
- `community_set_featured` — admin-only: feature or unfeature a trusted listing
as an onboarding starter package

## Privacy and isolation

Expand Down
130 changes: 130 additions & 0 deletions e2e/community-featured.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { expect, test } from './playwright-utils.ts'
import { seedCommunityListingInE2eDatabase } from './d1-utils.ts'

test('admins can feature trusted listings and members see them in onboarding', async ({
page,
seedE2eUser,
login,
}) => {
const runId = Date.now()
const trustedListingId = `e2e-featured-listing-${runId}`
const trustedListingName = `@kody/featured-package-${runId}`
const untrustedListingId = `e2e-unfeaturable-listing-${runId}`
const adminUser = await seedE2eUser({
email: `featured-admin-${runId}@example.com`,
username: `featured-admin-${runId}`,
password: 'featured-admin-password',
admin: true,
})
const memberUser = await seedE2eUser({
email: `featured-member-${runId}@example.com`,
username: `featured-member-${runId}`,
password: 'featured-member-password',
})
await seedCommunityListingInE2eDatabase({
listingId: trustedListingId,
ownerEmail: memberUser.email,
name: trustedListingName,
description: 'Trusted listing seeded for the featured e2e test.',
tags: ['featured'],
trusted: true,
})
await seedCommunityListingInE2eDatabase({
listingId: untrustedListingId,
ownerEmail: memberUser.email,
name: `@kody/unfeaturable-package-${runId}`,
description: 'Untrusted listing seeded for the featured e2e test.',
tags: ['featured'],
})

// Non-admin members never see the featuring controls and cannot hit the
// API.
await login({
email: memberUser.email,
password: memberUser.password,
mode: 'login',
})
await page.goto(`/community/${trustedListingId}`)
await expect(page.getByText('Fork with your agent')).toBeVisible()
await expect(page.getByTestId('community-admin-feature')).toHaveCount(0)
const memberFeatureResponse = await page.request.post(
`/community/${trustedListingId}/feature.json`,
{
data: { featured: true },
headers: { 'Content-Type': 'application/json' },
},
)
expect(memberFeatureResponse.status()).toBe(403)

// Without any featured listings, onboarding falls back to the browse CTA.
await page.goto('/onboarding')
await expect(
page.getByRole('heading', { name: 'Explore community packages' }),
).toBeVisible()
await expect(page.getByTestId('onboarding-starter-packages')).toHaveCount(0)

// Admins can feature the trusted listing from the detail page.
await page.context().clearCookies()
await login({
email: adminUser.email,
password: adminUser.password,
mode: 'login',
})
await page.goto(`/community/${trustedListingId}`)
await expect(page.getByTestId('community-admin-feature')).toBeVisible()
await page.getByRole('button', { name: 'Feature in onboarding' }).click()
await expect(
page.getByRole('button', { name: 'Remove from onboarding' }),
).toBeVisible()
await expect(
page.getByTestId('community-detail-featured-badge'),
).toBeVisible()

// Untrusted listings cannot be featured: the button stays disabled.
await page.goto(`/community/${untrustedListingId}`)
await expect(
page.getByRole('button', { name: 'Feature in onboarding' }),
).toBeDisabled()
await expect(
page.getByText('Only trusted listings can be featured in onboarding.'),
).toBeVisible()

// Members now see the featured listing as an onboarding starter package.
await page.context().clearCookies()
await login({
email: memberUser.email,
password: memberUser.password,
mode: 'login',
})
await page.goto('/onboarding')
await expect(page.getByTestId('onboarding-starter-packages')).toBeVisible()
const starterLink = page.getByTestId(`onboarding-starter-${trustedListingId}`)
await expect(starterLink).toBeVisible()
await expect(starterLink).toContainText(trustedListingName)
await starterLink.click()
await expect(page).toHaveURL(new RegExp(`/community/${trustedListingId}$`))
await expect(
page.getByRole('button', { name: 'Install', exact: true }),
).toBeVisible()

// Removing the mark pulls the listing from onboarding again.
await page.context().clearCookies()
await login({
email: adminUser.email,
password: adminUser.password,
mode: 'login',
})
await page.goto(`/community/${trustedListingId}`)
await page.getByRole('button', { name: 'Remove from onboarding' }).click()
await expect(
page.getByRole('button', { name: 'Feature in onboarding' }),
).toBeVisible()
await expect(page.getByTestId('community-detail-featured-badge')).toHaveCount(
0,
)
await page.goto('/onboarding')
await expect(
page.getByRole('heading', { name: 'Explore community packages' }),
).toBeVisible()
await expect(page.getByTestId('onboarding-starter-packages')).toHaveCount(0)
})
11 changes: 9 additions & 2 deletions e2e/d1-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ export async function seedCommunityListingInE2eDatabase(input: {
readmeContent?: string | null
sourceId?: string
trusted?: boolean
/** Implies nothing about trust: pass `trusted: true` too for an
* effectively featured (onboarding-visible) listing. */
featured?: boolean
}) {
const ownerUserId = await createStableUserIdFromEmail(input.ownerEmail)
const tagsJson = JSON.stringify(input.tags ?? [])
Expand All @@ -134,6 +137,7 @@ export async function seedCommunityListingInE2eDatabase(input: {
? quoteSqlString('e2e-seeded-admin')
: 'NULL'
const trustedAt = input.trusted ? 'CURRENT_TIMESTAMP' : 'NULL'
const featuredAt = input.featured ? 'CURRENT_TIMESTAMP' : 'NULL'
const sql = `
INSERT INTO community_listings (
id,
Expand All @@ -150,7 +154,8 @@ INSERT INTO community_listings (
status,
trusted_commit,
trusted_by_user_id,
trusted_at
trusted_at,
featured_at
) VALUES (
${quoteSqlString(input.listingId)},
${quoteSqlString(ownerUserId)},
Expand All @@ -166,7 +171,8 @@ INSERT INTO community_listings (
'active',
${trustedCommit},
${trustedByUserId},
${trustedAt}
${trustedAt},
${featuredAt}
)
ON CONFLICT(id) DO UPDATE SET
owner_user_id = excluded.owner_user_id,
Expand All @@ -178,6 +184,7 @@ ON CONFLICT(id) DO UPDATE SET
trusted_commit = excluded.trusted_commit,
trusted_by_user_id = excluded.trusted_by_user_id,
trusted_at = excluded.trusted_at,
featured_at = excluded.featured_at,
updated_at = CURRENT_TIMESTAMP;`.trim()
executeE2eD1Command(sql)
// Icon requests resolve through the listing's KV snapshot; without one the
Expand Down
Loading
Loading