Benaah commited on
Commit
059ec0d
Β·
verified Β·
1 Parent(s): 801f2d3

Upload folder using huggingface_hub

Browse files
Files changed (33) hide show
  1. scripts/documenso-cert/.gitkeep +0 -0
  2. scripts/documenso-cert/.passphrase +1 -0
  3. scripts/documenso-cert/README.md +28 -0
  4. scripts/documenso-cert/cert.p12 +0 -0
  5. scripts/generate-secrets.sh +148 -0
  6. scripts/init-databases.sql +43 -0
  7. scripts/render-build.sh +25 -0
  8. scripts/sync-env.sh +95 -0
  9. scripts/synthetic-agents/agents-manifest.json +347 -0
  10. scripts/synthetic-agents/lib/reporter.ts +198 -0
  11. scripts/synthetic-agents/lib/scenario-runner.ts +122 -0
  12. scripts/synthetic-agents/lib/trpc-client.ts +62 -0
  13. scripts/synthetic-agents/orchestrator.ts +194 -0
  14. scripts/synthetic-agents/package.json +18 -0
  15. scripts/synthetic-agents/podman-compose.yml +229 -0
  16. scripts/synthetic-agents/run-agents.sh +146 -0
  17. scripts/synthetic-agents/scenarios/admin-operations.ts +86 -0
  18. scripts/synthetic-agents/scenarios/booking-recording.ts +99 -0
  19. scripts/synthetic-agents/scenarios/calcom-scenario.ts +100 -0
  20. scripts/synthetic-agents/scenarios/cross-workspace.ts +174 -0
  21. scripts/synthetic-agents/scenarios/document-scenario.ts +138 -0
  22. scripts/synthetic-agents/scenarios/guest-pipeline.ts +110 -0
  23. scripts/synthetic-agents/scenarios/index.ts +30 -0
  24. scripts/synthetic-agents/scenarios/outreach-sequences.ts +79 -0
  25. scripts/synthetic-agents/scenarios/podcast-operations.ts +58 -0
  26. scripts/synthetic-agents/scenarios/security-policy-scenario.ts +87 -0
  27. scripts/synthetic-agents/scenarios/service-administration.ts +102 -0
  28. scripts/synthetic-agents/scenarios/support-scenario.ts +152 -0
  29. scripts/synthetic-agents/scenarios/verified-network.ts +118 -0
  30. scripts/synthetic-agents/scenarios/workspace-lifecycle.ts +77 -0
  31. scripts/synthetic-agents/test-report.txt +10 -0
  32. scripts/synthetic-agents/tsconfig.json +13 -0
  33. 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
+ []