Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- scripts/documenso-cert/.gitkeep +0 -0
- scripts/documenso-cert/.passphrase +1 -0
- scripts/documenso-cert/README.md +28 -0
- scripts/documenso-cert/cert.p12 +0 -0
- scripts/generate-secrets.sh +148 -0
- scripts/init-databases.sql +43 -0
- scripts/render-build.sh +25 -0
- scripts/sync-env.sh +95 -0
- scripts/synthetic-agents/agents-manifest.json +347 -0
- scripts/synthetic-agents/lib/reporter.ts +198 -0
- scripts/synthetic-agents/lib/scenario-runner.ts +122 -0
- scripts/synthetic-agents/lib/trpc-client.ts +62 -0
- scripts/synthetic-agents/orchestrator.ts +194 -0
- scripts/synthetic-agents/package.json +18 -0
- scripts/synthetic-agents/podman-compose.yml +229 -0
- scripts/synthetic-agents/run-agents.sh +146 -0
- scripts/synthetic-agents/scenarios/admin-operations.ts +86 -0
- scripts/synthetic-agents/scenarios/booking-recording.ts +99 -0
- scripts/synthetic-agents/scenarios/calcom-scenario.ts +100 -0
- scripts/synthetic-agents/scenarios/cross-workspace.ts +174 -0
- scripts/synthetic-agents/scenarios/document-scenario.ts +138 -0
- scripts/synthetic-agents/scenarios/guest-pipeline.ts +110 -0
- scripts/synthetic-agents/scenarios/index.ts +30 -0
- scripts/synthetic-agents/scenarios/outreach-sequences.ts +79 -0
- scripts/synthetic-agents/scenarios/podcast-operations.ts +58 -0
- scripts/synthetic-agents/scenarios/security-policy-scenario.ts +87 -0
- scripts/synthetic-agents/scenarios/service-administration.ts +102 -0
- scripts/synthetic-agents/scenarios/support-scenario.ts +152 -0
- scripts/synthetic-agents/scenarios/verified-network.ts +118 -0
- scripts/synthetic-agents/scenarios/workspace-lifecycle.ts +77 -0
- scripts/synthetic-agents/test-report.txt +10 -0
- scripts/synthetic-agents/tsconfig.json +13 -0
- scripts/synthetic-agents/weaknesses.json +1 -0
scripts/documenso-cert/.gitkeep
ADDED
|
File without changes
|
scripts/documenso-cert/.passphrase
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
PASSPHRASE=ed8a3ab79627281950bd22dcaada161a0182e1ab3a2d83455b369bf4318885b5
|
scripts/documenso-cert/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Documenso signing certificate
|
| 2 |
+
|
| 3 |
+
Documenso requires a signing certificate to digitally sign documents.
|
| 4 |
+
The compose file mounts this directory read-only into the container at
|
| 5 |
+
`/opt/documenso/cert`, and the app looks for `cert.p12` inside it.
|
| 6 |
+
|
| 7 |
+
## Generate a self-signed cert (dev only)
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
|
| 11 |
+
-days 365 -nodes -subj "/CN=MicProtocol Documenso"
|
| 12 |
+
openssl pkcs12 -export -out scripts/documenso-cert/cert.p12 \
|
| 13 |
+
-inkey key.pem -in cert.pem \
|
| 14 |
+
-passout pass:"$(openssl rand -hex 32)"
|
| 15 |
+
rm key.pem cert.pem
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
Then set in `.env`:
|
| 19 |
+
|
| 20 |
+
```
|
| 21 |
+
DOCUMENSO_SIGNING_PASSPHRASE=<the passphrase you used above>
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
## Production
|
| 25 |
+
|
| 26 |
+
Use a CA-issued signing certificate (e.g. from a qualified trust
|
| 27 |
+
provider). Never ship the cert or passphrase in git β keep both
|
| 28 |
+
out of version control.
|
scripts/documenso-cert/cert.p12
ADDED
|
Binary file (4.23 kB). View file
|
|
|
scripts/generate-secrets.sh
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Generate all production secrets for MicProtocol self-hosted integrations
|
| 3 |
+
# Run this once during initial setup. Save output to .env.production
|
| 4 |
+
|
| 5 |
+
set -euo pipefail
|
| 6 |
+
|
| 7 |
+
echo "# ============================================================"
|
| 8 |
+
echo "# MicProtocol Production Secrets - Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
| 9 |
+
echo "# ============================================================"
|
| 10 |
+
echo "# IMPORTANT: Save this output securely. Do not commit to git."
|
| 11 |
+
echo "# ============================================================"
|
| 12 |
+
echo ""
|
| 13 |
+
|
| 14 |
+
# Core Infrastructure
|
| 15 |
+
echo "# βββ Core Infrastructure βββββββββββββββββββββββββββββββββββ"
|
| 16 |
+
POSTGRES_PASSWORD=$(openssl rand -base64 32 | tr -d '/+=' | cut -c1-32)
|
| 17 |
+
REDIS_PASSWORD=$(openssl rand -base64 32 | tr -d '/+=' | cut -c1-32)
|
| 18 |
+
MEILISEARCH_API_KEY=$(openssl rand -base64 32 | tr -d '/+=' | cut -c1-32)
|
| 19 |
+
MINIO_ROOT_USER="micprotocol_admin"
|
| 20 |
+
MINIO_ROOT_PASSWORD=$(openssl rand -base64 32 | tr -d '/+=' | cut -c1-32)
|
| 21 |
+
echo "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}"
|
| 22 |
+
echo "REDIS_PASSWORD=${REDIS_PASSWORD}"
|
| 23 |
+
echo "MEILISEARCH_API_KEY=${MEILISEARCH_API_KEY}"
|
| 24 |
+
echo "MINIO_ROOT_USER=${MINIO_ROOT_USER}"
|
| 25 |
+
echo "MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}"
|
| 26 |
+
echo ""
|
| 27 |
+
|
| 28 |
+
# JWT/Encryption Keys (NEVER ROTATE THESE - data loss if rotated)
|
| 29 |
+
echo "# βββ Encryption Keys (NEVER ROTATE) ββββββββββββββββββββββββ"
|
| 30 |
+
ENCRYPTION_KEY=$(openssl rand -hex 32)
|
| 31 |
+
JWT_SECRET=$(openssl rand -base64 64 | tr -d '/+=' | cut -c1-64)
|
| 32 |
+
NEXTAUTH_SECRET=$(openssl rand -base64 32)
|
| 33 |
+
echo "ENCRYPTION_KEY=${ENCRYPTION_KEY}"
|
| 34 |
+
echo "JWT_SECRET=${JWT_SECRET}"
|
| 35 |
+
echo "NEXTAUTH_SECRET=${NEXTAUTH_SECRET}"
|
| 36 |
+
echo ""
|
| 37 |
+
|
| 38 |
+
# Chatwoot
|
| 39 |
+
echo "# βββ Chatwoot (Customer Support) ββββββββββββββββββββββββββ"
|
| 40 |
+
CHATWOOT_SECRET_KEY_BASE=$(openssl rand -hex 64)
|
| 41 |
+
CHATWOOT_WEBHOOK_SECRET=$(openssl rand -hex 32)
|
| 42 |
+
echo "CHATWOOT_SECRET_KEY_BASE=${CHATWOOT_SECRET_KEY_BASE}"
|
| 43 |
+
echo "CHATWOOT_WEBHOOK_SECRET=${CHATWOOT_WEBHOOK_SECRET}"
|
| 44 |
+
echo "# Generate these in Chatwoot UI after setup:"
|
| 45 |
+
echo "# CHATWOOT_INBOX_ID=<from Settings β Inboxes β Website>"
|
| 46 |
+
echo "# NEXT_PUBLIC_CHATWOOT_WEBSITE_TOKEN=<from Settings β Inboxes β Website β Configuration>"
|
| 47 |
+
echo ""
|
| 48 |
+
|
| 49 |
+
# Cal.com
|
| 50 |
+
echo "# βββ Cal.com (Scheduling) ββββββββββββββββββββββββββββββββββ"
|
| 51 |
+
CALCOM_NEXTAUTH_SECRET=$(openssl rand -base64 32)
|
| 52 |
+
CALCOM_ENCRYPTION_KEY=$(openssl rand -hex 12) # 24 chars, NEVER ROTATE
|
| 53 |
+
CALCOM_WEBHOOK_SECRET=$(openssl rand -hex 32)
|
| 54 |
+
echo "CALCOM_NEXTAUTH_SECRET=${CALCOM_NEXTAUTH_SECRET}"
|
| 55 |
+
echo "CALCOM_ENCRYPTION_KEY=${CALCOM_ENCRYPTION_KEY}"
|
| 56 |
+
echo "CALCOM_WEBHOOK_SECRET=${CALCOM_WEBHOOK_SECRET}"
|
| 57 |
+
echo "# Generate in Cal.com Admin β API Keys:"
|
| 58 |
+
echo "# CALCOM_API_KEY=cal_live_..."
|
| 59 |
+
echo ""
|
| 60 |
+
|
| 61 |
+
# Formbricks (v4 - 5 containers)
|
| 62 |
+
echo "# βββ Formbricks (Surveys & Feedback) ββββββββββββββββββββββ"
|
| 63 |
+
FORMBRICKS_NEXTAUTH_SECRET=$(openssl rand -base64 32)
|
| 64 |
+
FORMBRICKS_ENCRYPTION_KEY=$(openssl rand -hex 32) # NEVER ROTATE
|
| 65 |
+
FORMBRICKS_CRON_SECRET=$(openssl rand -base64 16)
|
| 66 |
+
FORMBRICKS_HUB_API_KEY=$(openssl rand -hex 32)
|
| 67 |
+
FORMBRICKS_CUBE_API_SECRET=$(openssl rand -hex 32)
|
| 68 |
+
FORMBRICKS_WEBHOOK_SECRET=$(openssl rand -hex 32)
|
| 69 |
+
echo "FORMBRICKS_NEXTAUTH_SECRET=${FORMBRICKS_NEXTAUTH_SECRET}"
|
| 70 |
+
echo "FORMBRICKS_ENCRYPTION_KEY=${FORMBRICKS_ENCRYPTION_KEY}"
|
| 71 |
+
echo "FORMBRICKS_CRON_SECRET=${FORMBRICKS_CRON_SECRET}"
|
| 72 |
+
echo "FORMBRICKS_HUB_API_KEY=${FORMBRICKS_HUB_API_KEY}"
|
| 73 |
+
echo "FORMBRICKS_CUBE_API_SECRET=${FORMBRICKS_CUBE_API_SECRET}"
|
| 74 |
+
echo "FORMBRICKS_WEBHOOK_SECRET=${FORMBRICKS_WEBHOOK_SECRET}"
|
| 75 |
+
echo "# Generate in Formbricks UI after setup:"
|
| 76 |
+
echo "# NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=<from Settings β Environments>"
|
| 77 |
+
echo "# FORMBRICKS_API_KEY=<from Settings β API Keys>"
|
| 78 |
+
echo ""
|
| 79 |
+
|
| 80 |
+
# Listmonk
|
| 81 |
+
echo "# βββ Listmonk (Email Marketing) βββββββββββββββββββββββββββ"
|
| 82 |
+
LISTMONK_ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-24)
|
| 83 |
+
echo "LISTMONK_ADMIN_PASSWORD=${LISTMONK_ADMIN_PASSWORD}"
|
| 84 |
+
echo "# Generate in Listmonk Admin β Settings β API:"
|
| 85 |
+
echo "# LISTMONK_API_TOKEN=<from API Keys page>"
|
| 86 |
+
echo "# Create a list in Listmonk Admin β Lists β Copy UUID:"
|
| 87 |
+
echo "# NEXT_PUBLIC_LISTMONK_LIST_UUID=<list-uuid>"
|
| 88 |
+
echo ""
|
| 89 |
+
|
| 90 |
+
# Documenso (if used)
|
| 91 |
+
echo "# βββ Documenso (E-Signatures) βββββββββββββββββββββββββββββ"
|
| 92 |
+
DOCUMENSO_NEXTAUTH_SECRET=$(openssl rand -base64 32)
|
| 93 |
+
DOCUMENSO_ENCRYPTION_KEY=$(openssl rand -hex 32) # NEVER ROTATE
|
| 94 |
+
DOCUMENSO_WEBHOOK_SECRET=$(openssl rand -hex 32)
|
| 95 |
+
echo "DOCUMENSO_NEXTAUTH_SECRET=${DOCUMENSO_NEXTAUTH_SECRET}"
|
| 96 |
+
echo "DOCUMENSO_ENCRYPTION_KEY=${DOCUMENSO_ENCRYPTION_KEY}"
|
| 97 |
+
echo "DOCUMENSO_WEBHOOK_SECRET=${DOCUMENSO_WEBHOOK_SECRET}"
|
| 98 |
+
echo "# Generate in Documenso Admin β Settings β API:"
|
| 99 |
+
echo "# DOCUMENSO_API_KEY=<from API Keys>"
|
| 100 |
+
echo ""
|
| 101 |
+
|
| 102 |
+
# LiveKit
|
| 103 |
+
echo "# βββ LiveKit (Recording Studio) βββββββββββββββββββββββββββ"
|
| 104 |
+
LIVEKIT_API_KEY="devkey"
|
| 105 |
+
LIVEKIT_API_SECRET=$(openssl rand -base64 32 | tr -d '/+=' | cut -c1-32)
|
| 106 |
+
LIVEKIT_WEBHOOK_SECRET=$(openssl rand -hex 32)
|
| 107 |
+
echo "LIVEKIT_API_KEY=${LIVEKIT_API_KEY}"
|
| 108 |
+
echo "LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET}"
|
| 109 |
+
echo "LIVEKIT_WEBHOOK_SECRET=${LIVEKIT_WEBHOOK_SECRET}"
|
| 110 |
+
echo ""
|
| 111 |
+
|
| 112 |
+
# Email (SMTP)
|
| 113 |
+
echo "# βββ Email (SMTP) βββββββββββββββββββββββββββββββββββββββββ"
|
| 114 |
+
echo "# Configure your production SMTP provider:"
|
| 115 |
+
echo "# SMTP_HOST=smtp.sendgrid.net"
|
| 116 |
+
echo "# SMTP_PORT=587"
|
| 117 |
+
echo "# SMTP_USER=apikey"
|
| 118 |
+
echo "# SMTP_PASSWORD=SG.xxx"
|
| 119 |
+
echo "# EMAIL_FROM=MicProtocol <noreply@yourdomain.com>"
|
| 120 |
+
echo ""
|
| 121 |
+
|
| 122 |
+
# Clerk (Auth)
|
| 123 |
+
echo "# βββ Clerk (Authentication) βββββββββββββββββββββββββββββββ"
|
| 124 |
+
echo "# Get from Clerk Dashboard β API Keys:"
|
| 125 |
+
echo "# NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_..."
|
| 126 |
+
echo "# CLERK_SECRET_KEY=sk_live_..."
|
| 127 |
+
echo "# CLERK_WEBHOOK_SECRET=whsec_... (from Clerk β Webhooks)"
|
| 128 |
+
echo ""
|
| 129 |
+
|
| 130 |
+
# Sentry (Optional)
|
| 131 |
+
echo "# βββ Sentry (Error Tracking) ββββββββββββββββββββββββββββββ"
|
| 132 |
+
echo "# SENTRY_DSN=https://xxx@sentry.io/xxx"
|
| 133 |
+
echo "# SENTRY_ORG=your-org"
|
| 134 |
+
echo "# SENTRY_PROJECT=micprotocol"
|
| 135 |
+
echo "# SENTRY_AUTH_TOKEN=xxx (for source maps upload)"
|
| 136 |
+
echo ""
|
| 137 |
+
|
| 138 |
+
# Umami (Analytics)
|
| 139 |
+
echo "# βββ Umami (Analytics) ββββββββββββββββββββββββββββββββββββ"
|
| 140 |
+
UMAMI_APP_SECRET=$(openssl rand -base64 32)
|
| 141 |
+
echo "UMAMI_APP_SECRET=${UMAMI_APP_SECRET}"
|
| 142 |
+
echo "# Generate in Umami UI after setup:"
|
| 143 |
+
echo "# NEXT_PUBLIC_UMAMI_WEBSITE_ID=<website-id>"
|
| 144 |
+
echo ""
|
| 145 |
+
|
| 146 |
+
echo "# ============================================================"
|
| 147 |
+
echo "# End of generated secrets"
|
| 148 |
+
echo "# ============================================================"
|
scripts/init-databases.sql
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
-- MicProtocol - Database initialization
|
| 3 |
+
-- Creates one DB per self-hosted integration. The MicProtocol
|
| 4 |
+
-- API itself uses POSTGRES_DB (mirroring POSTGRES_USER) which is
|
| 5 |
+
-- created automatically by the postgres image; these scripts add
|
| 6 |
+
-- the rest.
|
| 7 |
+
-- βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 8 |
+
|
| 9 |
+
-- Core extension used by MicProtocol itself.
|
| 10 |
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
| 11 |
+
-- pgvector is required by Chatwoot AND Formbricks. The image
|
| 12 |
+
-- pgvector/pgvector:pg16 ships the extension; just enable it.
|
| 13 |
+
CREATE EXTENSION IF NOT EXISTS vector;
|
| 14 |
+
|
| 15 |
+
-- βββ MicProtocol integration DBs (each owns its own schema) β
|
| 16 |
+
CREATE DATABASE chatwoot;
|
| 17 |
+
CREATE DATABASE calcom;
|
| 18 |
+
CREATE DATABASE documenso;
|
| 19 |
+
CREATE DATABASE formbricks;
|
| 20 |
+
CREATE DATABASE listmonk;
|
| 21 |
+
CREATE DATABASE umami;
|
| 22 |
+
|
| 23 |
+
-- βββ Per-DB extensions βββββββββββββββββββββββββββββββββββββ
|
| 24 |
+
-- pgvector has to be enabled inside each database that needs it.
|
| 25 |
+
\c chatwoot
|
| 26 |
+
CREATE EXTENSION IF NOT EXISTS vector;
|
| 27 |
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
| 28 |
+
|
| 29 |
+
\c formbricks
|
| 30 |
+
CREATE EXTENSION IF NOT EXISTS vector;
|
| 31 |
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
| 32 |
+
|
| 33 |
+
\c calcom
|
| 34 |
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
| 35 |
+
|
| 36 |
+
\c documenso
|
| 37 |
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
| 38 |
+
|
| 39 |
+
\c umami
|
| 40 |
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
| 41 |
+
|
| 42 |
+
\c listmonk
|
| 43 |
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
scripts/render-build.sh
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# Render Build Script β MicProtocol Monorepo
|
| 4 |
+
# Called by each Render service with SERVICE_NAME env var.
|
| 5 |
+
# Installs workspace deps from root, then builds the target.
|
| 6 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
+
set -euo pipefail
|
| 8 |
+
|
| 9 |
+
SERVICE="${1:-${SERVICE_NAME:-}}"
|
| 10 |
+
if [ -z "$SERVICE" ]; then
|
| 11 |
+
echo "Usage: SERVICE_NAME=@micprotocol/api bash scripts/render-build.sh"
|
| 12 |
+
echo " or: bash scripts/render-build.sh @micprotocol/api"
|
| 13 |
+
exit 1
|
| 14 |
+
fi
|
| 15 |
+
|
| 16 |
+
echo "β Render build for: $SERVICE"
|
| 17 |
+
echo "β Installing workspace dependencies..."
|
| 18 |
+
|
| 19 |
+
corepack enable
|
| 20 |
+
pnpm install --frozen-lockfile --filter "$SERVICE" --filter "$SERVICE"^...
|
| 21 |
+
|
| 22 |
+
echo "β Building $SERVICE..."
|
| 23 |
+
pnpm --filter "$SERVICE" build
|
| 24 |
+
|
| 25 |
+
echo "β Build complete."
|
scripts/sync-env.sh
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 3 |
+
# sync-env.sh β Ensure .env has every key from .env.example
|
| 4 |
+
#
|
| 5 |
+
# Reads .env.example as the source of truth. For any key that is
|
| 6 |
+
# missing in the local .env, appends the .env.example value (or
|
| 7 |
+
# a placeholder) under the same section comment. Existing keys in
|
| 8 |
+
# .env are preserved (never overwritten) so secrets stay intact.
|
| 9 |
+
#
|
| 10 |
+
# Usage:
|
| 11 |
+
# ./scripts/sync-env.sh # sync into .env
|
| 12 |
+
# ./scripts/sync-env.sh --check # report missing keys only
|
| 13 |
+
# ./scripts/sync-env.sh --target .env.production
|
| 14 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
+
|
| 16 |
+
set -euo pipefail
|
| 17 |
+
|
| 18 |
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
| 19 |
+
EXAMPLE="${REPO_ROOT}/.env.example"
|
| 20 |
+
COOLIFY="${REPO_ROOT}/.env.example.coolify"
|
| 21 |
+
|
| 22 |
+
target=".env"
|
| 23 |
+
mode="sync"
|
| 24 |
+
for arg in "$@"; do
|
| 25 |
+
case "$arg" in
|
| 26 |
+
--check) mode="check" ;;
|
| 27 |
+
--target) shift; target="${1:-.env}" ;;
|
| 28 |
+
*) ;;
|
| 29 |
+
esac
|
| 30 |
+
done
|
| 31 |
+
TARGET="${REPO_ROOT}/${target}"
|
| 32 |
+
|
| 33 |
+
if [[ ! -f "$EXAMPLE" ]]; then
|
| 34 |
+
echo "ERROR: $EXAMPLE not found" >&2
|
| 35 |
+
exit 1
|
| 36 |
+
fi
|
| 37 |
+
if [[ ! -f "$TARGET" ]]; then
|
| 38 |
+
echo "Creating $TARGET from $EXAMPLE template"
|
| 39 |
+
cp "$EXAMPLE" "$TARGET"
|
| 40 |
+
exit 0
|
| 41 |
+
fi
|
| 42 |
+
|
| 43 |
+
# Extract existing keys (handle values that span lines)
|
| 44 |
+
declare -A existing
|
| 45 |
+
while IFS= read -r line; do
|
| 46 |
+
# Skip comments / blanks
|
| 47 |
+
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
| 48 |
+
[[ -z "$line" ]] && continue
|
| 49 |
+
if [[ "$line" =~ ^[[:space:]]*([A-Z_][A-Z_0-9]*)= ]]; then
|
| 50 |
+
key="${BASH_REMATCH[1]}"
|
| 51 |
+
existing["$key"]=1
|
| 52 |
+
fi
|
| 53 |
+
done < "$TARGET"
|
| 54 |
+
|
| 55 |
+
missing=()
|
| 56 |
+
total=0
|
| 57 |
+
while IFS= read -r line; do
|
| 58 |
+
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
| 59 |
+
[[ -z "$line" ]] && continue
|
| 60 |
+
if [[ "$line" =~ ^[[:space:]]*([A-Z_][A-Z_0-9]*)= ]]; then
|
| 61 |
+
key="${BASH_REMATCH[1]}"
|
| 62 |
+
total=$((total + 1))
|
| 63 |
+
if [[ -z "${existing[$key]:-}" ]]; then
|
| 64 |
+
missing+=("$line")
|
| 65 |
+
fi
|
| 66 |
+
fi
|
| 67 |
+
done < "$EXAMPLE"
|
| 68 |
+
|
| 69 |
+
if [[ "$mode" == "check" ]]; then
|
| 70 |
+
echo "Checked $target against .env.example"
|
| 71 |
+
echo " Total keys in .env.example: $total"
|
| 72 |
+
echo " Already present in $target: $((total - ${#missing[@]}))"
|
| 73 |
+
echo " Missing keys: ${#missing[@]}"
|
| 74 |
+
if [[ ${#missing[@]} -gt 0 ]]; then
|
| 75 |
+
echo ""
|
| 76 |
+
echo "Missing keys (add these to $target):"
|
| 77 |
+
printf ' %s\n' "${missing[@]}"
|
| 78 |
+
exit 1
|
| 79 |
+
fi
|
| 80 |
+
exit 0
|
| 81 |
+
fi
|
| 82 |
+
|
| 83 |
+
if [[ ${#missing[@]} -eq 0 ]]; then
|
| 84 |
+
echo "β $target already in sync with .env.example ($total keys)"
|
| 85 |
+
exit 0
|
| 86 |
+
fi
|
| 87 |
+
|
| 88 |
+
{
|
| 89 |
+
echo ""
|
| 90 |
+
echo "# βββ Auto-synced from .env.example ($(date -u +%Y-%m-%dT%H:%M:%SZ)) ββ"
|
| 91 |
+
printf '%s\n' "${missing[@]}"
|
| 92 |
+
} >> "$TARGET"
|
| 93 |
+
|
| 94 |
+
echo "β Added ${#missing[@]} missing keys to $target"
|
| 95 |
+
echo " Total keys in $target: $total"
|
scripts/synthetic-agents/agents-manifest.json
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "1.0.0",
|
| 3 |
+
"description": "MicProtocol Synthetic Agent Manifest - Real-world simulation credentials",
|
| 4 |
+
"apiBaseUrl": "http://localhost:4000/trpc",
|
| 5 |
+
"healthEndpoint": "http://localhost:4000/health",
|
| 6 |
+
"devMode": true,
|
| 7 |
+
"agents": [
|
| 8 |
+
{
|
| 9 |
+
"id": "agent-admin-01",
|
| 10 |
+
"name": "Admin Agent",
|
| 11 |
+
"role": "admin",
|
| 12 |
+
"email": "agent.admin@micprotocol.local",
|
| 13 |
+
"userId": "00000000-0000-4000-a000-000000000001",
|
| 14 |
+
"credential": "sk_admin_micprotocol_v1_agent_admin_01",
|
| 15 |
+
"description": "Platform admin - monitors all workspaces, manages global settings, billing oversight",
|
| 16 |
+
"workspaces": []
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"id": "agent-workspace-morning",
|
| 20 |
+
"name": "Morning Brew Podcast Workspace",
|
| 21 |
+
"role": "owner",
|
| 22 |
+
"email": "agent.morning@micprotocol.local",
|
| 23 |
+
"userId": "00000000-0000-4000-a000-000000000010",
|
| 24 |
+
"credential": "sk_ws_morning_brew_v1_agent_01",
|
| 25 |
+
"description": "Daily business/tech podcast - morning show format, high-volume guest pipeline",
|
| 26 |
+
"workspaces": [
|
| 27 |
+
{
|
| 28 |
+
"id": "22222222-0000-4000-a000-000000000010",
|
| 29 |
+
"slug": "morning-brew-podcast",
|
| 30 |
+
"name": "Morning Brew Podcast",
|
| 31 |
+
"podcasts": [
|
| 32 |
+
{
|
| 33 |
+
"id": "33333333-0000-4000-a000-000000000011",
|
| 34 |
+
"title": "Morning Brew Daily",
|
| 35 |
+
"defaultPlatform": "riverside",
|
| 36 |
+
"preferredDurationMinutes": 30,
|
| 37 |
+
"guests": 15,
|
| 38 |
+
"episodes": 8
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": "33333333-0000-4000-a000-000000000012",
|
| 42 |
+
"title": "Morning Brew Deep Dive",
|
| 43 |
+
"defaultPlatform": "zoom",
|
| 44 |
+
"preferredDurationMinutes": 60,
|
| 45 |
+
"guests": 8,
|
| 46 |
+
"episodes": 4
|
| 47 |
+
}
|
| 48 |
+
]
|
| 49 |
+
}
|
| 50 |
+
]
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"id": "agent-workspace-devops",
|
| 54 |
+
"name": "DevOps Thursday Workspace",
|
| 55 |
+
"role": "owner",
|
| 56 |
+
"email": "agent.devops@micprotocol.local",
|
| 57 |
+
"userId": "00000000-0000-4000-a000-000000000020",
|
| 58 |
+
"credential": "sk_ws_devops_thurs_v1_agent_02",
|
| 59 |
+
"description": "Technical podcast for infrastructure engineers - weekly deep dives",
|
| 60 |
+
"workspaces": [
|
| 61 |
+
{
|
| 62 |
+
"id": "22222222-0000-4000-a000-000000000020",
|
| 63 |
+
"slug": "devops-thursday",
|
| 64 |
+
"name": "DevOps Thursday",
|
| 65 |
+
"podcasts": [
|
| 66 |
+
{
|
| 67 |
+
"id": "33333333-0000-4000-a000-000000000021",
|
| 68 |
+
"title": "DevOps Thursday",
|
| 69 |
+
"defaultPlatform": "zoom",
|
| 70 |
+
"preferredDurationMinutes": 45,
|
| 71 |
+
"guests": 20,
|
| 72 |
+
"episodes": 12
|
| 73 |
+
}
|
| 74 |
+
]
|
| 75 |
+
}
|
| 76 |
+
]
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"id": "agent-workspace-creator",
|
| 80 |
+
"name": "Creator Economics Workspace",
|
| 81 |
+
"role": "owner",
|
| 82 |
+
"email": "agent.creator@micprotocol.local",
|
| 83 |
+
"userId": "00000000-0000-4000-a000-000000000030",
|
| 84 |
+
"credential": "sk_ws_creator_econ_v1_agent_03",
|
| 85 |
+
"description": "Creator economy podcast - monetization, platform strategy, audience building",
|
| 86 |
+
"workspaces": [
|
| 87 |
+
{
|
| 88 |
+
"id": "22222222-0000-4000-a000-000000000030",
|
| 89 |
+
"slug": "creator-economics",
|
| 90 |
+
"name": "Creator Economics",
|
| 91 |
+
"podcasts": [
|
| 92 |
+
{
|
| 93 |
+
"id": "33333333-0000-4000-a000-000000000031",
|
| 94 |
+
"title": "Creator Economics Show",
|
| 95 |
+
"defaultPlatform": "riverside",
|
| 96 |
+
"preferredDurationMinutes": 50,
|
| 97 |
+
"guests": 12,
|
| 98 |
+
"episodes": 6
|
| 99 |
+
}
|
| 100 |
+
]
|
| 101 |
+
}
|
| 102 |
+
]
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
"id": "agent-workspace-climate",
|
| 106 |
+
"name": "Climate Frontiers Workspace",
|
| 107 |
+
"role": "owner",
|
| 108 |
+
"email": "agent.climate@micprotocol.local",
|
| 109 |
+
"userId": "00000000-0000-4000-a000-000000000040",
|
| 110 |
+
"credential": "sk_ws_climate_front_v1_agent_04",
|
| 111 |
+
"description": "Climate tech and sustainability podcast - founder stories and policy deep dives",
|
| 112 |
+
"workspaces": [
|
| 113 |
+
{
|
| 114 |
+
"id": "22222222-0000-4000-a000-000000000040",
|
| 115 |
+
"slug": "climate-frontiers",
|
| 116 |
+
"name": "Climate Frontiers",
|
| 117 |
+
"podcasts": [
|
| 118 |
+
{
|
| 119 |
+
"id": "33333333-0000-4000-a000-000000000041",
|
| 120 |
+
"title": "Climate Frontiers Podcast",
|
| 121 |
+
"defaultPlatform": "zoom",
|
| 122 |
+
"preferredDurationMinutes": 40,
|
| 123 |
+
"guests": 10,
|
| 124 |
+
"episodes": 5
|
| 125 |
+
}
|
| 126 |
+
]
|
| 127 |
+
}
|
| 128 |
+
]
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"id": "agent-workspace-security",
|
| 132 |
+
"name": "Zero Trust Podcast Workspace",
|
| 133 |
+
"role": "admin",
|
| 134 |
+
"email": "agent.security@micprotocol.local",
|
| 135 |
+
"userId": "00000000-0000-4000-a000-000000000050",
|
| 136 |
+
"credential": "sk_ws_zero_trust_v1_agent_05",
|
| 137 |
+
"description": "Cybersecurity podcast network - multiple shows on infosec topics",
|
| 138 |
+
"workspaces": [
|
| 139 |
+
{
|
| 140 |
+
"id": "22222222-0000-4000-a000-000000000050",
|
| 141 |
+
"slug": "zero-trust-podcast",
|
| 142 |
+
"name": "Zero Trust Security",
|
| 143 |
+
"podcasts": [
|
| 144 |
+
{
|
| 145 |
+
"id": "33333333-0000-4000-a000-000000000051",
|
| 146 |
+
"title": "Zero Trust Podcast",
|
| 147 |
+
"defaultPlatform": "riverside",
|
| 148 |
+
"preferredDurationMinutes": 55,
|
| 149 |
+
"guests": 18,
|
| 150 |
+
"episodes": 9
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"id": "33333333-0000-4000-a000-000000000052",
|
| 154 |
+
"title": "Red Team Diaries",
|
| 155 |
+
"defaultPlatform": "zoom",
|
| 156 |
+
"preferredDurationMinutes": 35,
|
| 157 |
+
"guests": 6,
|
| 158 |
+
"episodes": 3
|
| 159 |
+
}
|
| 160 |
+
]
|
| 161 |
+
}
|
| 162 |
+
]
|
| 163 |
+
}
|
| 164 |
+
],
|
| 165 |
+
"guestPool": [
|
| 166 |
+
{
|
| 167 |
+
"id": "guest-ai-01",
|
| 168 |
+
"email": "guest.aiml@micprotocol.local",
|
| 169 |
+
"fullName": "Dr. Sara Velez",
|
| 170 |
+
"expertise": ["Machine Learning", "NLP", "Responsible AI"],
|
| 171 |
+
"tier": "gold",
|
| 172 |
+
"verificationStatus": "featured"
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"id": "guest-devops-01",
|
| 176 |
+
"email": "guest.k8s@micprotocol.local",
|
| 177 |
+
"fullName": "Raj Patel",
|
| 178 |
+
"expertise": ["Kubernetes", "Cloud Native", "SRE"],
|
| 179 |
+
"tier": "silver",
|
| 180 |
+
"verificationStatus": "verified"
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"id": "guest-devops-02",
|
| 184 |
+
"email": "guest.sre@micprotocol.local",
|
| 185 |
+
"fullName": "Lena Schmidt",
|
| 186 |
+
"expertise": ["SRE", "Observability", "Incident Response"],
|
| 187 |
+
"tier": "gold",
|
| 188 |
+
"verificationStatus": "verified"
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"id": "guest-creator-01",
|
| 192 |
+
"email": "guest.monetize@micprotocol.local",
|
| 193 |
+
"fullName": "Carlos Mendez",
|
| 194 |
+
"expertise": ["Creator Economy", "Memberships", "Newsletters"],
|
| 195 |
+
"tier": "gold",
|
| 196 |
+
"verificationStatus": "featured"
|
| 197 |
+
},
|
| 198 |
+
{
|
| 199 |
+
"id": "guest-creator-02",
|
| 200 |
+
"email": "guest.video@micprotocol.local",
|
| 201 |
+
"fullName": "Yuki Tanaka",
|
| 202 |
+
"expertise": ["Video Strategy", "YouTube", "Short Form"],
|
| 203 |
+
"tier": "silver",
|
| 204 |
+
"verificationStatus": "verified"
|
| 205 |
+
},
|
| 206 |
+
{
|
| 207 |
+
"id": "guest-climate-01",
|
| 208 |
+
"email": "guest.carbon@micprotocol.local",
|
| 209 |
+
"fullName": "Dr. Amara Obi",
|
| 210 |
+
"expertise": ["Carbon Removal", "Climate Policy", "Clean Tech"],
|
| 211 |
+
"tier": "gold",
|
| 212 |
+
"verificationStatus": "featured"
|
| 213 |
+
},
|
| 214 |
+
{
|
| 215 |
+
"id": "guest-climate-02",
|
| 216 |
+
"email": "guest.energy@micprotocol.local",
|
| 217 |
+
"fullName": "Tom Eriksson",
|
| 218 |
+
"expertise": ["Renewable Energy", "Grid Storage", "Hydrogen"],
|
| 219 |
+
"tier": "silver",
|
| 220 |
+
"verificationStatus": "verified"
|
| 221 |
+
},
|
| 222 |
+
{
|
| 223 |
+
"id": "guest-security-01",
|
| 224 |
+
"email": "guest.threat@micprotocol.local",
|
| 225 |
+
"fullName": "Maya Johansson",
|
| 226 |
+
"expertise": ["Threat Intelligence", "Zero Trust", "Cloud Security"],
|
| 227 |
+
"tier": "gold",
|
| 228 |
+
"verificationStatus": "featured"
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
"id": "guest-security-02",
|
| 232 |
+
"email": "guest.appsec@micprotocol.local",
|
| 233 |
+
"fullName": "David Kim",
|
| 234 |
+
"expertise": ["AppSec", "DevSecOps", "Supply Chain Security"],
|
| 235 |
+
"tier": "silver",
|
| 236 |
+
"verificationStatus": "verified"
|
| 237 |
+
},
|
| 238 |
+
{
|
| 239 |
+
"id": "guest-business-01",
|
| 240 |
+
"email": "guest.saas@micprotocol.local",
|
| 241 |
+
"fullName": "Emily Westerfeld",
|
| 242 |
+
"expertise": ["B2B SaaS", "GTM Strategy", "Venture Capital"],
|
| 243 |
+
"tier": "silver",
|
| 244 |
+
"verificationStatus": "verified"
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
"id": "guest-business-02",
|
| 248 |
+
"email": "guest.people@micprotocol.local",
|
| 249 |
+
"fullName": "James Okonkwo",
|
| 250 |
+
"expertise": ["People Ops", "Remote Work", "Culture"],
|
| 251 |
+
"tier": "bronze",
|
| 252 |
+
"verificationStatus": "basic"
|
| 253 |
+
}
|
| 254 |
+
],
|
| 255 |
+
"testScenarios": [
|
| 256 |
+
{
|
| 257 |
+
"id": "sc-workspace-lifecycle",
|
| 258 |
+
"name": "Workspace Lifecycle",
|
| 259 |
+
"description": "CRUD operations on workspaces, member management, settings",
|
| 260 |
+
"agents": ["agent-workspace-morning", "agent-workspace-devops"]
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"id": "sc-podcast-operations",
|
| 264 |
+
"name": "Podcast Operations",
|
| 265 |
+
"description": "Full podcast CRUD, recording settings, outreach defaults",
|
| 266 |
+
"agents": ["agent-workspace-creator", "agent-workspace-climate"]
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"id": "sc-guest-pipeline",
|
| 270 |
+
"name": "Guest Pipeline Management",
|
| 271 |
+
"description": "Guest CRUD, status transitions, tagging, bulk operations",
|
| 272 |
+
"agents": ["agent-workspace-security", "agent-workspace-morning"]
|
| 273 |
+
},
|
| 274 |
+
{
|
| 275 |
+
"id": "sc-outreach-sequences",
|
| 276 |
+
"name": "Outreach Sequences",
|
| 277 |
+
"description": "Create sequences, add messages from templates, track opens/clicks",
|
| 278 |
+
"agents": ["agent-workspace-morning", "agent-workspace-creator"]
|
| 279 |
+
},
|
| 280 |
+
{
|
| 281 |
+
"id": "sc-booking-recording",
|
| 282 |
+
"name": "Booking & Recording",
|
| 283 |
+
"description": "Episode scheduling, booking confirmation, post-interview workflows",
|
| 284 |
+
"agents": ["agent-workspace-devops", "agent-workspace-climate"]
|
| 285 |
+
},
|
| 286 |
+
{
|
| 287 |
+
"id": "sc-verified-network",
|
| 288 |
+
"name": "Verified Guest Network",
|
| 289 |
+
"description": "Profile search, bookmarks, booking requests, reviews",
|
| 290 |
+
"agents": ["agent-workspace-security", "agent-workspace-creator"]
|
| 291 |
+
},
|
| 292 |
+
{
|
| 293 |
+
"id": "sc-billing-subscription",
|
| 294 |
+
"name": "Billing & Subscription",
|
| 295 |
+
"description": "Plan management, subscription upgrades, feature access checks",
|
| 296 |
+
"agents": ["agent-admin-01"]
|
| 297 |
+
},
|
| 298 |
+
{
|
| 299 |
+
"id": "sc-admin-operations",
|
| 300 |
+
"name": "Admin Operations",
|
| 301 |
+
"description": "System-wide operations, user management, security settings",
|
| 302 |
+
"agents": ["agent-admin-01"]
|
| 303 |
+
},
|
| 304 |
+
{
|
| 305 |
+
"id": "sc-service-administration",
|
| 306 |
+
"name": "Service Administration",
|
| 307 |
+
"description": "Clip-scorer lock/scratch and mic-realtime stats/traces access, admin-only",
|
| 308 |
+
"agents": ["agent-admin-01"]
|
| 309 |
+
},
|
| 310 |
+
{
|
| 311 |
+
"id": "sc-cross-workspace",
|
| 312 |
+
"name": "Cross-Workspace Collaboration",
|
| 313 |
+
"description": "Network operations between different workspace agents",
|
| 314 |
+
"agents": [
|
| 315 |
+
"agent-workspace-morning",
|
| 316 |
+
"agent-workspace-devops",
|
| 317 |
+
"agent-workspace-creator",
|
| 318 |
+
"agent-workspace-climate",
|
| 319 |
+
"agent-workspace-security"
|
| 320 |
+
]
|
| 321 |
+
},
|
| 322 |
+
{
|
| 323 |
+
"id": "sc-support",
|
| 324 |
+
"name": "Customer Support",
|
| 325 |
+
"description": "Ticket creation, message exchange, status changes, CSAT rating, SLA breaches, categories, cross-workspace isolation",
|
| 326 |
+
"agents": ["agent-workspace-morning", "agent-workspace-security"]
|
| 327 |
+
},
|
| 328 |
+
{
|
| 329 |
+
"id": "sc-calcom",
|
| 330 |
+
"name": "Cal.com Scheduling",
|
| 331 |
+
"description": "List Cal.com event types, create booking, cross-workspace isolation",
|
| 332 |
+
"agents": ["agent-workspace-morning", "agent-workspace-devops"]
|
| 333 |
+
},
|
| 334 |
+
{
|
| 335 |
+
"id": "sc-documents",
|
| 336 |
+
"name": "Document Management",
|
| 337 |
+
"description": "Upload document, create share, verify integrity, access logs, cross-workspace isolation",
|
| 338 |
+
"agents": ["agent-workspace-morning"]
|
| 339 |
+
},
|
| 340 |
+
{
|
| 341 |
+
"id": "sc-security-policy",
|
| 342 |
+
"name": "Security Policy",
|
| 343 |
+
"description": "Sanction screening, blocklist management, flag/revoke workflows",
|
| 344 |
+
"agents": ["agent-admin-01"]
|
| 345 |
+
}
|
| 346 |
+
]
|
| 347 |
+
}
|
scripts/synthetic-agents/lib/reporter.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface TestResult {
|
| 2 |
+
scenarioId: string;
|
| 3 |
+
scenarioName: string;
|
| 4 |
+
agentId: string;
|
| 5 |
+
agentName: string;
|
| 6 |
+
operation: string;
|
| 7 |
+
status: "pass" | "fail" | "warn" | "error";
|
| 8 |
+
durationMs: number;
|
| 9 |
+
error?: string;
|
| 10 |
+
details?: Record<string, unknown>;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export interface WeaknessReport {
|
| 14 |
+
severity: "critical" | "high" | "medium" | "low" | "info";
|
| 15 |
+
category: "auth" | "validation" | "performance" | "business_logic" | "security" | "reliability" | "ux";
|
| 16 |
+
title: string;
|
| 17 |
+
description: string;
|
| 18 |
+
affectedEndpoints: string[];
|
| 19 |
+
evidence: string;
|
| 20 |
+
recommendation: string;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export class ResultsReporter {
|
| 24 |
+
private results: TestResult[] = [];
|
| 25 |
+
private weaknesses: WeaknessReport[] = [];
|
| 26 |
+
private startTime = Date.now();
|
| 27 |
+
|
| 28 |
+
record(result: TestResult): void {
|
| 29 |
+
this.results.push(result);
|
| 30 |
+
this.analyzeWeakness(result);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
private analyzeWeakness(result: TestResult): void {
|
| 34 |
+
if (result.status === "fail" || result.status === "error") {
|
| 35 |
+
const errorMsg = result.error || "";
|
| 36 |
+
const op = result.operation;
|
| 37 |
+
|
| 38 |
+
// Authorization gaps
|
| 39 |
+
if (errorMsg.includes("UNAUTHORIZED") && op.includes("without")) {
|
| 40 |
+
this.weaknesses.push({
|
| 41 |
+
severity: "critical",
|
| 42 |
+
category: "auth",
|
| 43 |
+
title: "Insufficient auth guard coverage",
|
| 44 |
+
description: `Operation '${op}' exposed without proper auth: ${errorMsg}`,
|
| 45 |
+
affectedEndpoints: [op],
|
| 46 |
+
evidence: errorMsg,
|
| 47 |
+
recommendation: "Ensure all workspace-scoped procedures enforce authentication and workspace membership checks.",
|
| 48 |
+
});
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Input validation
|
| 52 |
+
if (errorMsg.includes("ZodError") || errorMsg.includes("validation") || errorMsg.includes("BAD_REQUEST")) {
|
| 53 |
+
this.weaknesses.push({
|
| 54 |
+
severity: "high",
|
| 55 |
+
category: "validation",
|
| 56 |
+
title: "Insufficient input validation",
|
| 57 |
+
description: `Operation '${op}' accepted invalid input: ${errorMsg}`,
|
| 58 |
+
affectedEndpoints: [op],
|
| 59 |
+
evidence: errorMsg,
|
| 60 |
+
recommendation: "Strengthen Zod schema validation with proper type guards, length limits, and format checks.",
|
| 61 |
+
});
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// Rate limiting
|
| 65 |
+
if (errorMsg.includes("429") || errorMsg.includes("TOO_MANY_REQUESTS") || errorMsg.includes("rate limit")) {
|
| 66 |
+
this.weaknesses.push({
|
| 67 |
+
severity: "medium",
|
| 68 |
+
category: "performance",
|
| 69 |
+
title: "Rate limiting triggered",
|
| 70 |
+
description: `Operation '${op}' hit rate limit. Evaluate if limit is too restrictive or burst handling needed.`,
|
| 71 |
+
affectedEndpoints: [op],
|
| 72 |
+
evidence: errorMsg,
|
| 73 |
+
recommendation: "Implement per-user rate limiting with burst allowance and exponential backoff hints.",
|
| 74 |
+
});
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Subscription/feature gating
|
| 78 |
+
if (errorMsg.includes("subscription") || errorMsg.includes("UPGRADE") || errorMsg.includes("plan")) {
|
| 79 |
+
this.weaknesses.push({
|
| 80 |
+
severity: "medium",
|
| 81 |
+
category: "business_logic",
|
| 82 |
+
title: "Subscription gating inconsistency",
|
| 83 |
+
description: `Subscription check failed for '${op}': ${errorMsg}`,
|
| 84 |
+
affectedEndpoints: [op],
|
| 85 |
+
evidence: errorMsg,
|
| 86 |
+
recommendation: "Audit subscription feature gating to ensure plan tiers are consistently enforced across all procedures.",
|
| 87 |
+
});
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// Forbidden access (potential privilege escalation)
|
| 91 |
+
if (errorMsg.includes("FORBIDDEN") || errorMsg.includes("access")) {
|
| 92 |
+
this.weaknesses.push({
|
| 93 |
+
severity: "high",
|
| 94 |
+
category: "security",
|
| 95 |
+
title: "Access control verification needed",
|
| 96 |
+
description: `Access denied for '${op}': ${errorMsg}`,
|
| 97 |
+
affectedEndpoints: [op],
|
| 98 |
+
evidence: errorMsg,
|
| 99 |
+
recommendation: "Verify that cross-workspace access is properly forbidden and role-based permissions are enforced.",
|
| 100 |
+
});
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// Performance weakness detection
|
| 105 |
+
if (result.durationMs > 5000) {
|
| 106 |
+
this.weaknesses.push({
|
| 107 |
+
severity: "low",
|
| 108 |
+
category: "performance",
|
| 109 |
+
title: "Slow operation detected",
|
| 110 |
+
description: `Operation '${result.operation}' took ${result.durationMs}ms (threshold: 5000ms)`,
|
| 111 |
+
affectedEndpoints: [result.operation],
|
| 112 |
+
evidence: `${result.durationMs}ms`,
|
| 113 |
+
recommendation: "Consider adding database indexes, query optimization, or caching for slow operations.",
|
| 114 |
+
});
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
getResults(): TestResult[] {
|
| 119 |
+
return this.results;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
getWeaknesses(): WeaknessReport[] {
|
| 123 |
+
return this.weaknesses;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
generateReport(): string {
|
| 127 |
+
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
|
| 128 |
+
const total = this.results.length;
|
| 129 |
+
const passed = this.results.filter((r) => r.status === "pass").length;
|
| 130 |
+
const failed = this.results.filter((r) => r.status === "fail" || r.status === "error").length;
|
| 131 |
+
const warned = this.results.filter((r) => r.status === "warn").length;
|
| 132 |
+
const passRate = total > 0 ? ((passed / total) * 100).toFixed(1) : "0.0";
|
| 133 |
+
|
| 134 |
+
const report: string[] = [
|
| 135 |
+
"βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ",
|
| 136 |
+
"β MicProtocol - Synthetic Agent Test Report β",
|
| 137 |
+
"βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ",
|
| 138 |
+
"",
|
| 139 |
+
`Duration: ${elapsed}s`,
|
| 140 |
+
`Total operations: ${total}`,
|
| 141 |
+
`Passed: ${passed} Failed: ${failed} Warnings: ${warned}`,
|
| 142 |
+
`Pass rate: ${passRate}%`,
|
| 143 |
+
"",
|
| 144 |
+
];
|
| 145 |
+
|
| 146 |
+
if (failed > 0) {
|
| 147 |
+
report.push("ββ Failed Operations ββ");
|
| 148 |
+
for (const r of this.results.filter((r) => r.status === "fail" || r.status === "error")) {
|
| 149 |
+
report.push(` β [${r.agentId}] ${r.operation} (${r.durationMs}ms)`);
|
| 150 |
+
if (r.error) report.push(` Error: ${r.error.slice(0, 200)}`);
|
| 151 |
+
}
|
| 152 |
+
report.push("");
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
if (this.weaknesses.length > 0) {
|
| 156 |
+
report.push("ββ Weaknesses & Vulnerabilities Detected ββ");
|
| 157 |
+
const bySeverity = (sev: string) =>
|
| 158 |
+
this.weaknesses.filter((w) => w.severity === sev);
|
| 159 |
+
|
| 160 |
+
for (const w of bySeverity("critical")) {
|
| 161 |
+
report.push(` π΄ CRITICAL: ${w.title}`);
|
| 162 |
+
report.push(` ${w.description}`);
|
| 163 |
+
report.push(` β ${w.recommendation}`);
|
| 164 |
+
}
|
| 165 |
+
for (const w of bySeverity("high")) {
|
| 166 |
+
report.push(` π HIGH: ${w.title}`);
|
| 167 |
+
report.push(` ${w.description}`);
|
| 168 |
+
report.push(` β ${w.recommendation}`);
|
| 169 |
+
}
|
| 170 |
+
for (const w of bySeverity("medium")) {
|
| 171 |
+
report.push(` π‘ MEDIUM: ${w.title}`);
|
| 172 |
+
report.push(` ${w.description}`);
|
| 173 |
+
}
|
| 174 |
+
for (const w of bySeverity("low")) {
|
| 175 |
+
report.push(` π΅ LOW: ${w.title}`);
|
| 176 |
+
}
|
| 177 |
+
for (const w of bySeverity("info")) {
|
| 178 |
+
report.push(` β INFO: ${w.title}`);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
report.push("");
|
| 182 |
+
report.push("ββ Categorized Recommendations ββ");
|
| 183 |
+
|
| 184 |
+
const categories = [...new Set(this.weaknesses.map((w) => w.category))];
|
| 185 |
+
for (const cat of categories) {
|
| 186 |
+
const items = this.weaknesses.filter((w) => w.category === cat);
|
| 187 |
+
report.push(` π ${cat.toUpperCase()}: ${items.length} issue(s)`);
|
| 188 |
+
for (const item of items) {
|
| 189 |
+
report.push(` β’ ${item.recommendation}`);
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
} else {
|
| 193 |
+
report.push(" β
No weaknesses detected.");
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
return report.join("\n");
|
| 197 |
+
}
|
| 198 |
+
}
|
scripts/synthetic-agents/lib/scenario-runner.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { AgentTrpcClient } from "./trpc-client";
|
| 2 |
+
import type { TestResult, WeaknessReport } from "./reporter";
|
| 3 |
+
import { ResultsReporter } from "./reporter";
|
| 4 |
+
|
| 5 |
+
export interface AgentContext {
|
| 6 |
+
agentId: string;
|
| 7 |
+
agentName: string;
|
| 8 |
+
email: string;
|
| 9 |
+
userId: string;
|
| 10 |
+
credential: string;
|
| 11 |
+
client: AgentTrpcClient;
|
| 12 |
+
workspaceId?: string;
|
| 13 |
+
workspaceSlug?: string;
|
| 14 |
+
podcastIds: string[];
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export type ScenarioFn = (ctx: AgentContext, report: ResultsReporter, allAgents: Map<string, AgentContext>) => Promise<void>;
|
| 18 |
+
|
| 19 |
+
export interface ScenarioDefinition {
|
| 20 |
+
id: string;
|
| 21 |
+
name: string;
|
| 22 |
+
description: string;
|
| 23 |
+
requiredAgents: string[];
|
| 24 |
+
fn: ScenarioFn;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export function measureOperation<T>(
|
| 28 |
+
agentId: string,
|
| 29 |
+
agentName: string,
|
| 30 |
+
scenarioId: string,
|
| 31 |
+
scenarioName: string,
|
| 32 |
+
operation: string,
|
| 33 |
+
fn: () => Promise<T>,
|
| 34 |
+
report: ResultsReporter,
|
| 35 |
+
): Promise<T> {
|
| 36 |
+
const start = Date.now();
|
| 37 |
+
return fn()
|
| 38 |
+
.then((result) => {
|
| 39 |
+
report.record({
|
| 40 |
+
scenarioId,
|
| 41 |
+
scenarioName,
|
| 42 |
+
agentId,
|
| 43 |
+
agentName,
|
| 44 |
+
operation,
|
| 45 |
+
status: "pass",
|
| 46 |
+
durationMs: Date.now() - start,
|
| 47 |
+
});
|
| 48 |
+
return result;
|
| 49 |
+
})
|
| 50 |
+
.catch((err: Error) => {
|
| 51 |
+
report.record({
|
| 52 |
+
scenarioId,
|
| 53 |
+
scenarioName,
|
| 54 |
+
agentId,
|
| 55 |
+
agentName,
|
| 56 |
+
operation,
|
| 57 |
+
status: "fail",
|
| 58 |
+
durationMs: Date.now() - start,
|
| 59 |
+
error: err.message,
|
| 60 |
+
});
|
| 61 |
+
throw err;
|
| 62 |
+
});
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
export function measureOperationWarn<T>(
|
| 66 |
+
agentId: string,
|
| 67 |
+
agentName: string,
|
| 68 |
+
scenarioId: string,
|
| 69 |
+
scenarioName: string,
|
| 70 |
+
operation: string,
|
| 71 |
+
fn: () => Promise<T>,
|
| 72 |
+
report: ResultsReporter,
|
| 73 |
+
): Promise<T | null> {
|
| 74 |
+
const start = Date.now();
|
| 75 |
+
return fn()
|
| 76 |
+
.then((result) => {
|
| 77 |
+
report.record({
|
| 78 |
+
scenarioId,
|
| 79 |
+
scenarioName,
|
| 80 |
+
agentId,
|
| 81 |
+
agentName,
|
| 82 |
+
operation,
|
| 83 |
+
status: "pass",
|
| 84 |
+
durationMs: Date.now() - start,
|
| 85 |
+
});
|
| 86 |
+
return result;
|
| 87 |
+
})
|
| 88 |
+
.catch((err: Error) => {
|
| 89 |
+
report.record({
|
| 90 |
+
scenarioId,
|
| 91 |
+
scenarioName,
|
| 92 |
+
agentId,
|
| 93 |
+
agentName,
|
| 94 |
+
operation,
|
| 95 |
+
status: "warn",
|
| 96 |
+
durationMs: Date.now() - start,
|
| 97 |
+
error: err.message,
|
| 98 |
+
});
|
| 99 |
+
return null;
|
| 100 |
+
});
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
export function runScenario(
|
| 104 |
+
def: ScenarioDefinition,
|
| 105 |
+
agents: Map<string, AgentContext>,
|
| 106 |
+
report: ResultsReporter,
|
| 107 |
+
): Promise<void> {
|
| 108 |
+
const agentContexts = def.requiredAgents
|
| 109 |
+
.map((id) => agents.get(id))
|
| 110 |
+
.filter((ctx): ctx is AgentContext => ctx !== undefined);
|
| 111 |
+
|
| 112 |
+
if (agentContexts.length < def.requiredAgents.length) {
|
| 113 |
+
const missing = def.requiredAgents.filter((id) => !agents.has(id));
|
| 114 |
+
console.warn(` β οΈ Scenario '${def.name}' skipped: missing agents [${missing.join(", ")}]`);
|
| 115 |
+
return Promise.resolve();
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// Run for the first matching agent, passing all agents for multi-agent scenarios
|
| 119 |
+
return def.fn(agentContexts[0], report, agents).catch((err) => {
|
| 120 |
+
console.error(` β Scenario '${def.name}' crashed: ${err.message}`);
|
| 121 |
+
});
|
| 122 |
+
}
|
scripts/synthetic-agents/lib/trpc-client.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
| 2 |
+
|
| 3 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 4 |
+
type AnyRouter = any;
|
| 5 |
+
export type AgentTrpcClient = ReturnType<typeof createTRPCClient<AnyRouter>>;
|
| 6 |
+
|
| 7 |
+
export interface TrpcClientOptions {
|
| 8 |
+
baseUrl: string;
|
| 9 |
+
userId: string;
|
| 10 |
+
credential: string;
|
| 11 |
+
workspaceId?: string;
|
| 12 |
+
devMode?: boolean;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export function createAgentClient(opts: TrpcClientOptions): AgentTrpcClient {
|
| 16 |
+
const headers: Record<string, string> = {};
|
| 17 |
+
|
| 18 |
+
if (opts.devMode) {
|
| 19 |
+
headers["x-user-id"] = opts.userId;
|
| 20 |
+
headers["x-credential"] = opts.credential;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
if (opts.workspaceId) {
|
| 24 |
+
headers["x-workspace-id"] = opts.workspaceId;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
return createTRPCClient<AnyRouter>({
|
| 28 |
+
links: [
|
| 29 |
+
httpBatchLink({
|
| 30 |
+
url: `${opts.baseUrl}`,
|
| 31 |
+
headers,
|
| 32 |
+
maxURLLength: 2048,
|
| 33 |
+
fetch: async (url, options) => {
|
| 34 |
+
const response = await fetch(url, {
|
| 35 |
+
...options,
|
| 36 |
+
signal: AbortSignal.timeout(30000),
|
| 37 |
+
});
|
| 38 |
+
if (!response.ok) {
|
| 39 |
+
const body = await response.text().catch(() => "");
|
| 40 |
+
throw new Error(`tRPC request failed: ${response.status} ${response.statusText}\n${body}`);
|
| 41 |
+
}
|
| 42 |
+
return response;
|
| 43 |
+
},
|
| 44 |
+
}),
|
| 45 |
+
],
|
| 46 |
+
});
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
export function withWorkspace(client: AgentTrpcClient, workspaceId: string): AgentTrpcClient {
|
| 50 |
+
return createTRPCClient<AnyRouter>({
|
| 51 |
+
links: [
|
| 52 |
+
httpBatchLink({
|
| 53 |
+
url: "http://localhost:4000/trpc",
|
| 54 |
+
headers: {
|
| 55 |
+
"x-user-id": "",
|
| 56 |
+
"x-workspace-id": workspaceId,
|
| 57 |
+
},
|
| 58 |
+
maxURLLength: 2048,
|
| 59 |
+
}),
|
| 60 |
+
],
|
| 61 |
+
});
|
| 62 |
+
}
|
scripts/synthetic-agents/orchestrator.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createAgentClient, type AgentTrpcClient } from "./lib/trpc-client";
|
| 2 |
+
import { ResultsReporter } from "./lib/reporter";
|
| 3 |
+
import { type AgentContext, type ScenarioDefinition } from "./lib/scenario-runner";
|
| 4 |
+
import { runScenario } from "./lib/scenario-runner";
|
| 5 |
+
import { allScenarios } from "./scenarios/index";
|
| 6 |
+
import * as fs from "fs";
|
| 7 |
+
import * as path from "path";
|
| 8 |
+
|
| 9 |
+
interface Manifest {
|
| 10 |
+
apiBaseUrl: string;
|
| 11 |
+
healthEndpoint: string;
|
| 12 |
+
devMode: boolean;
|
| 13 |
+
agents: ManifestAgent[];
|
| 14 |
+
testScenarios: ManifestScenario[];
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
interface ManifestAgent {
|
| 18 |
+
id: string;
|
| 19 |
+
name: string;
|
| 20 |
+
role: string;
|
| 21 |
+
email: string;
|
| 22 |
+
userId: string;
|
| 23 |
+
credential: string;
|
| 24 |
+
workspaces: ManifestWorkspace[];
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
interface ManifestWorkspace {
|
| 28 |
+
id: string;
|
| 29 |
+
slug: string;
|
| 30 |
+
name: string;
|
| 31 |
+
podcasts: ManifestPodcast[];
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
interface ManifestPodcast {
|
| 35 |
+
id: string;
|
| 36 |
+
title: string;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
interface ManifestScenario {
|
| 40 |
+
id: string;
|
| 41 |
+
name: string;
|
| 42 |
+
description: string;
|
| 43 |
+
agents: string[];
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
async function checkHealth(endpoint: string): Promise<boolean> {
|
| 47 |
+
try {
|
| 48 |
+
const res = await fetch(endpoint, { signal: AbortSignal.timeout(5000) });
|
| 49 |
+
return res.ok;
|
| 50 |
+
} catch {
|
| 51 |
+
return false;
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function loadManifest(): Manifest {
|
| 56 |
+
const manifestPath = path.join(__dirname, "agents-manifest.json");
|
| 57 |
+
return JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function buildAgentContexts(manifest: Manifest): Map<string, AgentContext> {
|
| 61 |
+
const agents = new Map<string, AgentContext>();
|
| 62 |
+
|
| 63 |
+
for (const agent of manifest.agents) {
|
| 64 |
+
const podcastIds: string[] = [];
|
| 65 |
+
let workspaceId: string | undefined;
|
| 66 |
+
let workspaceSlug: string | undefined;
|
| 67 |
+
|
| 68 |
+
if (agent.workspaces.length > 0) {
|
| 69 |
+
workspaceId = agent.workspaces[0].id;
|
| 70 |
+
workspaceSlug = agent.workspaces[0].slug;
|
| 71 |
+
for (const ws of agent.workspaces) {
|
| 72 |
+
for (const podcast of ws.podcasts) {
|
| 73 |
+
podcastIds.push(podcast.id);
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
const client = createAgentClient({
|
| 79 |
+
baseUrl: manifest.apiBaseUrl,
|
| 80 |
+
userId: agent.userId,
|
| 81 |
+
credential: agent.credential,
|
| 82 |
+
workspaceId,
|
| 83 |
+
devMode: manifest.devMode,
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
agents.set(agent.id, {
|
| 87 |
+
agentId: agent.id,
|
| 88 |
+
agentName: agent.name,
|
| 89 |
+
email: agent.email,
|
| 90 |
+
userId: agent.userId,
|
| 91 |
+
credential: agent.credential,
|
| 92 |
+
client,
|
| 93 |
+
workspaceId,
|
| 94 |
+
workspaceSlug,
|
| 95 |
+
podcastIds,
|
| 96 |
+
});
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
return agents;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
async function main() {
|
| 103 |
+
console.log("βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ");
|
| 104 |
+
console.log("β MicProtocol - Synthetic Agent Orchestrator β");
|
| 105 |
+
console.log("βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ");
|
| 106 |
+
console.log("");
|
| 107 |
+
|
| 108 |
+
const manifest = loadManifest();
|
| 109 |
+
|
| 110 |
+
// 1. Health check
|
| 111 |
+
console.log(`π Checking API health at ${manifest.healthEndpoint}...`);
|
| 112 |
+
const healthy = await checkHealth(manifest.healthEndpoint);
|
| 113 |
+
if (!healthy) {
|
| 114 |
+
console.error("β API is not healthy. Start the pod first:\n");
|
| 115 |
+
console.error(" cd scripts/synthetic-agents");
|
| 116 |
+
console.error(" podman-compose -f podman-compose.yml up -d");
|
| 117 |
+
console.error(" pnpm db:push");
|
| 118 |
+
console.error(" pnpm db:seed");
|
| 119 |
+
process.exit(1);
|
| 120 |
+
}
|
| 121 |
+
console.log("β
API is healthy\n");
|
| 122 |
+
|
| 123 |
+
// 2. Build agent contexts
|
| 124 |
+
console.log("π€ Building agent contexts...");
|
| 125 |
+
const agents = buildAgentContexts(manifest);
|
| 126 |
+
console.log(` ${agents.size} agents initialized\n`);
|
| 127 |
+
|
| 128 |
+
for (const [, agent] of agents) {
|
| 129 |
+
console.log(` π€ ${agent.agentName} (${agent.agentId})`);
|
| 130 |
+
console.log(` Email: ${agent.email}`);
|
| 131 |
+
console.log(` Workspace: ${agent.workspaceSlug || "N/A"}`);
|
| 132 |
+
if (agent.podcastIds.length > 0) {
|
| 133 |
+
console.log(` Podcasts: ${agent.podcastIds.join(", ")}`);
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
console.log("");
|
| 137 |
+
|
| 138 |
+
// 3. Map scenarios
|
| 139 |
+
const scenarioMap = new Map<string, ScenarioDefinition>();
|
| 140 |
+
for (const sc of allScenarios) {
|
| 141 |
+
scenarioMap.set(sc.id, sc);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// 4. Run scenarios
|
| 145 |
+
const reporter = new ResultsReporter();
|
| 146 |
+
const enabledScenarioIds = new Set(manifest.testScenarios.map((s) => s.id));
|
| 147 |
+
|
| 148 |
+
console.log("π¬ Running test scenarios...\n");
|
| 149 |
+
|
| 150 |
+
for (const manifestScenario of manifest.testScenarios) {
|
| 151 |
+
const scenario = scenarioMap.get(manifestScenario.id);
|
| 152 |
+
if (!scenario) {
|
| 153 |
+
console.warn(` β οΈ Scenario '${manifestScenario.id}' not found, skipping`);
|
| 154 |
+
continue;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
console.log(`ββ ${scenario.name} ββ`);
|
| 158 |
+
console.log(` ${scenario.description}`);
|
| 159 |
+
console.log(` Agents: ${manifestScenario.agents.join(", ")}`);
|
| 160 |
+
|
| 161 |
+
try {
|
| 162 |
+
await runScenario(scenario, agents, reporter);
|
| 163 |
+
console.log(` β
Completed\n`);
|
| 164 |
+
} catch (err) {
|
| 165 |
+
console.error(` β Failed: ${(err as Error).message}\n`);
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
// 5. Generate report
|
| 170 |
+
console.log("βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ");
|
| 171 |
+
console.log("β FINAL TEST REPORT β");
|
| 172 |
+
console.log("βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ");
|
| 173 |
+
console.log("");
|
| 174 |
+
console.log(reporter.generateReport());
|
| 175 |
+
|
| 176 |
+
// 6. Write report to file
|
| 177 |
+
const reportPath = path.join(__dirname, "test-report.txt");
|
| 178 |
+
fs.writeFileSync(reportPath, reporter.generateReport());
|
| 179 |
+
console.log(`\nπ Full report written to: ${reportPath}`);
|
| 180 |
+
|
| 181 |
+
// 7. Write weaknesses to structured JSON
|
| 182 |
+
const weaknessesPath = path.join(__dirname, "weaknesses.json");
|
| 183 |
+
fs.writeFileSync(weaknessesPath, JSON.stringify(reporter.getWeaknesses(), null, 2));
|
| 184 |
+
console.log(`π Weaknesses JSON written to: ${weaknessesPath}`);
|
| 185 |
+
|
| 186 |
+
// 8. Exit with appropriate code
|
| 187 |
+
const failed = reporter.getResults().filter((r) => r.status === "fail" || r.status === "error").length;
|
| 188 |
+
process.exit(failed > 0 ? 1 : 0);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
main().catch((err) => {
|
| 192 |
+
console.error("Orchestrator crashed:", err);
|
| 193 |
+
process.exit(1);
|
| 194 |
+
});
|
scripts/synthetic-agents/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@micprotocol/synthetic-agents",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"start": "bash run-agents.sh",
|
| 7 |
+
"orchestrator": "ts-node orchestrator.ts"
|
| 8 |
+
},
|
| 9 |
+
"dependencies": {
|
| 10 |
+
"@trpc/client": "^11.11.0",
|
| 11 |
+
"zod": "^4.3.6"
|
| 12 |
+
},
|
| 13 |
+
"devDependencies": {
|
| 14 |
+
"ts-node": "^10.9.2",
|
| 15 |
+
"typescript": "^5.9.3",
|
| 16 |
+
"@types/node": "^25.3.3"
|
| 17 |
+
}
|
| 18 |
+
}
|
scripts/synthetic-agents/podman-compose.yml
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
# MicProtocol - Podman Pod Orchestration
|
| 3 |
+
# Podman-compatible alternative to docker-compose.yml
|
| 4 |
+
# Uses a shared pod for infra isolation
|
| 5 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 6 |
+
|
| 7 |
+
version: "3.8"
|
| 8 |
+
|
| 9 |
+
x-pod: &pod
|
| 10 |
+
pod: micprotocol-pod
|
| 11 |
+
pod_options:
|
| 12 |
+
--infra-conmon
|
| 13 |
+
--network bridge
|
| 14 |
+
--publish 5432:5432
|
| 15 |
+
--publish 6379:6379
|
| 16 |
+
--publish 7700:7700
|
| 17 |
+
--publish 4000:4000
|
| 18 |
+
--publish 3000:3000
|
| 19 |
+
--publish 3001:3001
|
| 20 |
+
--publish 3002:3002
|
| 21 |
+
--publish 3003:3003
|
| 22 |
+
--publish 9000:9000
|
| 23 |
+
--publish 9001:9001
|
| 24 |
+
--publish 9090:9090
|
| 25 |
+
|
| 26 |
+
services:
|
| 27 |
+
# βββ PostgreSQL Database βββββββββββββββββββββββββββββββββ
|
| 28 |
+
postgres:
|
| 29 |
+
image: docker.io/postgres:15-alpine
|
| 30 |
+
<<: *pod
|
| 31 |
+
container_name: micprotocol-pg
|
| 32 |
+
restart: unless-stopped
|
| 33 |
+
environment:
|
| 34 |
+
POSTGRES_USER: ${POSTGRES_USER:-micprotocol}
|
| 35 |
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-micprotocol_secret}
|
| 36 |
+
POSTGRES_DB: ${POSTGRES_DB:-micprotocol}
|
| 37 |
+
volumes:
|
| 38 |
+
- pg_data:/var/lib/postgresql/data
|
| 39 |
+
healthcheck:
|
| 40 |
+
test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-micprotocol}" ]
|
| 41 |
+
interval: 10s
|
| 42 |
+
timeout: 5s
|
| 43 |
+
retries: 5
|
| 44 |
+
|
| 45 |
+
# βββ Redis (Caching & Job Queue) βββββββββββββββββββββββββ
|
| 46 |
+
redis:
|
| 47 |
+
image: docker.io/redis:7-alpine
|
| 48 |
+
<<: *pod
|
| 49 |
+
container_name: micprotocol-redis
|
| 50 |
+
restart: unless-stopped
|
| 51 |
+
command: redis-server --requirepass ${REDIS_PASSWORD:-redis_secret}
|
| 52 |
+
volumes:
|
| 53 |
+
- redis_data:/data
|
| 54 |
+
healthcheck:
|
| 55 |
+
test: [ "CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-redis_secret}", "ping" ]
|
| 56 |
+
interval: 10s
|
| 57 |
+
timeout: 3s
|
| 58 |
+
retries: 5
|
| 59 |
+
|
| 60 |
+
# βββ Meilisearch (Search Engine) βββββββββββββββββββββββββ
|
| 61 |
+
meilisearch:
|
| 62 |
+
image: docker.io/getmeili/meilisearch:v1.6
|
| 63 |
+
<<: *pod
|
| 64 |
+
container_name: micprotocol-meili
|
| 65 |
+
restart: unless-stopped
|
| 66 |
+
environment:
|
| 67 |
+
MEILI_MASTER_KEY: ${MEILISEARCH_API_KEY:-masterKey}
|
| 68 |
+
MEILI_NO_ANALYTICS: "true"
|
| 69 |
+
volumes:
|
| 70 |
+
- meili_data:/meili_data
|
| 71 |
+
healthcheck:
|
| 72 |
+
test: [ "CMD", "curl", "-f", "http://localhost:7700/health" ]
|
| 73 |
+
interval: 30s
|
| 74 |
+
timeout: 10s
|
| 75 |
+
retries: 3
|
| 76 |
+
|
| 77 |
+
# βββ API Server ββββββββββββββββββββββββββββββββββββββββββ
|
| 78 |
+
api:
|
| 79 |
+
build:
|
| 80 |
+
context: ../..
|
| 81 |
+
dockerfile: apps/api/Dockerfile
|
| 82 |
+
<<: *pod
|
| 83 |
+
container_name: micprotocol-api
|
| 84 |
+
restart: unless-stopped
|
| 85 |
+
environment:
|
| 86 |
+
NODE_ENV: development
|
| 87 |
+
API_PORT: 4000
|
| 88 |
+
DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/${POSTGRES_DB:-micprotocol}?schema=public
|
| 89 |
+
REDIS_HOST: redis
|
| 90 |
+
REDIS_PORT: 6379
|
| 91 |
+
REDIS_PASSWORD: ${REDIS_PASSWORD:-redis_secret}
|
| 92 |
+
MEILISEARCH_HOST: http://meilisearch:7700
|
| 93 |
+
CLERK_SECRET_KEY: ${CLERK_SECRET_KEY:-sk_test_placeholder}
|
| 94 |
+
CLERK_PUBLISHABLE_KEY: ${CLERK_PUBLISHABLE_KEY:-pk_test_placeholder}
|
| 95 |
+
WEB_URL: ${WEB_URL:-http://localhost:3000}
|
| 96 |
+
API_URL: ${API_URL:-http://localhost:4000}
|
| 97 |
+
COOKIE_SECRET: ${COOKIE_SECRET:-cookie_secret_for_testing}
|
| 98 |
+
ENABLE_AI_FEATURES: "false"
|
| 99 |
+
ENABLE_CONTENT_STUDIO: "true"
|
| 100 |
+
ENABLE_ANALYTICS: "true"
|
| 101 |
+
depends_on:
|
| 102 |
+
postgres:
|
| 103 |
+
condition: service_healthy
|
| 104 |
+
redis:
|
| 105 |
+
condition: service_healthy
|
| 106 |
+
healthcheck:
|
| 107 |
+
test: [ "CMD", "wget", "-q", "--spider", "http://localhost:4000/health" ]
|
| 108 |
+
interval: 30s
|
| 109 |
+
timeout: 10s
|
| 110 |
+
retries: 3
|
| 111 |
+
start_period: 40s
|
| 112 |
+
|
| 113 |
+
# βββ Background Workers ββββββββββββββββββββββββββββββββββ
|
| 114 |
+
worker:
|
| 115 |
+
build:
|
| 116 |
+
context: ../..
|
| 117 |
+
dockerfile: apps/api/Dockerfile
|
| 118 |
+
<<: *pod
|
| 119 |
+
container_name: micprotocol-worker
|
| 120 |
+
restart: unless-stopped
|
| 121 |
+
command: npx bullmq-pro
|
| 122 |
+
environment:
|
| 123 |
+
NODE_ENV: development
|
| 124 |
+
DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/${POSTGRES_DB:-micprotocol}?schema=public
|
| 125 |
+
REDIS_HOST: redis
|
| 126 |
+
REDIS_PORT: 6379
|
| 127 |
+
REDIS_PASSWORD: ${REDIS_PASSWORD:-redis_secret}
|
| 128 |
+
depends_on:
|
| 129 |
+
postgres:
|
| 130 |
+
condition: service_healthy
|
| 131 |
+
redis:
|
| 132 |
+
condition: service_healthy
|
| 133 |
+
|
| 134 |
+
calcom:
|
| 135 |
+
image: docker.io/calcom/cal.com:latest
|
| 136 |
+
<<: *pod
|
| 137 |
+
container_name: micprotocol-calcom
|
| 138 |
+
restart: unless-stopped
|
| 139 |
+
environment:
|
| 140 |
+
DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/${POSTGRES_DB:-micprotocol}?schema=calcom
|
| 141 |
+
NEXTAUTH_SECRET: ${CALCOM_NEXTAUTH_SECRET:-calcom_secret}
|
| 142 |
+
CALENDSO_ENCRYPTION_KEY: ${CALCOM_ENCRYPTION_KEY:-calcom_encryption}
|
| 143 |
+
NEXT_PUBLIC_WEBAPP_URL: ${CALCOM_URL:-http://localhost:3002}
|
| 144 |
+
NEXTAUTH_URL: ${CALCOM_URL:-http://localhost:3002}
|
| 145 |
+
depends_on:
|
| 146 |
+
postgres:
|
| 147 |
+
condition: service_healthy
|
| 148 |
+
|
| 149 |
+
listmonk:
|
| 150 |
+
image: docker.io/listmonk/listmonk:latest
|
| 151 |
+
<<: *pod
|
| 152 |
+
container_name: micprotocol-listmonk
|
| 153 |
+
restart: unless-stopped
|
| 154 |
+
environment:
|
| 155 |
+
LISTMONK_app__address: "0.0.0.0:9000"
|
| 156 |
+
LISTMONK_db__host: postgres
|
| 157 |
+
LISTMONK_db__port: 5432
|
| 158 |
+
LISTMONK_db__user: ${POSTGRES_USER:-micprotocol}
|
| 159 |
+
LISTMONK_db__password: ${POSTGRES_PASSWORD:-micprotocol_secret}
|
| 160 |
+
LISTMONK_db__database: ${POSTGRES_DB:-micprotocol}
|
| 161 |
+
LISTMONK_db__ssl_mode: disable
|
| 162 |
+
command: [ "sh", "-c", "./listmonk --install --yes && ./listmonk" ]
|
| 163 |
+
depends_on:
|
| 164 |
+
postgres:
|
| 165 |
+
condition: service_healthy
|
| 166 |
+
|
| 167 |
+
formbricks:
|
| 168 |
+
image: docker.io/formbricks/formbricks:latest
|
| 169 |
+
<<: *pod
|
| 170 |
+
container_name: micprotocol-formbricks
|
| 171 |
+
restart: unless-stopped
|
| 172 |
+
environment:
|
| 173 |
+
DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/${POSTGRES_DB:-micprotocol}?schema=formbricks
|
| 174 |
+
NEXTAUTH_URL: ${FORMBRICKS_URL:-http://localhost:3001}
|
| 175 |
+
NEXTAUTH_SECRET: ${FORMBRICKS_NEXTAUTH_SECRET:-formbricks_secret}
|
| 176 |
+
ENCRYPTION_KEY: ${FORMBRICKS_ENCRYPTION_KEY:-e12c4ab1239847123987123984723984}
|
| 177 |
+
depends_on:
|
| 178 |
+
postgres:
|
| 179 |
+
condition: service_healthy
|
| 180 |
+
|
| 181 |
+
minio:
|
| 182 |
+
image: docker.io/minio/minio:latest
|
| 183 |
+
<<: *pod
|
| 184 |
+
container_name: micprotocol-minio
|
| 185 |
+
restart: unless-stopped
|
| 186 |
+
environment:
|
| 187 |
+
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-micprotocol_admin}
|
| 188 |
+
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-micprotocol_secret}
|
| 189 |
+
command: server /data --console-address ":9090"
|
| 190 |
+
volumes:
|
| 191 |
+
- minio_data:/data
|
| 192 |
+
|
| 193 |
+
minio-createbuckets:
|
| 194 |
+
image: docker.io/minio/mc
|
| 195 |
+
<<: *pod
|
| 196 |
+
container_name: micprotocol-minio-mc
|
| 197 |
+
depends_on:
|
| 198 |
+
- minio
|
| 199 |
+
entrypoint: >
|
| 200 |
+
/bin/sh -c "
|
| 201 |
+
/usr/bin/mc alias set myminio http://minio:9000 ${MINIO_ROOT_USER:-micprotocol_admin} ${MINIO_ROOT_PASSWORD:-micprotocol_secret};
|
| 202 |
+
/usr/bin/mc mb myminio/micprotocol-storage --ignore-existing;
|
| 203 |
+
/usr/bin/mc anonymous set public myminio/micprotocol-storage;
|
| 204 |
+
exit 0;
|
| 205 |
+
"
|
| 206 |
+
|
| 207 |
+
umami:
|
| 208 |
+
image: ghcr.io/umami-software/umami:postgresql-latest
|
| 209 |
+
<<: *pod
|
| 210 |
+
container_name: micprotocol-umami
|
| 211 |
+
restart: unless-stopped
|
| 212 |
+
environment:
|
| 213 |
+
DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/${POSTGRES_DB:-micprotocol}?schema=umami
|
| 214 |
+
DATABASE_TYPE: postgresql
|
| 215 |
+
APP_SECRET: ${UMAMI_APP_SECRET:-umami_secret_key_12345}
|
| 216 |
+
depends_on:
|
| 217 |
+
postgres:
|
| 218 |
+
condition: service_healthy
|
| 219 |
+
|
| 220 |
+
# βββ Volumes βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 221 |
+
volumes:
|
| 222 |
+
pg_data:
|
| 223 |
+
driver: local
|
| 224 |
+
redis_data:
|
| 225 |
+
driver: local
|
| 226 |
+
meili_data:
|
| 227 |
+
driver: local
|
| 228 |
+
minio_data:
|
| 229 |
+
driver: local
|
scripts/synthetic-agents/run-agents.sh
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 5 |
+
# MicProtocol - Synthetic Agent Runner
|
| 6 |
+
# Orchestrates: podman pod + seed + agent simulation
|
| 7 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 8 |
+
|
| 9 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 10 |
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
| 11 |
+
API_DIR="$REPO_ROOT/apps/api"
|
| 12 |
+
|
| 13 |
+
RED='\033[0;31m'
|
| 14 |
+
GREEN='\033[0;32m'
|
| 15 |
+
YELLOW='\033[1;33m'
|
| 16 |
+
CYAN='\033[0;36m'
|
| 17 |
+
NC='\033[0m' # No Color
|
| 18 |
+
|
| 19 |
+
log() { echo -e "${GREEN}[β]${NC} $1"; }
|
| 20 |
+
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
| 21 |
+
err() { echo -e "${RED}[β]${NC} $1"; }
|
| 22 |
+
info() { echo -e "${CYAN}[β]${NC} $1"; }
|
| 23 |
+
|
| 24 |
+
cleanup() {
|
| 25 |
+
info "Cleaning up..."
|
| 26 |
+
cd "$SCRIPT_DIR" && podman-compose -f podman-compose.yml down 2>/dev/null || true
|
| 27 |
+
}
|
| 28 |
+
trap cleanup EXIT ERR
|
| 29 |
+
|
| 30 |
+
echo ""
|
| 31 |
+
echo "βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
|
| 32 |
+
echo "β MicProtocol - Synthetic Agent Test Runner β"
|
| 33 |
+
echo "βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
|
| 34 |
+
echo ""
|
| 35 |
+
|
| 36 |
+
# βββ Phase 1: Check prerequisites ββββββββββββββββββββββββββββ
|
| 37 |
+
info "Phase 1: Checking prerequisites..."
|
| 38 |
+
|
| 39 |
+
command -v podman >/dev/null 2>&1 || { err "podman is required"; exit 1; }
|
| 40 |
+
command -v podman-compose >/dev/null 2>&1 || { err "podman-compose is required (pip install podman-compose)"; exit 1; }
|
| 41 |
+
command -v pnpm >/dev/null 2>&1 || { err "pnpm is required"; exit 1; }
|
| 42 |
+
|
| 43 |
+
log "All prerequisites satisfied"
|
| 44 |
+
|
| 45 |
+
# βββ Phase 2: Start podman pod βββββββββββββββββββββββββββββββ
|
| 46 |
+
info "Phase 2: Starting podman pod with infra services..."
|
| 47 |
+
|
| 48 |
+
cd "$SCRIPT_DIR"
|
| 49 |
+
|
| 50 |
+
# Load env vars from repo root .env if present
|
| 51 |
+
if [ -f "$REPO_ROOT/.env" ]; then
|
| 52 |
+
export $(grep -v '^\s*#' "$REPO_ROOT/.env" | grep -v '^\s*$' | xargs)
|
| 53 |
+
fi
|
| 54 |
+
|
| 55 |
+
export NODE_ENV=development
|
| 56 |
+
|
| 57 |
+
podman-compose -f podman-compose.yml up -d postgres redis meilisearch
|
| 58 |
+
log "Podman infra services started"
|
| 59 |
+
|
| 60 |
+
# Wait for services
|
| 61 |
+
info "Waiting for Postgres..."
|
| 62 |
+
until podman exec micprotocol-pg pg_isready -U micprotocol >/dev/null 2>&1; do
|
| 63 |
+
sleep 2
|
| 64 |
+
done
|
| 65 |
+
log "Postgres is ready"
|
| 66 |
+
|
| 67 |
+
info "Waiting for Redis..."
|
| 68 |
+
until podman exec micprotocol-redis redis-cli -a "${REDIS_PASSWORD:-redis_secret}" ping >/dev/null 2>&1; do
|
| 69 |
+
sleep 1
|
| 70 |
+
done
|
| 71 |
+
log "Redis is ready"
|
| 72 |
+
|
| 73 |
+
info "Waiting for Meilisearch..."
|
| 74 |
+
until curl -s http://localhost:7700/health >/dev/null 2>&1; do
|
| 75 |
+
sleep 1
|
| 76 |
+
done
|
| 77 |
+
log "Meilisearch is ready"
|
| 78 |
+
|
| 79 |
+
# βββ Phase 3: Start API ββββββββββββββββββββββββββββββββββββββ
|
| 80 |
+
info "Phase 3: Starting API server..."
|
| 81 |
+
|
| 82 |
+
cd "$API_DIR"
|
| 83 |
+
|
| 84 |
+
# Generate Prisma client
|
| 85 |
+
pnpm prisma:generate
|
| 86 |
+
log "Prisma client generated"
|
| 87 |
+
|
| 88 |
+
# Push schema
|
| 89 |
+
DATABASE_URL="postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@localhost:5432/${POSTGRES_DB:-micprotocol}?schema=public" \
|
| 90 |
+
pnpm prisma:push --skip-generate
|
| 91 |
+
log "Database schema pushed"
|
| 92 |
+
|
| 93 |
+
# Seed the database with agent data
|
| 94 |
+
info "Seeding agent data..."
|
| 95 |
+
DATABASE_URL="postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@localhost:5432/${POSTGRES_DB:-micprotocol}?schema=public" \
|
| 96 |
+
npx ts-node "$SCRIPT_DIR/../../apps/api/prisma/seed-agents.ts"
|
| 97 |
+
log "Agent seed data loaded"
|
| 98 |
+
|
| 99 |
+
# Start the API in background
|
| 100 |
+
cd "$REPO_ROOT"
|
| 101 |
+
pnpm --filter @micprotocol/api start:dev &
|
| 102 |
+
API_PID=$!
|
| 103 |
+
log "API server starting (PID: $API_PID)..."
|
| 104 |
+
|
| 105 |
+
# Wait for API health
|
| 106 |
+
info "Waiting for API at http://localhost:4000/health..."
|
| 107 |
+
for i in $(seq 1 60); do
|
| 108 |
+
if curl -s http://localhost:4000/health >/dev/null 2>&1; then
|
| 109 |
+
log "API is healthy"
|
| 110 |
+
break
|
| 111 |
+
fi
|
| 112 |
+
if [ "$i" -eq 60 ]; then
|
| 113 |
+
err "API failed to start within 60s"
|
| 114 |
+
exit 1
|
| 115 |
+
fi
|
| 116 |
+
sleep 2
|
| 117 |
+
done
|
| 118 |
+
|
| 119 |
+
# βββ Phase 4: Run agent orchestrator βββββββββββββββββββββββββ
|
| 120 |
+
info "Phase 4: Running synthetic agent scenarios..."
|
| 121 |
+
|
| 122 |
+
cd "$SCRIPT_DIR"
|
| 123 |
+
|
| 124 |
+
# Install dependencies for the orchestrator
|
| 125 |
+
pnpm install --no-frozen-lockfile 2>/dev/null || true
|
| 126 |
+
|
| 127 |
+
# Use the local ts-node from pnpm
|
| 128 |
+
./node_modules/.bin/ts-node orchestrator.ts 2>&1
|
| 129 |
+
ORCHESTRATOR_EXIT=$?
|
| 130 |
+
|
| 131 |
+
# βββ Phase 5: Summary ββββββββββββββββββββββββββββββββββββββββ
|
| 132 |
+
echo ""
|
| 133 |
+
if [ $ORCHESTRATOR_EXIT -eq 0 ]; then
|
| 134 |
+
log "All agent scenarios completed successfully!"
|
| 135 |
+
else
|
| 136 |
+
warn "Some agent scenarios detected issues. Check test-report.txt"
|
| 137 |
+
fi
|
| 138 |
+
|
| 139 |
+
echo ""
|
| 140 |
+
info "Test report: $SCRIPT_DIR/test-report.txt"
|
| 141 |
+
info "Weaknesses: $SCRIPT_DIR/weaknesses.json"
|
| 142 |
+
|
| 143 |
+
# Kill API
|
| 144 |
+
kill $API_PID 2>/dev/null || true
|
| 145 |
+
|
| 146 |
+
exit $ORCHESTRATOR_EXIT
|
scripts/synthetic-agents/scenarios/admin-operations.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScenarioDefinition, AgentContext } from "../lib/scenario-runner";
|
| 2 |
+
import { measureOperation, measureOperationWarn } from "../lib/scenario-runner";
|
| 3 |
+
import type { ResultsReporter } from "../lib/reporter";
|
| 4 |
+
|
| 5 |
+
async function run(ctx: AgentContext, report: ResultsReporter) {
|
| 6 |
+
const sId = "sc-admin-operations";
|
| 7 |
+
const sName = "Admin Operations";
|
| 8 |
+
|
| 9 |
+
console.log(" π‘οΈ Running admin operations...");
|
| 10 |
+
|
| 11 |
+
// Try accessing admin-level network operations that require auth
|
| 12 |
+
const searchResults = await measureOperation(
|
| 13 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 14 |
+
"admin.network.search",
|
| 15 |
+
() => ctx.client.network.search.query({ query: "AI", limit: 20 }),
|
| 16 |
+
report,
|
| 17 |
+
);
|
| 18 |
+
console.log(` π Admin search returned ${searchResults?.length || 0} results`);
|
| 19 |
+
|
| 20 |
+
// Get trending as admin
|
| 21 |
+
await measureOperation(
|
| 22 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 23 |
+
"admin.network.trending",
|
| 24 |
+
() => ctx.client.network.trending.query({ region: "global", limit: 10 }),
|
| 25 |
+
report,
|
| 26 |
+
);
|
| 27 |
+
|
| 28 |
+
// Create a profile (admin creating their own network profile)
|
| 29 |
+
const myProfile = await measureOperationWarn(
|
| 30 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 31 |
+
"admin.network.getMyProfile",
|
| 32 |
+
() => ctx.client.network.getMyProfile.query(),
|
| 33 |
+
report,
|
| 34 |
+
);
|
| 35 |
+
|
| 36 |
+
if (!myProfile) {
|
| 37 |
+
await measureOperationWarn(
|
| 38 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 39 |
+
"admin.network.createProfile",
|
| 40 |
+
() => ctx.client.network.createProfile.mutate({
|
| 41 |
+
isPublic: true,
|
| 42 |
+
vanityUrl: `admin-${Date.now()}`,
|
| 43 |
+
headline: "Platform Administrator",
|
| 44 |
+
fullBio: "System administrator for MicProtocol synthetic testing.",
|
| 45 |
+
country: "Global",
|
| 46 |
+
timezone: "UTC",
|
| 47 |
+
languages: ["English"],
|
| 48 |
+
verificationStatus: "featured" as any,
|
| 49 |
+
tier: "gold" as any,
|
| 50 |
+
}),
|
| 51 |
+
report,
|
| 52 |
+
);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Test billing operations as admin
|
| 56 |
+
await measureOperationWarn(
|
| 57 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 58 |
+
"admin.billing.listPlans",
|
| 59 |
+
() => ctx.client.billing.listPlans.query(),
|
| 60 |
+
report,
|
| 61 |
+
);
|
| 62 |
+
|
| 63 |
+
// Test system-wide profile views
|
| 64 |
+
await measureOperationWarn(
|
| 65 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 66 |
+
"admin.network.getViewStats",
|
| 67 |
+
() => ctx.client.network.getViewStats.query({
|
| 68 |
+
profileId: "44444444-0000-4000-a000-000000000001",
|
| 69 |
+
days: 30,
|
| 70 |
+
}),
|
| 71 |
+
report,
|
| 72 |
+
);
|
| 73 |
+
|
| 74 |
+
// Try non-admin endpoints (these should still work for admin)
|
| 75 |
+
console.log(" β
Admin operations completed");
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
export const adminScenarios: ScenarioDefinition[] = [
|
| 79 |
+
{
|
| 80 |
+
id: "sc-admin-operations",
|
| 81 |
+
name: "Admin Operations",
|
| 82 |
+
description: "System-wide admin operations, user management, security",
|
| 83 |
+
requiredAgents: ["agent-admin-01"],
|
| 84 |
+
fn: run,
|
| 85 |
+
},
|
| 86 |
+
];
|
scripts/synthetic-agents/scenarios/booking-recording.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScenarioDefinition, AgentContext } from "../lib/scenario-runner";
|
| 2 |
+
import { measureOperation, measureOperationWarn } from "../lib/scenario-runner";
|
| 3 |
+
import type { ResultsReporter } from "../lib/reporter";
|
| 4 |
+
|
| 5 |
+
async function run(ctx: AgentContext, report: ResultsReporter) {
|
| 6 |
+
const sId = "sc-booking-recording";
|
| 7 |
+
const sName = "Booking & Recording";
|
| 8 |
+
|
| 9 |
+
for (const podcastId of ctx.podcastIds) {
|
| 10 |
+
// List episodes with filters
|
| 11 |
+
const episodes = await measureOperation(
|
| 12 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 13 |
+
`scheduling.listEpisodes.${podcastId.slice(-8)}`,
|
| 14 |
+
() => ctx.client.scheduling.listEpisodes.query({ podcastId, limit: 20 }),
|
| 15 |
+
report,
|
| 16 |
+
);
|
| 17 |
+
console.log(` πΊ ${episodes.length} episodes in podcast ${podcastId.slice(-8)}`);
|
| 18 |
+
|
| 19 |
+
// Get upcoming episodes
|
| 20 |
+
await measureOperation(
|
| 21 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 22 |
+
`scheduling.upcoming.${podcastId.slice(-8)}`,
|
| 23 |
+
() => ctx.client.scheduling.upcoming.query({ podcastId, days: 30 }),
|
| 24 |
+
report,
|
| 25 |
+
);
|
| 26 |
+
|
| 27 |
+
// Get individual episode details
|
| 28 |
+
if (episodes.length > 0) {
|
| 29 |
+
for (const ep of episodes.slice(0, 4)) {
|
| 30 |
+
await measureOperation(
|
| 31 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 32 |
+
`scheduling.getEpisode.${ep.id.slice(-8)}`,
|
| 33 |
+
() => ctx.client.scheduling.getEpisode.query({ id: ep.id }),
|
| 34 |
+
report,
|
| 35 |
+
);
|
| 36 |
+
|
| 37 |
+
// Try post-interview workflow for recorded/published episodes
|
| 38 |
+
if (ep.status === "recorded" || ep.status === "published") {
|
| 39 |
+
const workflow = await measureOperationWarn(
|
| 40 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 41 |
+
`scheduling.getPostInterviewWorkflow.${ep.id.slice(-8)}`,
|
| 42 |
+
() => ctx.client.scheduling.getPostInterviewWorkflow.query({ episodeId: ep.id }),
|
| 43 |
+
report,
|
| 44 |
+
);
|
| 45 |
+
|
| 46 |
+
// Initialize workflow if not exists
|
| 47 |
+
if (!workflow) {
|
| 48 |
+
await measureOperationWarn(
|
| 49 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 50 |
+
`scheduling.initializePostInterviewWorkflow.${ep.id.slice(-8)}`,
|
| 51 |
+
() => ctx.client.scheduling.initializePostInterviewWorkflow.mutate({
|
| 52 |
+
episodeId: ep.id,
|
| 53 |
+
tasks: [
|
| 54 |
+
{ taskType: "send_recording_link", status: "pending" as const },
|
| 55 |
+
{ taskType: "request_testimonial", status: "pending" as const },
|
| 56 |
+
{ taskType: "provide_clips", status: "pending" as const },
|
| 57 |
+
],
|
| 58 |
+
}),
|
| 59 |
+
report,
|
| 60 |
+
);
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Try creating a new episode with invalid data
|
| 67 |
+
await measureOperationWarn(
|
| 68 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 69 |
+
"scheduling.createEpisode.invalid",
|
| 70 |
+
() => ctx.client.scheduling.createEpisode.mutate({
|
| 71 |
+
podcastId,
|
| 72 |
+
guestId: "invalid-id",
|
| 73 |
+
title: "",
|
| 74 |
+
status: "draft" as any,
|
| 75 |
+
}),
|
| 76 |
+
report,
|
| 77 |
+
);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Try accessing calendar integration
|
| 81 |
+
for (const podcastId of ctx.podcastIds) {
|
| 82 |
+
await measureOperationWarn(
|
| 83 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 84 |
+
`scheduling.getCalendarIntegration.${podcastId.slice(-8)}`,
|
| 85 |
+
() => ctx.client.scheduling.getCalendarIntegration.query({ podcastId }),
|
| 86 |
+
report,
|
| 87 |
+
);
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
export const bookingRecordingScenarios: ScenarioDefinition[] = [
|
| 92 |
+
{
|
| 93 |
+
id: "sc-booking-recording",
|
| 94 |
+
name: "Booking & Recording",
|
| 95 |
+
description: "Episode scheduling, booking, post-interview workflows",
|
| 96 |
+
requiredAgents: ["agent-workspace-devops", "agent-workspace-climate"],
|
| 97 |
+
fn: run,
|
| 98 |
+
},
|
| 99 |
+
];
|
scripts/synthetic-agents/scenarios/calcom-scenario.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScenarioDefinition, AgentContext } from "../lib/scenario-runner";
|
| 2 |
+
import { measureOperation, measureOperationWarn } from "../lib/scenario-runner";
|
| 3 |
+
import type { ResultsReporter } from "../lib/reporter";
|
| 4 |
+
|
| 5 |
+
async function run(ctx: AgentContext, report: ResultsReporter, allAgents: Map<string, AgentContext>) {
|
| 6 |
+
const sId = "sc-calcom";
|
| 7 |
+
const sName = "Cal.com Scheduling";
|
| 8 |
+
const workspaceId = ctx.workspaceId;
|
| 9 |
+
|
| 10 |
+
if (!workspaceId) {
|
| 11 |
+
console.warn(` β οΈ Scenario '${sName}' skipped: no workspace context for agent ${ctx.agentId}`);
|
| 12 |
+
return;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
// ββ 1. List Cal.com event types ββ
|
| 16 |
+
let eventTypes: unknown[] = [];
|
| 17 |
+
await measureOperation(
|
| 18 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 19 |
+
`calcom.listEventTypes.${workspaceId.slice(-8)}`,
|
| 20 |
+
async () => {
|
| 21 |
+
const result = await ctx.client.integrations.listCalComEventTypes.query({ workspaceId });
|
| 22 |
+
eventTypes = Array.isArray(result) ? result : [];
|
| 23 |
+
return result;
|
| 24 |
+
},
|
| 25 |
+
report,
|
| 26 |
+
);
|
| 27 |
+
console.log(` π Found ${eventTypes.length} Cal.com event types`);
|
| 28 |
+
|
| 29 |
+
// ββ 2. Create a Cal.com booking ββ
|
| 30 |
+
const futureStart = new Date(Date.now() + 86400000 * 7).toISOString(); // 7 days from now
|
| 31 |
+
let bookingCreated = false;
|
| 32 |
+
if (eventTypes.length > 0) {
|
| 33 |
+
const firstEvent = eventTypes[0] as { id: number };
|
| 34 |
+
await measureOperationWarn(
|
| 35 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 36 |
+
`calcom.createBooking.${workspaceId.slice(-8)}`,
|
| 37 |
+
() => ctx.client.integrations.createCalComBooking.mutate({
|
| 38 |
+
workspaceId,
|
| 39 |
+
eventTypeId: firstEvent.id,
|
| 40 |
+
start: futureStart,
|
| 41 |
+
attendee: {
|
| 42 |
+
email: "synthetic-test@micprotocol.local",
|
| 43 |
+
name: "Synthetic Test Guest",
|
| 44 |
+
timeZone: "UTC",
|
| 45 |
+
},
|
| 46 |
+
meetingUrl: undefined,
|
| 47 |
+
}),
|
| 48 |
+
report,
|
| 49 |
+
);
|
| 50 |
+
bookingCreated = true;
|
| 51 |
+
console.log(` π
Booking created via Cal.com event type ${firstEvent.id}`);
|
| 52 |
+
} else {
|
| 53 |
+
console.log(` β οΈ No Cal.com event types available β skipping createBooking`);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// ββ 3. Cross-workspace isolation: try to list another workspace's event types ββ
|
| 57 |
+
const otherAgent = Array.from(allAgents.values()).find(
|
| 58 |
+
(a) => a.workspaceId && a.workspaceId !== workspaceId,
|
| 59 |
+
);
|
| 60 |
+
if (otherAgent) {
|
| 61 |
+
await measureOperationWarn(
|
| 62 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 63 |
+
`calcom.crossWorkspaceListEventTypes.${otherAgent.workspaceId.slice(-8)}`,
|
| 64 |
+
() => ctx.client.integrations.listCalComEventTypes.query({ workspaceId: otherAgent.workspaceId! }),
|
| 65 |
+
report,
|
| 66 |
+
);
|
| 67 |
+
console.log(` π Cross-workspace list attempted against ${otherAgent.workspaceId.slice(-8)}`);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// ββ 4. Cross-workspace isolation: try to create a booking for another workspace ββ
|
| 71 |
+
if (otherAgent && bookingCreated) {
|
| 72 |
+
await measureOperationWarn(
|
| 73 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 74 |
+
`calcom.crossWorkspaceCreateBooking.${otherAgent.workspaceId.slice(-8)}`,
|
| 75 |
+
() => ctx.client.integrations.createCalComBooking.mutate({
|
| 76 |
+
workspaceId: otherAgent.workspaceId!,
|
| 77 |
+
eventTypeId: (eventTypes[0] as { id: number }).id,
|
| 78 |
+
start: futureStart,
|
| 79 |
+
attendee: {
|
| 80 |
+
email: "synthetic-test@micprotocol.local",
|
| 81 |
+
name: "Cross Workspace Test",
|
| 82 |
+
timeZone: "UTC",
|
| 83 |
+
},
|
| 84 |
+
meetingUrl: undefined,
|
| 85 |
+
}),
|
| 86 |
+
report,
|
| 87 |
+
);
|
| 88 |
+
console.log(` π Cross-workspace createBooking attempted against ${otherAgent.workspaceId.slice(-8)}`);
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
export const calcomScenarios: ScenarioDefinition[] = [
|
| 93 |
+
{
|
| 94 |
+
id: "sc-calcom",
|
| 95 |
+
name: "Cal.com Scheduling",
|
| 96 |
+
description: "Connect Cal.com via API key, list event types, create booking, cross-workspace isolation",
|
| 97 |
+
requiredAgents: ["agent-workspace-morning", "agent-workspace-devops"],
|
| 98 |
+
fn: run,
|
| 99 |
+
},
|
| 100 |
+
];
|
scripts/synthetic-agents/scenarios/cross-workspace.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScenarioDefinition, AgentContext } from "../lib/scenario-runner";
|
| 2 |
+
import { measureOperation, measureOperationWarn } from "../lib/scenario-runner";
|
| 3 |
+
import type { ResultsReporter } from "../lib/reporter";
|
| 4 |
+
|
| 5 |
+
async function run(
|
| 6 |
+
primaryCtx: AgentContext,
|
| 7 |
+
report: ResultsReporter,
|
| 8 |
+
allAgents?: Map<string, AgentContext>,
|
| 9 |
+
) {
|
| 10 |
+
const sId = "sc-cross-workspace";
|
| 11 |
+
const sName = "Cross-Workspace Collaboration";
|
| 12 |
+
|
| 13 |
+
console.log(" π Running cross-workspace collaboration tests...");
|
| 14 |
+
|
| 15 |
+
// Search across all profiles (public, no workspace needed)
|
| 16 |
+
const searchResults = await measureOperation(
|
| 17 |
+
primaryCtx.agentId, primaryCtx.agentName, sId, sName,
|
| 18 |
+
"cross.network.search.global",
|
| 19 |
+
() => primaryCtx.client.network.search.query({ query: "expert", limit: 20 }),
|
| 20 |
+
report,
|
| 21 |
+
);
|
| 22 |
+
console.log(` π Cross-workspace search: ${searchResults?.length || 0} results`);
|
| 23 |
+
|
| 24 |
+
// Track views on profiles from other workspaces
|
| 25 |
+
if (searchResults && searchResults.length > 0) {
|
| 26 |
+
for (const profile of searchResults.slice(0, 3)) {
|
| 27 |
+
await measureOperation(
|
| 28 |
+
primaryCtx.agentId, primaryCtx.agentName, sId, sName,
|
| 29 |
+
`cross.network.trackView.${profile.id.slice(-8)}`,
|
| 30 |
+
() => primaryCtx.client.network.trackView.mutate({
|
| 31 |
+
profileId: profile.id,
|
| 32 |
+
source: "search" as any,
|
| 33 |
+
}),
|
| 34 |
+
report,
|
| 35 |
+
);
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// Get trending (no workspace needed)
|
| 40 |
+
await measureOperation(
|
| 41 |
+
primaryCtx.agentId, primaryCtx.agentName, sId, sName,
|
| 42 |
+
"cross.network.trending",
|
| 43 |
+
() => primaryCtx.client.network.trending.query({ limit: 20 }),
|
| 44 |
+
report,
|
| 45 |
+
);
|
| 46 |
+
|
| 47 |
+
// Each agent tries to access other workspaces' podcasts (tests workspace isolation)
|
| 48 |
+
const allPodcastIds = allAgents
|
| 49 |
+
? [...allAgents.values()].flatMap((a) => a.podcastIds)
|
| 50 |
+
: [];
|
| 51 |
+
|
| 52 |
+
const myPodcastIds = new Set(primaryCtx.podcastIds);
|
| 53 |
+
const foreignIds = allPodcastIds.filter((id) => !myPodcastIds.has(id));
|
| 54 |
+
|
| 55 |
+
for (const pid of foreignIds) {
|
| 56 |
+
await measureOperationWarn(
|
| 57 |
+
primaryCtx.agentId, primaryCtx.agentName, sId, sName,
|
| 58 |
+
`cross.podcast.access.foreign.${pid.slice(-8)}`,
|
| 59 |
+
() => primaryCtx.client.podcast.get.query({ id: pid }),
|
| 60 |
+
report,
|
| 61 |
+
);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// Test guest list isolation
|
| 65 |
+
for (const pid of foreignIds) {
|
| 66 |
+
await measureOperationWarn(
|
| 67 |
+
primaryCtx.agentId, primaryCtx.agentName, sId, sName,
|
| 68 |
+
`cross.guest.list.foreign.${pid.slice(-8)}`,
|
| 69 |
+
() => primaryCtx.client.guest.list.query({ podcastId: pid }),
|
| 70 |
+
report,
|
| 71 |
+
);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// Test outreach isolation
|
| 75 |
+
for (const pid of foreignIds.slice(0, 2)) {
|
| 76 |
+
await measureOperationWarn(
|
| 77 |
+
primaryCtx.agentId, primaryCtx.agentName, sId, sName,
|
| 78 |
+
`cross.outreach.stats.foreign.${pid.slice(-8)}`,
|
| 79 |
+
() => primaryCtx.client.outreach.stats.query({ podcastId: pid }),
|
| 80 |
+
report,
|
| 81 |
+
);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Test scheduling isolation
|
| 85 |
+
for (const pid of foreignIds.slice(0, 2)) {
|
| 86 |
+
await measureOperationWarn(
|
| 87 |
+
primaryCtx.agentId, primaryCtx.agentName, sId, sName,
|
| 88 |
+
`cross.scheduling.upcoming.foreign.${pid.slice(-8)}`,
|
| 89 |
+
() => primaryCtx.client.scheduling.upcoming.query({ podcastId: pid, days: 30 }),
|
| 90 |
+
report,
|
| 91 |
+
);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Verify that booking requests from one workspace are visible to the correct workspace
|
| 95 |
+
if (searchResults && searchResults.length > 0) {
|
| 96 |
+
const profile = searchResults[0];
|
| 97 |
+
|
| 98 |
+
// Bookmark from primary workspace
|
| 99 |
+
await measureOperationWarn(
|
| 100 |
+
primaryCtx.agentId, primaryCtx.agentName, sId, sName,
|
| 101 |
+
"cross.network.bookmark.cross",
|
| 102 |
+
() => primaryCtx.client.network.bookmark.mutate({
|
| 103 |
+
profileId: profile.id,
|
| 104 |
+
podcastId: primaryCtx.podcastIds[0],
|
| 105 |
+
status: "saved" as any,
|
| 106 |
+
notes: "Cross-workspace bookmark test",
|
| 107 |
+
}),
|
| 108 |
+
report,
|
| 109 |
+
);
|
| 110 |
+
|
| 111 |
+
// List bookmarks for own workspace
|
| 112 |
+
await measureOperation(
|
| 113 |
+
primaryCtx.agentId, primaryCtx.agentName, sId, sName,
|
| 114 |
+
"cross.network.listBookmarks",
|
| 115 |
+
() => primaryCtx.client.network.listBookmarks.query({
|
| 116 |
+
podcastId: primaryCtx.podcastIds[0],
|
| 117 |
+
}),
|
| 118 |
+
report,
|
| 119 |
+
);
|
| 120 |
+
|
| 121 |
+
// Create booking request to a profile
|
| 122 |
+
await measureOperationWarn(
|
| 123 |
+
primaryCtx.agentId, primaryCtx.agentName, sId, sName,
|
| 124 |
+
"cross.network.createBookingRequest",
|
| 125 |
+
() => primaryCtx.client.network.createBookingRequest.mutate({
|
| 126 |
+
toProfileId: profile.id,
|
| 127 |
+
fromPodcastId: primaryCtx.podcastIds[0],
|
| 128 |
+
message: `Cross-workspace test from ${primaryCtx.agentName}`,
|
| 129 |
+
proposedTopics: ["technology", "collaboration"],
|
| 130 |
+
proposedDates: [new Date(Date.now() + 14 * 86400000).toISOString()],
|
| 131 |
+
}),
|
| 132 |
+
report,
|
| 133 |
+
);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// Now test from EVERY agent's perspective for comprehensive isolation checking
|
| 137 |
+
if (allAgents) {
|
| 138 |
+
for (const [agentId, agentCtx] of allAgents) {
|
| 139 |
+
if (agentId === primaryCtx.agentId) continue;
|
| 140 |
+
|
| 141 |
+
console.log(` Testing isolation from ${agentCtx.agentName}...`);
|
| 142 |
+
|
| 143 |
+
// Each agent tries accessing every other agent's podcasts
|
| 144 |
+
for (const pid of allPodcastIds) {
|
| 145 |
+
if (!agentCtx.podcastIds.includes(pid)) {
|
| 146 |
+
await measureOperationWarn(
|
| 147 |
+
agentId, agentCtx.agentName, sId, sName,
|
| 148 |
+
`iso.podcast.get.${pid.slice(-8)}.from.${agentId.slice(-8)}`,
|
| 149 |
+
() => agentCtx.client.podcast.get.query({ id: pid }),
|
| 150 |
+
report,
|
| 151 |
+
);
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
console.log(" β
Cross-workspace tests completed");
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
export const crossWorkspaceScenarios: ScenarioDefinition[] = [
|
| 161 |
+
{
|
| 162 |
+
id: "sc-cross-workspace",
|
| 163 |
+
name: "Cross-Workspace Collaboration",
|
| 164 |
+
description: "Network operations between different workspace agents",
|
| 165 |
+
requiredAgents: [
|
| 166 |
+
"agent-workspace-morning",
|
| 167 |
+
"agent-workspace-devops",
|
| 168 |
+
"agent-workspace-creator",
|
| 169 |
+
"agent-workspace-climate",
|
| 170 |
+
"agent-workspace-security",
|
| 171 |
+
],
|
| 172 |
+
fn: run,
|
| 173 |
+
},
|
| 174 |
+
];
|
scripts/synthetic-agents/scenarios/document-scenario.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScenarioDefinition, AgentContext } from "../lib/scenario-runner";
|
| 2 |
+
import { measureOperation, measureOperationWarn } from "../lib/scenario-runner";
|
| 3 |
+
import type { ResultsReporter } from "../lib/reporter";
|
| 4 |
+
|
| 5 |
+
async function run(ctx: AgentContext, report: ResultsReporter, allAgents: Map<string, AgentContext>) {
|
| 6 |
+
const sId = "sc-documents";
|
| 7 |
+
const sName = "Document Management";
|
| 8 |
+
const workspaceId = ctx.workspaceId;
|
| 9 |
+
|
| 10 |
+
if (!workspaceId) {
|
| 11 |
+
console.warn(` β οΈ Scenario '${sName}' skipped: no workspace context for agent ${ctx.agentId}`);
|
| 12 |
+
return;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
// ββ 1. List documents ββ
|
| 16 |
+
let documents: unknown[] = [];
|
| 17 |
+
await measureOperation(
|
| 18 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 19 |
+
`documents.list.${workspaceId.slice(-8)}`,
|
| 20 |
+
async () => {
|
| 21 |
+
const result = await ctx.client.documents.list.query({ workspaceId, page: 1, limit: 25 });
|
| 22 |
+
documents = Array.isArray(result?.documents) ? result.documents : [];
|
| 23 |
+
return result;
|
| 24 |
+
},
|
| 25 |
+
report,
|
| 26 |
+
);
|
| 27 |
+
console.log(` π Found ${documents.length} documents`);
|
| 28 |
+
|
| 29 |
+
// ββ 2. Upload a document ββ
|
| 30 |
+
let uploadedDocId: string | null = null;
|
| 31 |
+
await measureOperationWarn(
|
| 32 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 33 |
+
`documents.upload.${workspaceId.slice(-8)}`,
|
| 34 |
+
async () => {
|
| 35 |
+
const sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
| 36 |
+
const result = await ctx.client.documents.upload.mutate({
|
| 37 |
+
workspaceId,
|
| 38 |
+
title: "Synthetic Test Document",
|
| 39 |
+
description: "Uploaded by synthetic agent scenario",
|
| 40 |
+
mimeType: "application/pdf",
|
| 41 |
+
sizeBytes: 1024,
|
| 42 |
+
storageKey: "synthetic-test-doc-001",
|
| 43 |
+
sha256,
|
| 44 |
+
});
|
| 45 |
+
uploadedDocId = (result as any)?.id ?? null;
|
| 46 |
+
return result;
|
| 47 |
+
},
|
| 48 |
+
report,
|
| 49 |
+
);
|
| 50 |
+
if (uploadedDocId) {
|
| 51 |
+
console.log(` π€ Document uploaded: ${uploadedDocId.slice(-8)}`);
|
| 52 |
+
} else {
|
| 53 |
+
console.log(` β οΈ Document upload skipped (feature may be gated)`);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// ββ 3. Get document details ββ
|
| 57 |
+
if (uploadedDocId) {
|
| 58 |
+
await measureOperationWarn(
|
| 59 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 60 |
+
`documents.get.${uploadedDocId.slice(-8)}`,
|
| 61 |
+
() => ctx.client.documents.get.query({ id: uploadedDocId }),
|
| 62 |
+
report,
|
| 63 |
+
);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// ββ 4. Create share link ββ
|
| 67 |
+
if (uploadedDocId) {
|
| 68 |
+
await measureOperationWarn(
|
| 69 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 70 |
+
`documents.createShare.${uploadedDocId.slice(-8)}`,
|
| 71 |
+
async () => {
|
| 72 |
+
const result = await ctx.client.documents.createShare.mutate({
|
| 73 |
+
documentId: uploadedDocId,
|
| 74 |
+
shareType: "public",
|
| 75 |
+
expiresAt: new Date(Date.now() + 86400000 * 7).toISOString(),
|
| 76 |
+
maxDownloads: 10,
|
| 77 |
+
});
|
| 78 |
+
return result;
|
| 79 |
+
},
|
| 80 |
+
report,
|
| 81 |
+
);
|
| 82 |
+
console.log(` π Share link created for document ${uploadedDocId.slice(-8)}`);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// ββ 5. Verify document integrity ββ
|
| 86 |
+
if (uploadedDocId) {
|
| 87 |
+
await measureOperationWarn(
|
| 88 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 89 |
+
`documents.verifyIntegrity.${uploadedDocId.slice(-8)}`,
|
| 90 |
+
() => ctx.client.documents.verifyIntegrity.query({ documentId: uploadedDocId }),
|
| 91 |
+
report,
|
| 92 |
+
);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// ββ 6. Access logs ββ
|
| 96 |
+
if (uploadedDocId) {
|
| 97 |
+
await measureOperationWarn(
|
| 98 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 99 |
+
`documents.accessLogs.${uploadedDocId.slice(-8)}`,
|
| 100 |
+
() => ctx.client.documents.accessLogs.query({ documentId: uploadedDocId, page: 1, limit: 20 }),
|
| 101 |
+
report,
|
| 102 |
+
);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// ββ 7. Cross-workspace isolation: try to list another workspace's documents ββ
|
| 106 |
+
const otherAgent = Array.from(allAgents.values()).find(
|
| 107 |
+
(a) => a.workspaceId && a.workspaceId !== workspaceId,
|
| 108 |
+
);
|
| 109 |
+
if (otherAgent) {
|
| 110 |
+
await measureOperationWarn(
|
| 111 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 112 |
+
`documents.crossWorkspaceList.${otherAgent.workspaceId.slice(-8)}`,
|
| 113 |
+
() => ctx.client.documents.list.query({ workspaceId: otherAgent.workspaceId!, page: 1, limit: 25 }),
|
| 114 |
+
report,
|
| 115 |
+
);
|
| 116 |
+
console.log(` π Cross-workspace document list attempted against ${otherAgent.workspaceId.slice(-8)}`);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// ββ 8. Cleanup: delete uploaded document ββ
|
| 120 |
+
if (uploadedDocId) {
|
| 121 |
+
await measureOperationWarn(
|
| 122 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 123 |
+
`documents.delete.${uploadedDocId.slice(-8)}`,
|
| 124 |
+
() => ctx.client.documents.delete.mutate({ id: uploadedDocId }),
|
| 125 |
+
report,
|
| 126 |
+
);
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
export const documentScenarios: ScenarioDefinition[] = [
|
| 131 |
+
{
|
| 132 |
+
id: "sc-documents",
|
| 133 |
+
name: "Document Management",
|
| 134 |
+
description: "Upload document, create share, verify integrity, access logs, cross-workspace isolation",
|
| 135 |
+
requiredAgents: ["agent-workspace-morning"],
|
| 136 |
+
fn: run,
|
| 137 |
+
},
|
| 138 |
+
];
|
scripts/synthetic-agents/scenarios/guest-pipeline.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScenarioDefinition, AgentContext } from "../lib/scenario-runner";
|
| 2 |
+
import { measureOperation, measureOperationWarn } from "../lib/scenario-runner";
|
| 3 |
+
import type { ResultsReporter } from "../lib/reporter";
|
| 4 |
+
|
| 5 |
+
async function run(ctx: AgentContext, report: ResultsReporter) {
|
| 6 |
+
const sId = "sc-guest-pipeline";
|
| 7 |
+
const sName = "Guest Pipeline";
|
| 8 |
+
|
| 9 |
+
for (const podcastId of ctx.podcastIds) {
|
| 10 |
+
// List guests with various status filters
|
| 11 |
+
const allGuests = await measureOperation(
|
| 12 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 13 |
+
`guest.list.${podcastId.slice(-8)}`,
|
| 14 |
+
() => ctx.client.guest.list.query({ podcastId }),
|
| 15 |
+
report,
|
| 16 |
+
);
|
| 17 |
+
console.log(` π₯ ${allGuests.length} guests in podcast ${podcastId.slice(-8)}`);
|
| 18 |
+
|
| 19 |
+
// Get status counts
|
| 20 |
+
await measureOperation(
|
| 21 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 22 |
+
`guest.statusCounts.${podcastId.slice(-8)}`,
|
| 23 |
+
() => ctx.client.guest.statusCounts.query({ podcastId }),
|
| 24 |
+
report,
|
| 25 |
+
);
|
| 26 |
+
|
| 27 |
+
// Get priority guests
|
| 28 |
+
await measureOperation(
|
| 29 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 30 |
+
`guest.topPriority.${podcastId.slice(-8)}`,
|
| 31 |
+
() => ctx.client.guest.topPriority.query({ podcastId, limit: 5 }),
|
| 32 |
+
report,
|
| 33 |
+
);
|
| 34 |
+
|
| 35 |
+
// Check for guests needing follow-up
|
| 36 |
+
await measureOperation(
|
| 37 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 38 |
+
`guest.needsFollowUp.${podcastId.slice(-8)}`,
|
| 39 |
+
() => ctx.client.guest.needsFollowUp.query({ podcastId, days: 14 }),
|
| 40 |
+
report,
|
| 41 |
+
);
|
| 42 |
+
|
| 43 |
+
// List tags
|
| 44 |
+
await measureOperation(
|
| 45 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 46 |
+
`guest.listTags.${podcastId.slice(-8)}`,
|
| 47 |
+
() => ctx.client.guest.listTags.query({ podcastId }),
|
| 48 |
+
report,
|
| 49 |
+
);
|
| 50 |
+
|
| 51 |
+
// Update each guest's status through the pipeline
|
| 52 |
+
if (allGuests.length > 0) {
|
| 53 |
+
for (const guest of allGuests.slice(0, 3)) {
|
| 54 |
+
const nextStatus = getNextStatus(guest.status);
|
| 55 |
+
if (nextStatus) {
|
| 56 |
+
await measureOperationWarn(
|
| 57 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 58 |
+
`guest.updateStatus.${guest.id.slice(-8)}.to.${nextStatus}`,
|
| 59 |
+
() => ctx.client.guest.updateStatus.mutate({
|
| 60 |
+
podcastId,
|
| 61 |
+
id: guest.id,
|
| 62 |
+
status: nextStatus as any,
|
| 63 |
+
}),
|
| 64 |
+
report,
|
| 65 |
+
);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// Get individual guest
|
| 69 |
+
await measureOperation(
|
| 70 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 71 |
+
`guest.get.${guest.id.slice(-8)}`,
|
| 72 |
+
() => ctx.client.guest.get.query({ podcastId, id: guest.id }),
|
| 73 |
+
report,
|
| 74 |
+
);
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// Create a new guest
|
| 79 |
+
const newGuestId = `guest-${Date.now()}`;
|
| 80 |
+
await measureOperationWarn(
|
| 81 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 82 |
+
"guest.create.new",
|
| 83 |
+
() => ctx.client.guest.create.mutate({
|
| 84 |
+
podcastId,
|
| 85 |
+
data: {
|
| 86 |
+
email: `test-${Date.now()}@micprotocol.local`,
|
| 87 |
+
fullName: `Test Guest ${Date.now()}`,
|
| 88 |
+
},
|
| 89 |
+
}),
|
| 90 |
+
report,
|
| 91 |
+
);
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
function getNextStatus(current: string): string | null {
|
| 96 |
+
const flow = ["prospect", "contacted", "scheduled", "recorded"];
|
| 97 |
+
const idx = flow.indexOf(current);
|
| 98 |
+
if (idx >= 0 && idx < flow.length - 1) return flow[idx + 1];
|
| 99 |
+
return null;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
export const guestPipelineScenarios: ScenarioDefinition[] = [
|
| 103 |
+
{
|
| 104 |
+
id: "sc-guest-pipeline",
|
| 105 |
+
name: "Guest Pipeline Management",
|
| 106 |
+
description: "Guest CRUD, status transitions, tagging, priority scoring",
|
| 107 |
+
requiredAgents: ["agent-workspace-security", "agent-workspace-morning"],
|
| 108 |
+
fn: run,
|
| 109 |
+
},
|
| 110 |
+
];
|
scripts/synthetic-agents/scenarios/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScenarioDefinition } from "../lib/scenario-runner";
|
| 2 |
+
import { workspaceLifecycleScenarios } from "./workspace-lifecycle";
|
| 3 |
+
import { podcastOperationsScenarios } from "./podcast-operations";
|
| 4 |
+
import { guestPipelineScenarios } from "./guest-pipeline";
|
| 5 |
+
import { outreachScenarios } from "./outreach-sequences";
|
| 6 |
+
import { bookingRecordingScenarios } from "./booking-recording";
|
| 7 |
+
import { verifiedNetworkScenarios } from "./verified-network";
|
| 8 |
+
import { adminScenarios } from "./admin-operations";
|
| 9 |
+
import { serviceAdministrationScenarios } from "./service-administration";
|
| 10 |
+
import { crossWorkspaceScenarios } from "./cross-workspace";
|
| 11 |
+
import { supportScenarios } from "./support-scenario";
|
| 12 |
+
import { calcomScenarios } from "./calcom-scenario";
|
| 13 |
+
import { documentScenarios } from "./document-scenario";
|
| 14 |
+
import { securityPolicyScenarios } from "./security-policy-scenario";
|
| 15 |
+
|
| 16 |
+
export const allScenarios: ScenarioDefinition[] = [
|
| 17 |
+
...workspaceLifecycleScenarios,
|
| 18 |
+
...podcastOperationsScenarios,
|
| 19 |
+
...guestPipelineScenarios,
|
| 20 |
+
...outreachScenarios,
|
| 21 |
+
...bookingRecordingScenarios,
|
| 22 |
+
...verifiedNetworkScenarios,
|
| 23 |
+
...adminScenarios,
|
| 24 |
+
...serviceAdministrationScenarios,
|
| 25 |
+
...crossWorkspaceScenarios,
|
| 26 |
+
...supportScenarios,
|
| 27 |
+
...calcomScenarios,
|
| 28 |
+
...documentScenarios,
|
| 29 |
+
...securityPolicyScenarios,
|
| 30 |
+
];
|
scripts/synthetic-agents/scenarios/outreach-sequences.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScenarioDefinition, AgentContext } from "../lib/scenario-runner";
|
| 2 |
+
import { measureOperation, measureOperationWarn } from "../lib/scenario-runner";
|
| 3 |
+
import type { ResultsReporter } from "../lib/reporter";
|
| 4 |
+
|
| 5 |
+
async function run(ctx: AgentContext, report: ResultsReporter) {
|
| 6 |
+
const sId = "sc-outreach-sequences";
|
| 7 |
+
const sName = "Outreach Sequences";
|
| 8 |
+
|
| 9 |
+
for (const podcastId of ctx.podcastIds) {
|
| 10 |
+
// Get available templates
|
| 11 |
+
const templates = await measureOperation(
|
| 12 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 13 |
+
`outreach.getAvailableTemplates.${podcastId.slice(-8)}`,
|
| 14 |
+
() => ctx.client.outreach.getAvailableTemplates.query(),
|
| 15 |
+
report,
|
| 16 |
+
);
|
| 17 |
+
console.log(` π ${templates.length} outreach templates available`);
|
| 18 |
+
|
| 19 |
+
// List outreach sequences for podcast
|
| 20 |
+
const sequences = await measureOperation(
|
| 21 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 22 |
+
`outreach.listByPodcast.${podcastId.slice(-8)}`,
|
| 23 |
+
() => ctx.client.outreach.listByPodcast.query({ podcastId, page: 1, limit: 20 }),
|
| 24 |
+
report,
|
| 25 |
+
);
|
| 26 |
+
console.log(` π¬ ${sequences.items?.length || 0} outreach sequences`);
|
| 27 |
+
|
| 28 |
+
// Get outreach stats
|
| 29 |
+
await measureOperation(
|
| 30 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 31 |
+
`outreach.stats.${podcastId.slice(-8)}`,
|
| 32 |
+
() => ctx.client.outreach.stats.query({ podcastId }),
|
| 33 |
+
report,
|
| 34 |
+
);
|
| 35 |
+
|
| 36 |
+
// Create a sequence for a guest if guests exist
|
| 37 |
+
const guests = await ctx.client.guest.list.query({ podcastId }).catch(() => null);
|
| 38 |
+
if (guests && guests.length > 0) {
|
| 39 |
+
const guest = guests[0];
|
| 40 |
+
|
| 41 |
+
// Try to create sequence (may fail if not Creator plan)
|
| 42 |
+
await measureOperationWarn(
|
| 43 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 44 |
+
`outreach.createSequence.${guest.id.slice(-8)}`,
|
| 45 |
+
() => ctx.client.outreach.createSequence.mutate({
|
| 46 |
+
guestId: guest.id,
|
| 47 |
+
podcastId,
|
| 48 |
+
status: "active" as any,
|
| 49 |
+
}),
|
| 50 |
+
report,
|
| 51 |
+
);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// Track a message open (even without creating one, tests the endpoint)
|
| 55 |
+
await measureOperationWarn(
|
| 56 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 57 |
+
"outreach.trackOpen.fake",
|
| 58 |
+
() => ctx.client.outreach.trackOpen.mutate({ messageId: "00000000-0000-4000-a000-000000000000" }),
|
| 59 |
+
report,
|
| 60 |
+
);
|
| 61 |
+
|
| 62 |
+
await measureOperationWarn(
|
| 63 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 64 |
+
"outreach.trackClick.fake",
|
| 65 |
+
() => ctx.client.outreach.trackClick.mutate({ messageId: "00000000-0000-4000-a000-000000000000" }),
|
| 66 |
+
report,
|
| 67 |
+
);
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
export const outreachScenarios: ScenarioDefinition[] = [
|
| 72 |
+
{
|
| 73 |
+
id: "sc-outreach-sequences",
|
| 74 |
+
name: "Outreach Sequences",
|
| 75 |
+
description: "Create sequences, messages, track opens/clicks",
|
| 76 |
+
requiredAgents: ["agent-workspace-morning", "agent-workspace-creator"],
|
| 77 |
+
fn: run,
|
| 78 |
+
},
|
| 79 |
+
];
|
scripts/synthetic-agents/scenarios/podcast-operations.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScenarioDefinition, AgentContext } from "../lib/scenario-runner";
|
| 2 |
+
import { measureOperation, measureOperationWarn } from "../lib/scenario-runner";
|
| 3 |
+
import type { ResultsReporter } from "../lib/reporter";
|
| 4 |
+
|
| 5 |
+
async function run(ctx: AgentContext, report: ResultsReporter) {
|
| 6 |
+
const sId = "sc-podcast-operations";
|
| 7 |
+
const sName = "Podcast Operations";
|
| 8 |
+
|
| 9 |
+
for (const podcastId of ctx.podcastIds) {
|
| 10 |
+
// Get podcast details
|
| 11 |
+
const podcast = await measureOperation(
|
| 12 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 13 |
+
`podcast.get.${podcastId.slice(-8)}`,
|
| 14 |
+
() => ctx.client.podcast.get.query({ id: podcastId }),
|
| 15 |
+
report,
|
| 16 |
+
);
|
| 17 |
+
|
| 18 |
+
console.log(` ποΈ Podcast: ${podcast.title}`);
|
| 19 |
+
|
| 20 |
+
// Get podcast stats
|
| 21 |
+
await measureOperation(
|
| 22 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 23 |
+
`podcast.stats.${podcastId.slice(-8)}`,
|
| 24 |
+
() => ctx.client.podcast.stats.query({ id: podcastId }),
|
| 25 |
+
report,
|
| 26 |
+
);
|
| 27 |
+
|
| 28 |
+
// Update podcast
|
| 29 |
+
await measureOperation(
|
| 30 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 31 |
+
`podcast.update.${podcastId.slice(-8)}`,
|
| 32 |
+
() => ctx.client.podcast.update.mutate({
|
| 33 |
+
id: podcastId,
|
| 34 |
+
data: { recordingSettings: { defaultPlatform: "zoom", preferredDurationMinutes: 45 } },
|
| 35 |
+
}),
|
| 36 |
+
report,
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Try access to another workspace's podcast (should be forbidden)
|
| 41 |
+
const foreignPodcastId = "33333333-0000-4000-a000-000000000031";
|
| 42 |
+
await measureOperationWarn(
|
| 43 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 44 |
+
"podcast.get.foreign.workspace",
|
| 45 |
+
() => ctx.client.podcast.get.query({ id: foreignPodcastId }),
|
| 46 |
+
report,
|
| 47 |
+
);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export const podcastOperationsScenarios: ScenarioDefinition[] = [
|
| 51 |
+
{
|
| 52 |
+
id: "sc-podcast-operations",
|
| 53 |
+
name: "Podcast Operations",
|
| 54 |
+
description: "CRUD on podcasts, stats, recording settings",
|
| 55 |
+
requiredAgents: ["agent-workspace-creator", "agent-workspace-climate"],
|
| 56 |
+
fn: run,
|
| 57 |
+
},
|
| 58 |
+
];
|
scripts/synthetic-agents/scenarios/security-policy-scenario.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScenarioDefinition, AgentContext } from "../lib/scenario-runner";
|
| 2 |
+
import { measureOperationWarn } from "../lib/scenario-runner";
|
| 3 |
+
import type { ResultsReporter } from "../lib/reporter";
|
| 4 |
+
|
| 5 |
+
async function run(ctx: AgentContext, report: ResultsReporter, allAgents: Map<string, AgentContext>) {
|
| 6 |
+
const sId = "sc-security-policy";
|
| 7 |
+
const sName = "Security Policy";
|
| 8 |
+
|
| 9 |
+
// ββ 1. Screen an email domain against blocklist ββ
|
| 10 |
+
await measureOperationWarn(
|
| 11 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 12 |
+
`securityPolicy.screenEmail`,
|
| 13 |
+
() => ctx.client.securityPolicy.screenEmail.query({ email: "user@sanctioned-domain.com" }),
|
| 14 |
+
report,
|
| 15 |
+
);
|
| 16 |
+
|
| 17 |
+
// ββ 2. Screen an IP against blocklist ββ
|
| 18 |
+
await measureOperationWarn(
|
| 19 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 20 |
+
`securityPolicy.screenIp`,
|
| 21 |
+
() => ctx.client.securityPolicy.screenIp.query({ ip: "10.0.0.99" }),
|
| 22 |
+
report,
|
| 23 |
+
);
|
| 24 |
+
|
| 25 |
+
// ββ 3. Screen a workspace slug ββ
|
| 26 |
+
await measureOperationWarn(
|
| 27 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 28 |
+
`securityPolicy.screenWorkspaceSlug`,
|
| 29 |
+
() => ctx.client.securityPolicy.screenWorkspaceSlug.query({ slug: "sanctioned-workspace" }),
|
| 30 |
+
report,
|
| 31 |
+
);
|
| 32 |
+
|
| 33 |
+
// ββ 4. Add to blocklist ββ
|
| 34 |
+
let blocklistEntryId: string | null = null;
|
| 35 |
+
await measureOperationWarn(
|
| 36 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 37 |
+
`securityPolicy.addToBlocklist`,
|
| 38 |
+
async () => {
|
| 39 |
+
const result = await ctx.client.securityPolicy.addToBlocklist.mutate({
|
| 40 |
+
type: "EMAIL_DOMAIN",
|
| 41 |
+
value: "synthetic-sanctioned.com",
|
| 42 |
+
reason: "Synthetic agent test - sanctioned domain screening",
|
| 43 |
+
});
|
| 44 |
+
blocklistEntryId = (result as any)?.id ?? null;
|
| 45 |
+
return result;
|
| 46 |
+
},
|
| 47 |
+
report,
|
| 48 |
+
);
|
| 49 |
+
|
| 50 |
+
// ββ 5. List blocklist ββ
|
| 51 |
+
await measureOperationWarn(
|
| 52 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 53 |
+
`securityPolicy.listBlocklist`,
|
| 54 |
+
() => ctx.client.securityPolicy.listBlocklist.query({ activeOnly: true }),
|
| 55 |
+
report,
|
| 56 |
+
);
|
| 57 |
+
|
| 58 |
+
// ββ 6. Review queue ββ
|
| 59 |
+
await measureOperationWarn(
|
| 60 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 61 |
+
`securityPolicy.reviewQueue`,
|
| 62 |
+
() => ctx.client.securityPolicy.reviewQueue.query({ page: 1, limit: 20 }),
|
| 63 |
+
report,
|
| 64 |
+
);
|
| 65 |
+
|
| 66 |
+
// ββ 7. Remove from blocklist (cleanup) ββ
|
| 67 |
+
if (blocklistEntryId) {
|
| 68 |
+
await measureOperationWarn(
|
| 69 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 70 |
+
`securityPolicy.removeFromBlocklist`,
|
| 71 |
+
() => ctx.client.securityPolicy.removeFromBlocklist.mutate({ id: blocklistEntryId }),
|
| 72 |
+
report,
|
| 73 |
+
);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
console.log(` π‘οΈ Security policy screening and blocklist operations complete`);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
export const securityPolicyScenarios: ScenarioDefinition[] = [
|
| 80 |
+
{
|
| 81 |
+
id: "sc-security-policy",
|
| 82 |
+
name: "Security Policy",
|
| 83 |
+
description: "Sanction screening, blocklist management, flag/revoke workflows",
|
| 84 |
+
requiredAgents: ["agent-admin-01"],
|
| 85 |
+
fn: run,
|
| 86 |
+
},
|
| 87 |
+
];
|
scripts/synthetic-agents/scenarios/service-administration.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScenarioDefinition, AgentContext } from "../lib/scenario-runner";
|
| 2 |
+
import { measureOperation } from "../lib/scenario-runner";
|
| 3 |
+
import type { ResultsReporter } from "../lib/reporter";
|
| 4 |
+
import { createAgentClient } from "../lib/trpc-client";
|
| 5 |
+
|
| 6 |
+
async function run(ctx: AgentContext, report: ResultsReporter, allAgents: Map<string, AgentContext>) {
|
| 7 |
+
const sId = "sc-service-administration";
|
| 8 |
+
const sName = "Service Administration";
|
| 9 |
+
console.log(" π οΈ Running service administration (clip-scorer + mic-realtime)...");
|
| 10 |
+
|
| 11 |
+
// ββ Platform-level service admin reads (admin agent only) ββββββββββββββ
|
| 12 |
+
await measureOperation(ctx.agentId, ctx.agentName, sId, sName, "admin.clipScorer.locks",
|
| 13 |
+
() => ctx.client.admin.clipScorer.locks.query(), report);
|
| 14 |
+
await measureOperation(ctx.agentId, ctx.agentName, sId, sName, "admin.clipScorer.scratch",
|
| 15 |
+
() => ctx.client.admin.clipScorer.scratch.query(), report);
|
| 16 |
+
await measureOperation(ctx.agentId, ctx.agentName, sId, sName, "admin.realtime.stats",
|
| 17 |
+
() => ctx.client.admin.realtime.stats.query(), report);
|
| 18 |
+
await measureOperation(ctx.agentId, ctx.agentName, sId, sName, "admin.realtime.metrics",
|
| 19 |
+
() => ctx.client.admin.realtime.metrics.query(), report);
|
| 20 |
+
await measureOperation(ctx.agentId, ctx.agentName, sId, sName, "admin.realtime.traces",
|
| 21 |
+
() => ctx.client.admin.realtime.traces.query({ limit: 20 }), report);
|
| 22 |
+
|
| 23 |
+
// ββ Privilege escalation check: a non-admin workspace owner must be FORBIDDEN βββββ
|
| 24 |
+
const workspaceAgent = allAgents.get("agent-workspace-morning");
|
| 25 |
+
if (workspaceAgent) {
|
| 26 |
+
const wsClient = createAgentClient({
|
| 27 |
+
baseUrl: "http://localhost:4000/trpc",
|
| 28 |
+
userId: workspaceAgent.userId,
|
| 29 |
+
credential: workspaceAgent.credential,
|
| 30 |
+
devMode: true,
|
| 31 |
+
});
|
| 32 |
+
await expectForbidden(report, sId, sName, workspaceAgent.agentId, workspaceAgent.agentName,
|
| 33 |
+
"admin.clipScorer.locks (non-admin)", () => wsClient.admin.clipScorer.locks.query());
|
| 34 |
+
await expectForbidden(report, sId, sName, workspaceAgent.agentId, workspaceAgent.agentName,
|
| 35 |
+
"admin.realtime.stats (non-admin)", () => wsClient.admin.realtime.stats.query());
|
| 36 |
+
await expectForbidden(report, sId, sName, workspaceAgent.agentId, workspaceAgent.agentName,
|
| 37 |
+
"admin.realtime.traces (non-admin)", () => wsClient.admin.realtime.traces.query({ limit: 5 }));
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
console.log(" β
Service administration completed");
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/** Records pass only when the call is correctly rejected with FORBIDDEN/403.
|
| 44 |
+
* A success here signals a privilege-escalation weakness and is recorded as fail. */
|
| 45 |
+
async function expectForbidden(
|
| 46 |
+
report: ResultsReporter,
|
| 47 |
+
sId: string,
|
| 48 |
+
sName: string,
|
| 49 |
+
agentId: string,
|
| 50 |
+
agentName: string,
|
| 51 |
+
operation: string,
|
| 52 |
+
fn: () => Promise<unknown>,
|
| 53 |
+
): Promise<void> {
|
| 54 |
+
const start = Date.now();
|
| 55 |
+
try {
|
| 56 |
+
await fn();
|
| 57 |
+
report.record({
|
| 58 |
+
scenarioId: sId,
|
| 59 |
+
scenarioName: sName,
|
| 60 |
+
agentId,
|
| 61 |
+
agentName,
|
| 62 |
+
operation,
|
| 63 |
+
status: "fail",
|
| 64 |
+
durationMs: Date.now() - start,
|
| 65 |
+
error: "Expected FORBIDDEN but request succeeded (potential privilege escalation)",
|
| 66 |
+
});
|
| 67 |
+
} catch (err) {
|
| 68 |
+
const msg = (err as Error).message;
|
| 69 |
+
if (msg.includes("FORBIDDEN") || msg.includes("403")) {
|
| 70 |
+
report.record({
|
| 71 |
+
scenarioId: sId,
|
| 72 |
+
scenarioName: sName,
|
| 73 |
+
agentId,
|
| 74 |
+
agentName,
|
| 75 |
+
operation,
|
| 76 |
+
status: "pass",
|
| 77 |
+
durationMs: Date.now() - start,
|
| 78 |
+
});
|
| 79 |
+
} else {
|
| 80 |
+
report.record({
|
| 81 |
+
scenarioId: sId,
|
| 82 |
+
scenarioName: sName,
|
| 83 |
+
agentId,
|
| 84 |
+
agentName,
|
| 85 |
+
operation,
|
| 86 |
+
status: "fail",
|
| 87 |
+
durationMs: Date.now() - start,
|
| 88 |
+
error: msg,
|
| 89 |
+
});
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
export const serviceAdministrationScenarios: ScenarioDefinition[] = [
|
| 95 |
+
{
|
| 96 |
+
id: "sc-service-administration",
|
| 97 |
+
name: "Service Administration",
|
| 98 |
+
description: "Clip-scorer lock/scratch and mic-realtime stats/traces access, admin-only",
|
| 99 |
+
requiredAgents: ["agent-admin-01"],
|
| 100 |
+
fn: run,
|
| 101 |
+
},
|
| 102 |
+
];
|
scripts/synthetic-agents/scenarios/support-scenario.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScenarioDefinition, AgentContext } from "../lib/scenario-runner";
|
| 2 |
+
import { measureOperation, measureOperationWarn } from "../lib/scenario-runner";
|
| 3 |
+
import type { ResultsReporter } from "../lib/reporter";
|
| 4 |
+
|
| 5 |
+
async function run(ctx: AgentContext, report: ResultsReporter, allAgents: Map<string, AgentContext>) {
|
| 6 |
+
const sId = "sc-support";
|
| 7 |
+
const sName = "Customer Support";
|
| 8 |
+
const workspaceId = ctx.workspaceId;
|
| 9 |
+
|
| 10 |
+
if (!workspaceId) {
|
| 11 |
+
console.warn(` β οΈ Scenario '${sName}' skipped: no workspace context for agent ${ctx.agentId}`);
|
| 12 |
+
return;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
// ββ 1. List tickets (no filters) ββ
|
| 16 |
+
await measureOperation(
|
| 17 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 18 |
+
`support.listTickets.${workspaceId.slice(-8)}`,
|
| 19 |
+
() => ctx.client.support.list.query({ workspaceId }),
|
| 20 |
+
report,
|
| 21 |
+
);
|
| 22 |
+
|
| 23 |
+
// ββ 2. List tickets with status filter ββ
|
| 24 |
+
await measureOperation(
|
| 25 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 26 |
+
`support.listTickets.open.${workspaceId.slice(-8)}`,
|
| 27 |
+
() => ctx.client.support.list.query({ workspaceId, filter: { status: "open", page: 1, limit: 25 } }),
|
| 28 |
+
report,
|
| 29 |
+
);
|
| 30 |
+
|
| 31 |
+
// ββ 3. Create a support ticket ββ
|
| 32 |
+
let ticketId: string | null = null;
|
| 33 |
+
const createResult = await measureOperationWarn(
|
| 34 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 35 |
+
`support.createTicket.${workspaceId.slice(-8)}`,
|
| 36 |
+
() => ctx.client.support.create.mutate({
|
| 37 |
+
subject: "Test ticket from synthetic agent",
|
| 38 |
+
description: "This is a test ticket created by the synthetic support scenario.",
|
| 39 |
+
category: "technical",
|
| 40 |
+
priority: "high",
|
| 41 |
+
}),
|
| 42 |
+
report,
|
| 43 |
+
);
|
| 44 |
+
|
| 45 |
+
if (createResult && typeof createResult === "object" && (createResult as any).id) {
|
| 46 |
+
ticketId = (createResult as any).id;
|
| 47 |
+
console.log(` π« Created ticket ${ticketId.slice(-8)}`);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// ββ 4. Get ticket details ββ
|
| 51 |
+
if (ticketId) {
|
| 52 |
+
await measureOperation(
|
| 53 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 54 |
+
`support.getTicket.${ticketId.slice(-8)}`,
|
| 55 |
+
() => ctx.client.support.get.query({ ticketId }),
|
| 56 |
+
report,
|
| 57 |
+
);
|
| 58 |
+
|
| 59 |
+
// ββ 5. Get ticket with messages ββ
|
| 60 |
+
await measureOperation(
|
| 61 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 62 |
+
`support.getTicketWithMessages.${ticketId.slice(-8)}`,
|
| 63 |
+
() => ctx.client.support.getWithMessages.query({ ticketId }),
|
| 64 |
+
report,
|
| 65 |
+
);
|
| 66 |
+
|
| 67 |
+
// ββ 6. Create a message on the ticket ββ
|
| 68 |
+
await measureOperation(
|
| 69 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 70 |
+
`support.createMessage.${ticketId.slice(-8)}`,
|
| 71 |
+
() => ctx.client.support.createMessage.mutate({
|
| 72 |
+
ticketId,
|
| 73 |
+
body: "This is a test message from the synthetic support scenario.",
|
| 74 |
+
messageType: "inbound",
|
| 75 |
+
}),
|
| 76 |
+
report,
|
| 77 |
+
);
|
| 78 |
+
|
| 79 |
+
// ββ 7. Update ticket status ββ
|
| 80 |
+
await measureOperationWarn(
|
| 81 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 82 |
+
`support.updateTicket.${ticketId.slice(-8)}`,
|
| 83 |
+
() => ctx.client.support.update.mutate({
|
| 84 |
+
ticketId,
|
| 85 |
+
status: "pending",
|
| 86 |
+
}),
|
| 87 |
+
report,
|
| 88 |
+
);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// ββ 8. List categories (admin operation) ββ
|
| 92 |
+
await measureOperationWarn(
|
| 93 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 94 |
+
`support.listCategories.${workspaceId.slice(-8)}`,
|
| 95 |
+
() => ctx.client.support.adminListCategories.query({ workspaceId }),
|
| 96 |
+
report,
|
| 97 |
+
);
|
| 98 |
+
|
| 99 |
+
// ββ 9. CSAT summary ββ
|
| 100 |
+
await measureOperationWarn(
|
| 101 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 102 |
+
`support.csatSummary.${workspaceId.slice(-8)}`,
|
| 103 |
+
() => ctx.client.support.csatSummary.query({}),
|
| 104 |
+
report,
|
| 105 |
+
);
|
| 106 |
+
|
| 107 |
+
// ββ 10. SLA breaches ββ
|
| 108 |
+
await measureOperationWarn(
|
| 109 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 110 |
+
`support.slaBreaches.${workspaceId.slice(-8)}`,
|
| 111 |
+
() => ctx.client.support.slaBreaches.query({}),
|
| 112 |
+
report,
|
| 113 |
+
);
|
| 114 |
+
|
| 115 |
+
// ββ 11. Cross-workspace isolation: try to access another workspace's ticket ββ
|
| 116 |
+
// Find another agent with a workspace
|
| 117 |
+
const otherAgent = Array.from(allAgents.values()).find(
|
| 118 |
+
(a) => a.workspaceId && a.workspaceId !== workspaceId,
|
| 119 |
+
);
|
| 120 |
+
if (otherAgent && ticketId) {
|
| 121 |
+
await measureOperationWarn(
|
| 122 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 123 |
+
`support.crossWorkspaceAccess.${ticketId.slice(-8)}`,
|
| 124 |
+
() => ctx.client.support.get.query({ ticketId, workspaceId: otherAgent.workspaceId! }),
|
| 125 |
+
report,
|
| 126 |
+
);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// ββ 12. Rate satisfaction (if ticket exists) ββ
|
| 130 |
+
if (ticketId) {
|
| 131 |
+
await measureOperationWarn(
|
| 132 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 133 |
+
`support.rateSatisfaction.${ticketId.slice(-8)}`,
|
| 134 |
+
() => ctx.client.support.rateSatisfaction.mutate({
|
| 135 |
+
ticketId,
|
| 136 |
+
rating: 5,
|
| 137 |
+
comment: "Great support experience!",
|
| 138 |
+
}),
|
| 139 |
+
report,
|
| 140 |
+
);
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
export const supportScenarios: ScenarioDefinition[] = [
|
| 145 |
+
{
|
| 146 |
+
id: "sc-support",
|
| 147 |
+
name: "Customer Support",
|
| 148 |
+
description: "Ticket creation, message exchange, status changes, CSAT rating, SLA breaches, categories, cross-workspace isolation",
|
| 149 |
+
requiredAgents: ["agent-workspace-morning", "agent-workspace-security"],
|
| 150 |
+
fn: run,
|
| 151 |
+
},
|
| 152 |
+
];
|
scripts/synthetic-agents/scenarios/verified-network.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScenarioDefinition, AgentContext } from "../lib/scenario-runner";
|
| 2 |
+
import { measureOperation, measureOperationWarn } from "../lib/scenario-runner";
|
| 3 |
+
import type { ResultsReporter } from "../lib/reporter";
|
| 4 |
+
|
| 5 |
+
async function run(ctx: AgentContext, report: ResultsReporter) {
|
| 6 |
+
const sId = "sc-verified-network";
|
| 7 |
+
const sName = "Verified Guest Network";
|
| 8 |
+
|
| 9 |
+
// Search for guest profiles
|
| 10 |
+
const searchResults = await measureOperation(
|
| 11 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 12 |
+
"network.search",
|
| 13 |
+
() => ctx.client.network.search.query({
|
| 14 |
+
query: "security cloud",
|
| 15 |
+
limit: 10,
|
| 16 |
+
}),
|
| 17 |
+
report,
|
| 18 |
+
);
|
| 19 |
+
console.log(` π Search returned ${searchResults?.length || 0} results`);
|
| 20 |
+
|
| 21 |
+
// Get trending profiles
|
| 22 |
+
const trending = await measureOperation(
|
| 23 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 24 |
+
"network.trending",
|
| 25 |
+
() => ctx.client.network.trending.query({ limit: 5 }),
|
| 26 |
+
report,
|
| 27 |
+
);
|
| 28 |
+
console.log(` π₯ ${trending?.length || 0} trending profiles`);
|
| 29 |
+
|
| 30 |
+
// Get my profile
|
| 31 |
+
const myProfile = await measureOperationWarn(
|
| 32 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 33 |
+
"network.getMyProfile",
|
| 34 |
+
() => ctx.client.network.getMyProfile.query(),
|
| 35 |
+
report,
|
| 36 |
+
);
|
| 37 |
+
|
| 38 |
+
// Get profile by vanity URL (if search returned results)
|
| 39 |
+
if (searchResults && searchResults.length > 0) {
|
| 40 |
+
const profile = searchResults[0];
|
| 41 |
+
await measureOperation(
|
| 42 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 43 |
+
"network.getProfileById",
|
| 44 |
+
() => ctx.client.network.getProfileById.query({ id: profile.id }),
|
| 45 |
+
report,
|
| 46 |
+
);
|
| 47 |
+
|
| 48 |
+
// Track a profile view
|
| 49 |
+
await measureOperation(
|
| 50 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 51 |
+
"network.trackView",
|
| 52 |
+
() => ctx.client.network.trackView.mutate({
|
| 53 |
+
profileId: profile.id,
|
| 54 |
+
source: "search" as any,
|
| 55 |
+
}),
|
| 56 |
+
report,
|
| 57 |
+
);
|
| 58 |
+
|
| 59 |
+
// Create a bookmark
|
| 60 |
+
const bookmark = await measureOperationWarn(
|
| 61 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 62 |
+
"network.bookmark",
|
| 63 |
+
() => ctx.client.network.bookmark.mutate({
|
| 64 |
+
guestProfileId: profile.id,
|
| 65 |
+
podcastId: ctx.podcastIds[0],
|
| 66 |
+
notes: `Bookmarked via ${ctx.agentName} synthetic agent`,
|
| 67 |
+
}),
|
| 68 |
+
report,
|
| 69 |
+
);
|
| 70 |
+
|
| 71 |
+
// List bookmarks
|
| 72 |
+
await measureOperation(
|
| 73 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 74 |
+
"network.listBookmarks",
|
| 75 |
+
() => ctx.client.network.listBookmarks.query({
|
| 76 |
+
podcastId: ctx.podcastIds[0],
|
| 77 |
+
}),
|
| 78 |
+
report,
|
| 79 |
+
);
|
| 80 |
+
|
| 81 |
+
// Send a booking request
|
| 82 |
+
const bookingRequest = await measureOperationWarn(
|
| 83 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 84 |
+
"network.createBookingRequest",
|
| 85 |
+
() => ctx.client.network.createBookingRequest.mutate({
|
| 86 |
+
toProfileId: profile.id,
|
| 87 |
+
fromPodcastId: ctx.podcastIds[0],
|
| 88 |
+
message: `Synthetic test: would love to feature you!`,
|
| 89 |
+
proposedTopics: ["technology", "innovation"],
|
| 90 |
+
proposedDates: [new Date(Date.now() + 30 * 86400000).toISOString()],
|
| 91 |
+
}),
|
| 92 |
+
report,
|
| 93 |
+
);
|
| 94 |
+
|
| 95 |
+
// Respond to booking request (accept)
|
| 96 |
+
if (bookingRequest) {
|
| 97 |
+
await measureOperationWarn(
|
| 98 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 99 |
+
"network.respondToBookingRequest",
|
| 100 |
+
() => ctx.client.network.respondToBookingRequest.mutate({
|
| 101 |
+
requestId: bookingRequest.id,
|
| 102 |
+
status: "accepted" as any,
|
| 103 |
+
}),
|
| 104 |
+
report,
|
| 105 |
+
);
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
export const verifiedNetworkScenarios: ScenarioDefinition[] = [
|
| 111 |
+
{
|
| 112 |
+
id: "sc-verified-network",
|
| 113 |
+
name: "Verified Guest Network",
|
| 114 |
+
description: "Profile search, bookmarks, booking requests, reviews",
|
| 115 |
+
requiredAgents: ["agent-workspace-security", "agent-workspace-creator"],
|
| 116 |
+
fn: run,
|
| 117 |
+
},
|
| 118 |
+
];
|
scripts/synthetic-agents/scenarios/workspace-lifecycle.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ScenarioDefinition, AgentContext } from "../lib/scenario-runner";
|
| 2 |
+
import { measureOperation } from "../lib/scenario-runner";
|
| 3 |
+
import type { ResultsReporter } from "../lib/reporter";
|
| 4 |
+
|
| 5 |
+
async function run(ctx: AgentContext, report: ResultsReporter) {
|
| 6 |
+
const sId = "sc-workspace-lifecycle";
|
| 7 |
+
const sName = "Workspace Lifecycle";
|
| 8 |
+
|
| 9 |
+
// List workspaces
|
| 10 |
+
const workspaces = await measureOperation(
|
| 11 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 12 |
+
"workspace.list",
|
| 13 |
+
() => ctx.client.workspace.list.query(),
|
| 14 |
+
report,
|
| 15 |
+
);
|
| 16 |
+
console.log(` π Found ${workspaces.length} workspace(s)`);
|
| 17 |
+
|
| 18 |
+
if (workspaces.length > 0) {
|
| 19 |
+
const ws = workspaces[0];
|
| 20 |
+
|
| 21 |
+
// Get workspace details
|
| 22 |
+
const wsDetail = await measureOperation(
|
| 23 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 24 |
+
"workspace.get",
|
| 25 |
+
() => ctx.client.workspace.get.query({ id: ws.id }),
|
| 26 |
+
report,
|
| 27 |
+
);
|
| 28 |
+
|
| 29 |
+
// Update workspace
|
| 30 |
+
await measureOperation(
|
| 31 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 32 |
+
"workspace.update",
|
| 33 |
+
() => ctx.client.workspace.update.mutate({
|
| 34 |
+
id: ws.id,
|
| 35 |
+
data: { name: `${wsDetail.name} (Agent Test)` },
|
| 36 |
+
}),
|
| 37 |
+
report,
|
| 38 |
+
);
|
| 39 |
+
|
| 40 |
+
// Restore original name
|
| 41 |
+
await measureOperation(
|
| 42 |
+
ctx.agentId, ctx.agentName, sId, sName,
|
| 43 |
+
"workspace.update.restore",
|
| 44 |
+
() => ctx.client.workspace.update.mutate({
|
| 45 |
+
id: ws.id,
|
| 46 |
+
data: { name: wsDetail.name },
|
| 47 |
+
}),
|
| 48 |
+
report,
|
| 49 |
+
);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Try workspace operations without workspace context (should fail)
|
| 53 |
+
try {
|
| 54 |
+
await ctx.client.workspace.list.query();
|
| 55 |
+
} catch (e) {
|
| 56 |
+
report.record({
|
| 57 |
+
scenarioId: sId,
|
| 58 |
+
scenarioName: sName,
|
| 59 |
+
agentId: ctx.agentId,
|
| 60 |
+
agentName: ctx.agentName,
|
| 61 |
+
operation: "workspace.list.without.workspace",
|
| 62 |
+
status: "warn",
|
| 63 |
+
durationMs: 0,
|
| 64 |
+
error: (e as Error).message,
|
| 65 |
+
});
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
export const workspaceLifecycleScenarios: ScenarioDefinition[] = [
|
| 70 |
+
{
|
| 71 |
+
id: "sc-workspace-lifecycle",
|
| 72 |
+
name: "Workspace Lifecycle",
|
| 73 |
+
description: "CRUD operations on workspaces",
|
| 74 |
+
requiredAgents: ["agent-workspace-morning", "agent-workspace-devops"],
|
| 75 |
+
fn: run,
|
| 76 |
+
},
|
| 77 |
+
];
|
scripts/synthetic-agents/test-report.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
β MicProtocol - Synthetic Agent Test Report β
|
| 3 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 4 |
+
|
| 5 |
+
Duration: 20.9s
|
| 6 |
+
Total operations: 100
|
| 7 |
+
Passed: 49 Failed: 0 Warnings: 51
|
| 8 |
+
Pass rate: 49.0%
|
| 9 |
+
|
| 10 |
+
β
No weaknesses detected.
|
scripts/synthetic-agents/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"module": "commonjs",
|
| 5 |
+
"moduleResolution": "node10",
|
| 6 |
+
"ignoreDeprecations": "6.0",
|
| 7 |
+
"esModuleInterop": true,
|
| 8 |
+
"skipLibCheck": true,
|
| 9 |
+
"resolveJsonModule": true,
|
| 10 |
+
"rootDir": "."
|
| 11 |
+
},
|
| 12 |
+
"include": ["**/*.ts"]
|
| 13 |
+
}
|
scripts/synthetic-agents/weaknesses.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
[]
|