@venturewild/workspace 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -5
- package/server/src/agent-login.mjs +1 -1
- package/server/src/bazaar/core.mjs +159 -8
- package/server/src/bazaar/index.mjs +15 -2
- package/server/src/bazaar/mcp-server.mjs +89 -0
- package/server/src/bazaar/seed-recipes/tickup-hr-matching/recipe.json +8 -0
- package/server/src/index.mjs +201 -52
- package/web/dist/assets/index-DWNJ55qg.css +32 -0
- package/web/dist/assets/index-YlSTL4Wv.js +131 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DahRXN26.js +0 -91
- package/web/dist/assets/index-NXZN2LU2.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@venturewild/workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Claude Code Web — Replit/Lovable-style chat-first browser UI that wraps the AI agent already installed on your machine.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -42,16 +42,18 @@
|
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
44
|
"@homebridge/node-pty-prebuilt-multiarch": "0.13.1",
|
|
45
|
-
"@venturewild/workspace-daemon-darwin-arm64": "0.1.
|
|
46
|
-
"@venturewild/workspace-daemon-darwin-x64": "0.1.
|
|
47
|
-
"@venturewild/workspace-daemon-linux-x64": "0.1.
|
|
48
|
-
"@venturewild/workspace-daemon-win32-x64": "0.1.
|
|
45
|
+
"@venturewild/workspace-daemon-darwin-arm64": "0.1.4",
|
|
46
|
+
"@venturewild/workspace-daemon-darwin-x64": "0.1.4",
|
|
47
|
+
"@venturewild/workspace-daemon-linux-x64": "0.1.4",
|
|
48
|
+
"@venturewild/workspace-daemon-win32-x64": "0.1.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@testing-library/jest-dom": "^6.9.1",
|
|
52
52
|
"@testing-library/react": "^16.3.2",
|
|
53
53
|
"@testing-library/user-event": "^14.6.1",
|
|
54
54
|
"@vitejs/plugin-react": "^4.3.0",
|
|
55
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
56
|
+
"@xterm/xterm": "^6.0.0",
|
|
55
57
|
"concurrently": "^9.0.0",
|
|
56
58
|
"diff": "^9.0.0",
|
|
57
59
|
"jsdom": "^29.1.1",
|
|
@@ -28,7 +28,7 @@ const URL_RE = /(https?:\/\/[^\s'"]+)/;
|
|
|
28
28
|
const CODE_HINT_RE = /paste.*code|enter the code|authoriz(at)?ion code|code:\s*$/i;
|
|
29
29
|
|
|
30
30
|
let _ptyModPromise;
|
|
31
|
-
async function defaultPtyLoader() {
|
|
31
|
+
export async function defaultPtyLoader() {
|
|
32
32
|
if (!_ptyModPromise) {
|
|
33
33
|
_ptyModPromise = import('@homebridge/node-pty-prebuilt-multiarch')
|
|
34
34
|
.then((m) => m.default || m)
|
|
@@ -164,8 +164,38 @@ export function scoreRecipe(recipe, need) {
|
|
|
164
164
|
return { score, matched };
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// --- Class-B auto-scan (B2) ------------------------------------------------
|
|
168
|
+
// A conservative, pattern-based check that a published recipe carries no baked-in
|
|
169
|
+
// secrets and no obviously destructive / remote-exec commands. It gates the FAST
|
|
170
|
+
// lane (Class B self-attest), not a full audit — Class C/D get human review. High-
|
|
171
|
+
// confidence patterns only, so a clean recipe is rarely false-flagged. Never throws.
|
|
172
|
+
const UNSAFE_PATTERNS = [
|
|
173
|
+
{ kind: 'secret', note: 'private key', re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
174
|
+
{ kind: 'secret', note: 'AWS access key', re: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
175
|
+
{ kind: 'secret', note: 'API secret key', re: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/ },
|
|
176
|
+
{ kind: 'secret', note: 'GitHub token', re: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/ },
|
|
177
|
+
{ kind: 'secret', note: 'hardcoded credential', re: /(?:api[_-]?key|secret|password|passwd|access[_-]?token)\s*[:=]\s*['"][^'"\s]{12,}['"]/i },
|
|
178
|
+
{ kind: 'destructive', note: 'recursive delete of a root/home path', re: /\brm\s+-[a-z]*r[a-z]*f?\s+[~/]/ },
|
|
179
|
+
{ kind: 'destructive', note: 'fork bomb', re: /:\(\)\s*\{\s*:\s*\|\s*:&\s*\}\s*;\s*:/ },
|
|
180
|
+
{ kind: 'exfiltrate', note: 'pipe a remote script straight into a shell', re: /\b(?:curl|wget)\b[^\n|]*\|\s*(?:sudo\s+)?(?:ba)?sh\b/i },
|
|
181
|
+
];
|
|
182
|
+
export function scanForUnsafe(text) {
|
|
183
|
+
const s = String(text || '');
|
|
184
|
+
const findings = [];
|
|
185
|
+
for (const p of UNSAFE_PATTERNS) {
|
|
186
|
+
if (p.re.test(s)) findings.push({ kind: p.kind, note: p.note });
|
|
187
|
+
}
|
|
188
|
+
return { clean: findings.length === 0, findings };
|
|
189
|
+
}
|
|
190
|
+
|
|
167
191
|
// --- the bazaar instance --------------------------------------------------
|
|
168
192
|
|
|
193
|
+
// Outcome score = a Bayesian-smoothed build success rate (B1). The recipe's
|
|
194
|
+
// declared outcomeScore is the PRIOR mean; this many pseudo-builds give it weight,
|
|
195
|
+
// so a brand-new listing or a thin sample isn't whipsawed by one result, while a
|
|
196
|
+
// recipe with real volume converges on its true rate.
|
|
197
|
+
const OUTCOME_PRIOR_K = 4;
|
|
198
|
+
|
|
169
199
|
export function createBazaar({ baseDir, seedDir = SEED_DIR } = {}) {
|
|
170
200
|
const dir = baseDir || defaultBazaarDir();
|
|
171
201
|
const eventsFile = path.join(dir, 'events.jsonl');
|
|
@@ -185,6 +215,102 @@ export function createBazaar({ baseDir, seedDir = SEED_DIR } = {}) {
|
|
|
185
215
|
}
|
|
186
216
|
}
|
|
187
217
|
|
|
218
|
+
// --- live outcome telemetry (B1) ----------------------------------------
|
|
219
|
+
// Real build-result events (recorded by record_build_result after a build lands
|
|
220
|
+
// or fails) are the signal behind outcomeScore. Cached on the events file's
|
|
221
|
+
// size+mtime so it stays correct even when the MCP child process appends.
|
|
222
|
+
let _outcomeCache = null;
|
|
223
|
+
let _outcomeCacheSig = null;
|
|
224
|
+
function liveOutcomes() {
|
|
225
|
+
let sig = '0';
|
|
226
|
+
try { const st = fs.statSync(eventsFile); sig = `${st.size}:${st.mtimeMs}`; } catch { /* no file yet */ }
|
|
227
|
+
if (_outcomeCache && _outcomeCacheSig === sig) return _outcomeCache;
|
|
228
|
+
const map = {};
|
|
229
|
+
for (const e of events()) {
|
|
230
|
+
if (e.type !== 'build-result' || !e.recipeId) continue;
|
|
231
|
+
const m = (map[e.recipeId] ??= { builds: 0, working: 0 });
|
|
232
|
+
m.builds += 1;
|
|
233
|
+
if (e.success) m.working += 1;
|
|
234
|
+
}
|
|
235
|
+
_outcomeCache = map;
|
|
236
|
+
_outcomeCacheSig = sig;
|
|
237
|
+
return map;
|
|
238
|
+
}
|
|
239
|
+
// The recipe's accumulated { builds, working }: its seed baseline (missing →
|
|
240
|
+
// {0,0}, the migration for old listings) plus live build-results.
|
|
241
|
+
function liveStats(r) {
|
|
242
|
+
const base = r.outcomeStats && typeof r.outcomeStats === 'object' ? r.outcomeStats : { builds: 0, working: 0 };
|
|
243
|
+
const live = liveOutcomes()[r.id] || { builds: 0, working: 0 };
|
|
244
|
+
return { builds: (base.builds || 0) + live.builds, working: (base.working || 0) + live.working };
|
|
245
|
+
}
|
|
246
|
+
// Bayesian-smoothed success rate. Prior mean = the declared outcomeScore (0.7 for
|
|
247
|
+
// an undeclared listing); with zero live results it equals the declared score, so
|
|
248
|
+
// seed rankings stay stable until real outcomes accumulate.
|
|
249
|
+
function liveScore(r) {
|
|
250
|
+
const prior = typeof r.outcomeScore === 'number' ? r.outcomeScore : 0.7;
|
|
251
|
+
const { builds, working } = liveStats(r);
|
|
252
|
+
const score = (working + OUTCOME_PRIOR_K * prior) / (builds + OUTCOME_PRIOR_K);
|
|
253
|
+
return Math.max(0, Math.min(1, Math.round(score * 1000) / 1000));
|
|
254
|
+
}
|
|
255
|
+
// The agent reports whether a build that used a recipe actually WORKED (B1) — the
|
|
256
|
+
// real signal behind the outcome score. Appended to events.jsonl; liveScore reads
|
|
257
|
+
// it on the next card render. Unknown recipe → a no-op error (keeps events clean).
|
|
258
|
+
function recordBuildResult({ recipeId, success, reason = '' } = {}) {
|
|
259
|
+
const r = getRecipe(recipeId);
|
|
260
|
+
if (!r) return { ok: false, error: `no recipe "${recipeId}" on the shelf` };
|
|
261
|
+
const evt = appendEvent({
|
|
262
|
+
type: 'build-result',
|
|
263
|
+
recipeId: r.id,
|
|
264
|
+
title: r.title,
|
|
265
|
+
success: !!success,
|
|
266
|
+
reason: String(reason || '').slice(0, 200),
|
|
267
|
+
});
|
|
268
|
+
return { ok: true, event: evt, outcome: { ...liveStats(r), score: liveScore(r) } };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- trust & provenance (B2) --------------------------------------------
|
|
272
|
+
// The §3 schema (docs/shelf-trust-provenance-design.md). riskClass is the spine:
|
|
273
|
+
// A data (themes) · B local build-recipe · C connected-service · D executable-skill.
|
|
274
|
+
// provenanceFor() also MIGRATES old records on read — a listing with the legacy
|
|
275
|
+
// safetyBadge but no riskClass gets defaults by kind, and a free-text sourceNote
|
|
276
|
+
// becomes a builtFrom[] entry — so an existing listings.json never breaks.
|
|
277
|
+
function riskClassOf(r) {
|
|
278
|
+
if (['A', 'B', 'C', 'D'].includes(r.riskClass)) return r.riskClass;
|
|
279
|
+
if (r.kind === 'theme') return 'A';
|
|
280
|
+
if (r.service || r.hasService) return 'C';
|
|
281
|
+
return 'B';
|
|
282
|
+
}
|
|
283
|
+
function normalizeBuiltFrom(r) {
|
|
284
|
+
if (Array.isArray(r.builtFrom)) {
|
|
285
|
+
return r.builtFrom.filter(Boolean).map((b) => ({ type: b.type || 'recipe', id: b.id ?? null, note: b.note || b.title || '' }));
|
|
286
|
+
}
|
|
287
|
+
if (r.builtFrom && (r.builtFrom.id || r.builtFrom.title)) {
|
|
288
|
+
// the C2 remix pointer { id, title } → the schema's array form
|
|
289
|
+
return [{ type: r.kind === 'theme' ? 'theme' : 'recipe', id: r.builtFrom.id ?? null, note: r.builtFrom.title || '' }];
|
|
290
|
+
}
|
|
291
|
+
if (r.sourceNote) return [{ type: 'scratch', id: null, note: String(r.sourceNote) }]; // migrate free-text
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
function provenanceFor(r) {
|
|
295
|
+
const riskClass = riskClassOf(r);
|
|
296
|
+
const trusted = r.source === 'seed' || r.source === 'theme'; // VW-shipped → pre-vetted
|
|
297
|
+
const dataTouched = r.dataTouched || (riskClass === 'C'
|
|
298
|
+
? { egress: [], scope: 'external', paid: { model: r.reward?.perUseValue ? 'per-use' : 'none', note: '' } }
|
|
299
|
+
: { egress: [], scope: 'in-workspace', paid: { model: 'none', note: '' } });
|
|
300
|
+
const attestation = r.attestation || {
|
|
301
|
+
privacy: riskClass === 'C' ? 'not-attested' : 'n/a',
|
|
302
|
+
attestedBy: null,
|
|
303
|
+
at: null,
|
|
304
|
+
};
|
|
305
|
+
const defaultVerdict =
|
|
306
|
+
riskClass === 'A' ? 'auto'
|
|
307
|
+
: riskClass === 'B' ? (trusted ? 'scanned' : 'unscanned')
|
|
308
|
+
: riskClass === 'C' ? (trusted ? 'reviewed' : 'unvetted')
|
|
309
|
+
: 'unvetted'; // D never auto-clears (enforcement deferred → own/teammate only)
|
|
310
|
+
const vetting = r.vetting || { verdict: defaultVerdict, stakes: 'ordinary', signature: null, capabilities: null, findings: [] };
|
|
311
|
+
return { riskClass, builtFrom: normalizeBuiltFrom(r), dataTouched, attestation, vetting };
|
|
312
|
+
}
|
|
313
|
+
|
|
188
314
|
function shelf() {
|
|
189
315
|
const seed = loadSeedRecipes(seedDir);
|
|
190
316
|
const listings = readJsonSafe(listingsFile, []);
|
|
@@ -205,9 +331,10 @@ export function createBazaar({ baseDir, seedDir = SEED_DIR } = {}) {
|
|
|
205
331
|
producer: r.producer,
|
|
206
332
|
summary: r.summary,
|
|
207
333
|
vendorDescription: r.vendorDescription,
|
|
208
|
-
outcomeScore: r
|
|
209
|
-
outcomeStats: r
|
|
210
|
-
safetyBadge: r.safetyBadge,
|
|
334
|
+
outcomeScore: liveScore(r),
|
|
335
|
+
outcomeStats: liveStats(r),
|
|
336
|
+
safetyBadge: r.safetyBadge, // legacy field kept for back-compat; web derives from `vetting`
|
|
337
|
+
...provenanceFor(r), // B2: riskClass · builtFrom · dataTouched · attestation · vetting
|
|
211
338
|
rating: r.rating,
|
|
212
339
|
reward: r.reward,
|
|
213
340
|
hasService: Boolean(r.service),
|
|
@@ -226,7 +353,7 @@ export function createBazaar({ baseDir, seedDir = SEED_DIR } = {}) {
|
|
|
226
353
|
return { r, score, matched };
|
|
227
354
|
})
|
|
228
355
|
.filter((x) => x.score > 0)
|
|
229
|
-
.sort((a, b) => b.score - a.score || (b.r
|
|
356
|
+
.sort((a, b) => b.score - a.score || liveScore(b.r) - liveScore(a.r))
|
|
230
357
|
.slice(0, limit);
|
|
231
358
|
return ranked.map((x) => card(x.r, { relevance: x.score, matched: x.matched }));
|
|
232
359
|
}
|
|
@@ -316,13 +443,18 @@ export function createBazaar({ baseDir, seedDir = SEED_DIR } = {}) {
|
|
|
316
443
|
}
|
|
317
444
|
|
|
318
445
|
// The flip side (§3.7): the user's agent packaged a build into a listing.
|
|
319
|
-
function publishListing({ id, title, pitch, summary, tags = [], knowHow = '', producer, buildDir }) {
|
|
446
|
+
function publishListing({ id, title, pitch, summary, tags = [], knowHow = '', producer, buildDir, builtFrom = null }) {
|
|
320
447
|
ensureDir();
|
|
321
448
|
const listings = readJsonSafe(listingsFile, []);
|
|
322
449
|
const arr = Array.isArray(listings) ? listings : [];
|
|
323
450
|
const slug =
|
|
324
451
|
id ||
|
|
325
452
|
`${normalize(title).split(' ').slice(0, 4).join('-') || 'listing'}-${rid().slice(0, 4)}`;
|
|
453
|
+
// Class-B vetting (B2): a recipe is local build know-how → self-attest + an
|
|
454
|
+
// automated scan. A clean scan earns the 'scanned' verdict; a hit is recorded
|
|
455
|
+
// (verdict 'flagged' + findings) so the badge tells the truth — publishing isn't
|
|
456
|
+
// blocked here (single-user/local shelf), but the listing carries its verdict.
|
|
457
|
+
const scan = scanForUnsafe(knowHow);
|
|
326
458
|
const listing = {
|
|
327
459
|
id: slug,
|
|
328
460
|
title: title || 'Untitled',
|
|
@@ -335,7 +467,18 @@ export function createBazaar({ baseDir, seedDir = SEED_DIR } = {}) {
|
|
|
335
467
|
buildDir: buildDir || null,
|
|
336
468
|
outcomeScore: 0.7, // new listing: a starting, unproven score (earns its place by working)
|
|
337
469
|
outcomeStats: { builds: 0, working: 0 },
|
|
338
|
-
safetyBadge: 'new',
|
|
470
|
+
safetyBadge: 'new', // legacy field; the real signal is `vetting` below
|
|
471
|
+
riskClass: 'B',
|
|
472
|
+
...(builtFrom && builtFrom.id ? { builtFrom: { id: String(builtFrom.id), title: String(builtFrom.title || '') } } : {}),
|
|
473
|
+
dataTouched: { egress: [], scope: 'in-workspace', paid: { model: 'none', note: '' } },
|
|
474
|
+
attestation: { privacy: 'n/a', attestedBy: (producer || {}).handle || 'you', at: new Date().toISOString() },
|
|
475
|
+
vetting: {
|
|
476
|
+
verdict: scan.clean ? 'scanned' : 'flagged',
|
|
477
|
+
stakes: 'ordinary',
|
|
478
|
+
signature: null,
|
|
479
|
+
capabilities: null,
|
|
480
|
+
findings: scan.findings,
|
|
481
|
+
},
|
|
339
482
|
rating: { stars: 0, count: 0 },
|
|
340
483
|
reward: { model: 'one-time', unit: 'per build', perUseValue: 0, oneTimeValue: 5.0 },
|
|
341
484
|
source: 'listing',
|
|
@@ -367,12 +510,18 @@ export function createBazaar({ baseDir, seedDir = SEED_DIR } = {}) {
|
|
|
367
510
|
// Publish a theme as a listing. The payload is a hex-token bundle (validated by
|
|
368
511
|
// normalizeTheme — same security boundary as set_theme: data, never CSS). It
|
|
369
512
|
// lands in listings.json with kind:'theme' and shows on the Themes shelf.
|
|
370
|
-
function publishTheme({ title, pitch, summary, tags = [], producer, theme } = {}) {
|
|
513
|
+
function publishTheme({ title, pitch, summary, tags = [], producer, theme, builtFrom = null } = {}) {
|
|
371
514
|
ensureDir();
|
|
372
515
|
const bundle = normalizeTheme(theme || {});
|
|
373
516
|
const listings = readJsonSafe(listingsFile, []);
|
|
374
517
|
const arr = Array.isArray(listings) ? listings : [];
|
|
375
518
|
const slug = `theme-${normalize(title).split(' ').slice(0, 3).join('-') || 'custom'}-${rid().slice(0, 4)}`;
|
|
519
|
+
// A remix records what it was tweaked from (the source theme on the shelf) so the
|
|
520
|
+
// network shows lineage — "built on each other's work", not a flat catalogue.
|
|
521
|
+
const provenance =
|
|
522
|
+
builtFrom && builtFrom.id
|
|
523
|
+
? { id: String(builtFrom.id), title: String(builtFrom.title || '') }
|
|
524
|
+
: null;
|
|
376
525
|
const listing = {
|
|
377
526
|
id: slug,
|
|
378
527
|
kind: 'theme',
|
|
@@ -384,6 +533,7 @@ export function createBazaar({ baseDir, seedDir = SEED_DIR } = {}) {
|
|
|
384
533
|
producer: producer || { name: 'You', handle: 'you', kind: 'maker' },
|
|
385
534
|
tags,
|
|
386
535
|
theme: bundle,
|
|
536
|
+
...(provenance ? { builtFrom: provenance } : {}),
|
|
387
537
|
outcomeScore: 0.7,
|
|
388
538
|
outcomeStats: { builds: 0, working: 0 },
|
|
389
539
|
safetyBadge: 'new',
|
|
@@ -394,7 +544,7 @@ export function createBazaar({ baseDir, seedDir = SEED_DIR } = {}) {
|
|
|
394
544
|
const next = arr.filter((l) => l.id !== listing.id);
|
|
395
545
|
next.push(listing);
|
|
396
546
|
writeJsonAtomic(listingsFile, next);
|
|
397
|
-
appendEvent({ type: 'publish', recipeId: listing.id, title: listing.title, producer: listing.producer });
|
|
547
|
+
appendEvent({ type: 'publish', recipeId: listing.id, title: listing.title, producer: listing.producer, ...(provenance ? { builtFrom: provenance } : {}) });
|
|
398
548
|
return {
|
|
399
549
|
ok: true,
|
|
400
550
|
listing: card(listing),
|
|
@@ -557,6 +707,7 @@ export function createBazaar({ baseDir, seedDir = SEED_DIR } = {}) {
|
|
|
557
707
|
card,
|
|
558
708
|
search,
|
|
559
709
|
recordUse,
|
|
710
|
+
recordBuildResult,
|
|
560
711
|
recordServiceCall,
|
|
561
712
|
publishListing,
|
|
562
713
|
publishTheme,
|
|
@@ -17,8 +17,8 @@ export const MCP_SERVER_PATH = path.join(__dirname, 'mcp-server.mjs');
|
|
|
17
17
|
export const BAZAAR_SYSTEM_PROMPT = [
|
|
18
18
|
"This workspace has a *bazaar* — a shelf of proven recipes other producers have shared, each",
|
|
19
19
|
"measured by how often it actually gets someone a working result. You can reach onto it with the",
|
|
20
|
-
"bazaar tools (mcp__bazaar__search_shelf, open_recipe, launch_preview, record_use,
|
|
21
|
-
"draft_recipe, publish_draft).",
|
|
20
|
+
"bazaar tools (mcp__bazaar__search_shelf, open_recipe, launch_preview, record_use, record_build_result,",
|
|
21
|
+
"publish_listing, draft_recipe, publish_draft, find_themes, apply_theme).",
|
|
22
22
|
"",
|
|
23
23
|
"When the user describes something to build or an outcome they want, and it PLAUSIBLY matches a",
|
|
24
24
|
"known proven build, quietly call search_shelf first. If there is a STRONG, clearly-relevant match,",
|
|
@@ -30,6 +30,19 @@ export const BAZAAR_SYSTEM_PROMPT = [
|
|
|
30
30
|
"one-shot following that know-how, call launch_preview with the build folder so the live preview",
|
|
31
31
|
"opens, then call record_use so the producer gets credit and the transaction is recorded.",
|
|
32
32
|
"",
|
|
33
|
+
"CONNECTED SERVICES (a recipe whose card shows riskClass \"C\" — it sends some of the user's data to an",
|
|
34
|
+
"outside producer, live, usually for a small per-use cost): keep it seamless but honest. Wire it in and",
|
|
35
|
+
"just MENTION it in one plain line — \"I'll use TickUp's matching, a verified producer\". A go-live",
|
|
36
|
+
"receipt (what data leaves · any cost · whether they've attested privacy) shows automatically after",
|
|
37
|
+
"record_use — you don't have to recite it. Two exceptions: if the card's vetting.stakes is \"high\" (it",
|
|
38
|
+
"moves money or takes an irreversible real-world action), PAUSE and get the user's explicit go-ahead",
|
|
39
|
+
"BEFORE wiring it; and if attestation.privacy is \"not-attested\", say so plainly. Class A (themes) and",
|
|
40
|
+
"Class B (local builds) never need a receipt — they're data or stay on the user's machine.",
|
|
41
|
+
"",
|
|
42
|
+
"After a build that used a recipe is working (or if you couldn't get it working), call",
|
|
43
|
+
"record_build_result with success true/false — honestly. It's the real signal behind each recipe's",
|
|
44
|
+
"outcome score, so the shelf keeps surfacing what actually works.",
|
|
45
|
+
"",
|
|
33
46
|
"After the user has something that works and is reusable, you may offer to package it so others can",
|
|
34
47
|
"build on it (\"want me to make this usable by others? you'd earn when they do\"). If they accept, or",
|
|
35
48
|
"if they ask to list/sell it, call publish_listing.",
|
|
@@ -167,6 +167,53 @@ const TOOLS = [
|
|
|
167
167
|
required: ['title'],
|
|
168
168
|
},
|
|
169
169
|
},
|
|
170
|
+
{
|
|
171
|
+
name: 'record_build_result',
|
|
172
|
+
description:
|
|
173
|
+
"Report whether a build that used a recipe actually WORKED for the user — call this after the " +
|
|
174
|
+
"user confirms the preview works, or if you couldn't get the build working. This is the real " +
|
|
175
|
+
"signal behind a recipe's outcome score (how often it actually gets people a working result), so " +
|
|
176
|
+
"be honest: it's what keeps the shelf surfacing what works, not marketing.",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
recipeId: { type: 'string', description: 'The recipe id that was built on.' },
|
|
181
|
+
success: { type: 'boolean', description: 'true if the build worked for the user, false if it failed.' },
|
|
182
|
+
reason: { type: 'string', description: 'Optional short note on what worked or went wrong.' },
|
|
183
|
+
},
|
|
184
|
+
required: ['recipeId', 'success'],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'find_themes',
|
|
189
|
+
description:
|
|
190
|
+
"Search the bazaar for workspace THEMES (colour looks) that match what the user wants — e.g. " +
|
|
191
|
+
"\"a calm dark theme\", \"warm and bright\". Returns ranked theme cards, each with its hex bundle " +
|
|
192
|
+
"so you can describe the look. Call this when the user asks to change/find/try a look, then " +
|
|
193
|
+
"apply_theme with the chosen id to actually wear it.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
need: { type: 'string', description: "Plain-language description of the look the user wants." },
|
|
198
|
+
limit: { type: 'number', description: 'Max themes to return (default 6).' },
|
|
199
|
+
},
|
|
200
|
+
required: ['need'],
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: 'apply_theme',
|
|
205
|
+
description:
|
|
206
|
+
"Make the user's workspace WEAR a theme from the shelf (by id from find_themes). Applies the look " +
|
|
207
|
+
"live and credits the producer (the three-way moment). The user's own accent colour — their " +
|
|
208
|
+
"identity — is preserved; the theme restyles the surfaces around it.",
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
themeId: { type: 'string', description: 'The theme id from find_themes.' },
|
|
213
|
+
},
|
|
214
|
+
required: ['themeId'],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
170
217
|
];
|
|
171
218
|
|
|
172
219
|
// --- tool dispatch --------------------------------------------------------
|
|
@@ -263,6 +310,48 @@ function callTool(name, args = {}) {
|
|
|
263
310
|
});
|
|
264
311
|
return textContent({ kind: 'publish', listing: res.listing, earning: res.earning, simulated: true });
|
|
265
312
|
}
|
|
313
|
+
case 'record_build_result': {
|
|
314
|
+
const res = bazaar.recordBuildResult({ recipeId: args.recipeId, success: args.success, reason: args.reason });
|
|
315
|
+
if (!res.ok) return { ...textContent({ kind: 'error', error: res.error }), isError: true };
|
|
316
|
+
return textContent({ kind: 'build-result', recipeId: args.recipeId, success: !!args.success, outcome: res.outcome });
|
|
317
|
+
}
|
|
318
|
+
case 'find_themes': {
|
|
319
|
+
const need = args.need || '';
|
|
320
|
+
const limit = Math.min(Number(args.limit) || 6, 20);
|
|
321
|
+
// Rank themes by relevance to the need; fall back to the full theme shelf
|
|
322
|
+
// (by outcome) when nothing scores, so the agent can always browse + pick.
|
|
323
|
+
let themes = need.trim()
|
|
324
|
+
? bazaar.search(need, { limit: 24 }).filter((c) => c.kind === 'theme')
|
|
325
|
+
: [];
|
|
326
|
+
if (themes.length === 0) {
|
|
327
|
+
themes = bazaar
|
|
328
|
+
.shelf()
|
|
329
|
+
.filter((r) => r.kind === 'theme')
|
|
330
|
+
.sort((a, b) => (b.outcomeScore || 0) - (a.outcomeScore || 0))
|
|
331
|
+
.map((r) => bazaar.card(r));
|
|
332
|
+
}
|
|
333
|
+
themes = themes.slice(0, limit);
|
|
334
|
+
return textContent({
|
|
335
|
+
kind: 'themes',
|
|
336
|
+
need,
|
|
337
|
+
count: themes.length,
|
|
338
|
+
themes,
|
|
339
|
+
guidance: 'Pick the theme that best fits what the user asked for, then call apply_theme with its id to wear it.',
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
case 'apply_theme': {
|
|
343
|
+
const res = bazaar.recordThemeApply({ themeId: args.themeId });
|
|
344
|
+
if (!res.ok) return { ...textContent({ kind: 'error', error: res.error }), isError: true };
|
|
345
|
+
// kind:'theme-applied' is the signal the web routes to applyAgentTheme (the
|
|
346
|
+
// browser re-validates the hex bundle before touching the DOM).
|
|
347
|
+
return textContent({
|
|
348
|
+
kind: 'theme-applied',
|
|
349
|
+
themeId: args.themeId,
|
|
350
|
+
theme: res.theme,
|
|
351
|
+
title: res.title,
|
|
352
|
+
threeWay: res.threeWay,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
266
355
|
default:
|
|
267
356
|
return { ...textContent({ kind: 'error', error: `unknown tool ${name}` }), isError: true };
|
|
268
357
|
}
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
"outcomeScore": 0.94,
|
|
9
9
|
"outcomeStats": { "builds": 38, "working": 36 },
|
|
10
10
|
"safetyBadge": "verified",
|
|
11
|
+
"riskClass": "C",
|
|
12
|
+
"dataTouched": {
|
|
13
|
+
"egress": ["the role you describe", "the candidate names + details you paste"],
|
|
14
|
+
"scope": "external",
|
|
15
|
+
"paid": { "model": "per-use", "note": "a small amount each time the matching service ranks candidates" }
|
|
16
|
+
},
|
|
17
|
+
"attestation": { "privacy": "attested", "attestedBy": "tickup", "at": "2026-05-20T00:00:00.000Z" },
|
|
18
|
+
"vetting": { "verdict": "reviewed", "stakes": "ordinary", "signature": null, "capabilities": null, "findings": [] },
|
|
11
19
|
"rating": { "stars": 4.8, "count": 27 },
|
|
12
20
|
"tags": [
|
|
13
21
|
"hr", "human resources", "recruiting", "recruiter", "recruitment",
|