fix(aiui): fabricated demo content is demo-build-only, fake credential scrubbed (S7)

Per operator decision 2026-08-07 (mock content is isolated to
demo.archipelago-foundation.org, never in shipped code): the auto-seeded
'node-demo' conversation (invented balances, file listings, bitcoin.conf)
no longer ships on nodes — a VITE_DEMO_CONTENT build flag (or dev) gates
it, /seed, and the Guide 'Load Demo' button. The genuine onboarding guide
still seeds everywhere. The fixture's bitcoin.conf rpcpassword is now
unmistakably example-shaped: fake must never look like a real credential.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-07 06:28:51 -04:00
co-authored by Claude
parent 4e6df488fc
commit 9bb38c36d3
6 changed files with 50 additions and 11 deletions
@@ -113,7 +113,7 @@ txindex=1
# RPC Settings # RPC Settings
rpcuser=archipelago rpcuser=archipelago
rpcpassword=archipelago123 rpcpassword=EXAMPLE-ONLY-not-a-real-password
rpcallowip=127.0.0.1 rpcallowip=127.0.0.1
rpcbind=127.0.0.1 rpcbind=127.0.0.1
rpcport=8332 rpcport=8332
@@ -148,6 +148,7 @@
import { computed, ref, watch, nextTick } from 'vue' import { computed, ref, watch, nextTick } from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual' import { useVirtualizer } from '@tanstack/vue-virtual'
import { useChatStore } from '@/stores/chat' import { useChatStore } from '@/stores/chat'
import { DEMO_CONTENT_ENABLED } from '@/utils/demoContent'
import { useAI, streamWithModel } from '@/composables/useAI' import { useAI, streamWithModel } from '@/composables/useAI'
import { useContentPanel } from '@/composables/useContentPanel' import { useContentPanel } from '@/composables/useContentPanel'
import { useComparisonMode } from '@/composables/useComparisonMode' import { useComparisonMode } from '@/composables/useComparisonMode'
@@ -354,6 +355,18 @@ async function handleSend(text: string, images: ImageAttachment[] = []) {
} }
if (trimmed === '/seed') { if (trimmed === '/seed') {
// Demo-site/dev only (S7): the seed showcase is fabricated example
// content and must not ship in the node build.
if (!DEMO_CONTENT_ENABLED) {
const convId = chatStore.activeConversationId
if (convId) {
chatStore.addMessage(convId, {
role: 'assistant',
content: 'The `/seed` showcase is only available in demo builds.',
})
}
return
}
await chatStore.loadSeedChats() await chatStore.loadSeedChats()
// loadSeedChats already switches to the seed conversation // loadSeedChats already switches to the seed conversation
// Collapse chat so user sees PromptIndex for quick picking // Collapse chat so user sees PromptIndex for quick picking
@@ -86,6 +86,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { usePromptTemplateStore, extractVariables, type PromptTemplate } from '@/stores/promptTemplates' import { usePromptTemplateStore, extractVariables, type PromptTemplate } from '@/stores/promptTemplates'
import { DEMO_CONTENT_ENABLED } from '@/utils/demoContent'
interface PaletteCommand { interface PaletteCommand {
id: string id: string
@@ -99,7 +100,10 @@ const BUILT_IN_COMMANDS: PaletteCommand[] = [
{ id: 'cmd-nostr', slash: '/nostr', title: 'Nostr', preview: 'Browse the Nostr network feed' }, { id: 'cmd-nostr', slash: '/nostr', title: 'Nostr', preview: 'Browse the Nostr network feed' },
{ id: 'cmd-design', slash: '/design', title: 'Design System', preview: 'Open the design system viewer' }, { id: 'cmd-design', slash: '/design', title: 'Design System', preview: 'Open the design system viewer' },
{ id: 'cmd-search', slash: '/search ', title: 'Search', preview: 'Search your content library' }, { id: 'cmd-search', slash: '/search ', title: 'Search', preview: 'Search your content library' },
{ id: 'cmd-seed', slash: '/seed', title: 'Seed', preview: 'Load seed conversations for all content types' }, // /seed loads fabricated showcase content — demo-site/dev builds only (S7)
...(DEMO_CONTENT_ENABLED
? [{ id: 'cmd-seed', slash: '/seed', title: 'Seed', preview: 'Load seed conversations for all content types' }]
: []),
{ id: 'cmd-freefilms', slash: '/freefilms', title: 'Free Films', preview: 'Browse free documentary films from InDeeHub' }, { id: 'cmd-freefilms', slash: '/freefilms', title: 'Free Films', preview: 'Browse free documentary films from InDeeHub' },
] ]
+4 -2
View File
@@ -197,8 +197,9 @@
</ul> </ul>
</section> </section>
<!-- Try It --> <!-- Try It — demo builds only (S7): the demo conversation is
<section class="guide-section"> fabricated example node state, demo-site/dev only -->
<section v-if="DEMO_CONTENT_ENABLED" class="guide-section">
<h2 class="section-title"> <h2 class="section-title">
<span class="section-icon">&#x1F680;</span> <span class="section-icon">&#x1F680;</span>
Try It Out Try It Out
@@ -220,6 +221,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useChatStore } from '@/stores/chat' import { useChatStore } from '@/stores/chat'
import { DEMO_CONTENT_ENABLED } from '@/utils/demoContent'
const router = useRouter() const router = useRouter()
const chatStore = useChatStore() const chatStore = useChatStore()
+16 -7
View File
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import type { Message, Conversation, WebSearchResult } from '@aiui/core/types/message' import type { Message, Conversation, WebSearchResult } from '@aiui/core/types/message'
import { apiFetch } from '@/utils/api-fetch' import { apiFetch } from '@/utils/api-fetch'
import { DEMO_CONTENT_ENABLED } from '@/utils/demoContent'
function generateId(): string { function generateId(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
@@ -406,8 +407,9 @@ export const useChatStore = defineStore('chat', () => {
return siblings return siblings
} }
/** Load all seed prompts as a single conversation */ /** Load all seed prompts as a single conversation — demo builds only (S7) */
async function loadSeedChats(): Promise<number> { async function loadSeedChats(): Promise<number> {
if (!DEMO_CONTENT_ENABLED) return 0
try { try {
const { seedPromptsToConversation } = await import('@/__tests__/fixtures/seedPrompts') const { seedPromptsToConversation } = await import('@/__tests__/fixtures/seedPrompts')
const conv = seedPromptsToConversation() as Conversation const conv = seedPromptsToConversation() as Conversation
@@ -429,8 +431,10 @@ export const useChatStore = defineStore('chat', () => {
} }
} }
/** Load node demo conversation showing local node capabilities */ /** Load node demo conversation — demo builds only (S7); the content is
* fabricated node state and must not ship on real nodes */
async function loadNodeDemoChat(): Promise<number> { async function loadNodeDemoChat(): Promise<number> {
if (!DEMO_CONTENT_ENABLED) return 0
try { try {
const { nodeDemoToConversation } = await import('@/__tests__/fixtures/nodeDemoPrompts') const { nodeDemoToConversation } = await import('@/__tests__/fixtures/nodeDemoPrompts')
const conv = nodeDemoToConversation() as Conversation const conv = nodeDemoToConversation() as Conversation
@@ -472,11 +476,16 @@ export const useChatStore = defineStore('chat', () => {
} }
} }
/** Seed demo conversations (guide + node demo) on first use */ /** Seed demo conversations (guide always; node demo only in demo builds) */
async function seedDemoConversations(): Promise<void> { async function seedDemoConversations(): Promise<void> {
// The guide is genuine onboarding documentation — always seed it.
// `node-demo` is FABRICATED node state (invented balances, file listings,
// a fake bitcoin.conf) — demo-site/dev builds only (S7, operator
// decision 2026-08-07: mocks belong to the demo site, not shipped code).
const wantDemo = DEMO_CONTENT_ENABLED
const hasGuide = conversations.value.has('aiui-guide') const hasGuide = conversations.value.has('aiui-guide')
const hasDemo = conversations.value.has('node-demo') const hasDemo = conversations.value.has('node-demo')
if (hasGuide && hasDemo) { if (hasGuide && (hasDemo || !wantDemo)) {
// Already seeded — still select guide if nothing active // Already seeded — still select guide if nothing active
if (!activeConversationId.value) { if (!activeConversationId.value) {
activeConversationId.value = 'aiui-guide' activeConversationId.value = 'aiui-guide'
@@ -485,15 +494,15 @@ export const useChatStore = defineStore('chat', () => {
} }
try { try {
const { guideToConversation } = await import('@/__tests__/fixtures/guideConversation') const { guideToConversation } = await import('@/__tests__/fixtures/guideConversation')
const { nodeDemoToConversation } = await import('@/__tests__/fixtures/nodeDemoPrompts')
const guide = guideToConversation() as Conversation const guide = guideToConversation() as Conversation
const demo = nodeDemoToConversation() as Conversation
const merged = new Map(conversations.value) const merged = new Map(conversations.value)
if (!merged.has(guide.id)) { if (!merged.has(guide.id)) {
merged.set(guide.id, guide) merged.set(guide.id, guide)
immediateIDBSave(guide) immediateIDBSave(guide)
} }
if (!merged.has(demo.id)) { if (wantDemo && !merged.has('node-demo')) {
const { nodeDemoToConversation } = await import('@/__tests__/fixtures/nodeDemoPrompts')
const demo = nodeDemoToConversation() as Conversation
merged.set(demo.id, demo) merged.set(demo.id, demo)
immediateIDBSave(demo) immediateIDBSave(demo)
} }
@@ -0,0 +1,11 @@
/**
* Demo-content gate (operator decision 2026-08-07): seed/mock showcase
* conversations exist for the demo.archipelago-foundation.org website ONLY
* and must not ship in the node build. Demo builds set
* `VITE_DEMO_CONTENT=true`; local dev keeps them for development.
*
* Anything behind this flag is fabricated example content — never present
* it as real node data in a shipped build.
*/
export const DEMO_CONTENT_ENABLED =
import.meta.env.DEV || import.meta.env.VITE_DEMO_CONTENT === 'true'