@dypai-ai/mcp 1.5.6 → 1.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +205 -37
- package/src/tools/frontend.js +3 -3
- package/src/tools/search-logs-offload.js +1 -0
- package/src/tools/sync/push.js +1 -1
- package/src/tools/sync/test-endpoint.js +3 -0
- package/src/tools/sync/validate.js +185 -46
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -115,7 +115,37 @@ const REMOTE_TOOLS = [
|
|
|
115
115
|
// ── Project ───────────────────────────────────────────────────────────────
|
|
116
116
|
{ name: "list_projects", description: "Lists all projects you have access to across your organizations. Returns project id, name, description, organization, subscription plan, and status. Use this as the first step to discover which projects are available, then pass project_id to other tools.", inputSchema: { type: "object", properties: { organization_id: { type: "string", description: "Optional. Filter projects by organization UUID." } }, required: [] } },
|
|
117
117
|
{ name: "get_project", description: "Gets detailed information about a specific project. Returns project name, description, organization, plan, status, engine URL, frontend slug, and timestamps.", inputSchema: { type: "object", properties: { project_id: { type: "string" } }, required: ["project_id"] } },
|
|
118
|
-
{
|
|
118
|
+
{
|
|
119
|
+
name: "manage_project_access_profile",
|
|
120
|
+
description: `Read or update the project's product access profile.
|
|
121
|
+
|
|
122
|
+
Use this when you know what kind of app is being built:
|
|
123
|
+
- private/admin tool: app_visibility='private', auth_scope='admin_only', has_admin_area=true, root_requires_auth=true
|
|
124
|
+
- public landing with admin panel: app_visibility='mixed', auth_scope='admin_only', has_public_area=true, has_admin_area=true
|
|
125
|
+
- customer/user portal: app_visibility='private' or 'mixed', auth_scope='end_users' or 'admin_and_end_users', has_end_user_accounts=true
|
|
126
|
+
- public-only site: app_visibility='public', auth_scope='none', role_model='none', root_requires_auth=false
|
|
127
|
+
|
|
128
|
+
This stores classification metadata only. It does not create users, roles, login UI, tables, endpoints, or publish anything.`,
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: "object",
|
|
131
|
+
properties: {
|
|
132
|
+
project_id: { type: "string", description: "Project UUID. Required for user tokens; auto-detected for project tokens." },
|
|
133
|
+
operation: { type: "string", enum: ["get", "update"], description: "Read or update the project access classification." },
|
|
134
|
+
app_visibility: { type: "string", enum: ["public", "private", "mixed"], description: "Public surface of the app." },
|
|
135
|
+
auth_scope: { type: "string", enum: ["none", "admin_only", "internal_users", "end_users", "admin_and_end_users"], description: "Who needs authentication." },
|
|
136
|
+
role_model: { type: "string", enum: ["none", "single_role", "multi_role"], description: "Whether the app has no roles, one role, or multiple roles." },
|
|
137
|
+
has_admin_area: { type: "boolean", description: "Whether the app should include an admin/private management area." },
|
|
138
|
+
has_public_area: { type: "boolean", description: "Whether the app should include pages usable without login." },
|
|
139
|
+
has_end_user_accounts: { type: "boolean", description: "Whether non-admin end users have their own accounts." },
|
|
140
|
+
root_requires_auth: { type: "boolean", description: "Whether the root route should require login." },
|
|
141
|
+
metadata_patch: { type: "object", description: "Optional non-sensitive notes to merge into access_metadata. Sensitive-looking keys are dropped." },
|
|
142
|
+
reason: { type: "string", description: "Short reason for the update." },
|
|
143
|
+
source: { type: "string", description: "Optional caller label. Defaults to mcp." },
|
|
144
|
+
},
|
|
145
|
+
required: ["operation"],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{ name: "list_ai_models", description: "List only the DYPAI Managed AI models that are active for a project. Returns the project-gated OpenRouter model catalog priced in AI Credits per 1M tokens, RPM limit, max output tokens, active/available counts, billing metadata, and the exact node parameters to use. Call this before creating or editing an AI Agent node with DYPAI Managed models. Agents must not invent or use inactive model ids. Use provider='openrouter' and do NOT set credential_id; DYPAI uses the platform OpenRouter key and deducts usage from the organization's AI Credits.", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "Project UUID whose plan and Model Gateway settings determine the active Managed AI catalog." } }, required: ["project_id"] } },
|
|
119
149
|
{ name: "create_project", description: "Create a new DYPAI project (free plan). Creates a full project with database, engine, GitHub repo, and frontend hosting. BLOCKS by default until provisioning finishes (~60s typical, 120s max) — when it returns, the project_id is ready to use with execute_sql, endpoint tools, etc. Pass wait_until_ready:false for batch flows.\n\nName collision: if another project in the same org already uses the name (case-insensitive), returns {error:'name_taken', existing_project_id, suggestions:[...]}. Pick a different name or use the existing project.\n\nIMPORTANT: before calling, check for a matching template with `search_project_templates`. Passing a `template_slug` drops in a ready-made schema + endpoints + UI that cover 70% of common app types. Only create a blank project if nothing matches.", inputSchema: { type: "object", properties: { name: { type: "string", description: "Project name (e.g. 'My Veterinary App')" }, organization_id: { type: "string", description: "Optional. Uses default org if omitted." }, description: { type: "string" }, template_slug: { type: "string", description: "RECOMMENDED. Project template slug to start from (e.g. 'clinic', 'gym', 'waitlist', 'blank'). Always call search_project_templates first to find the best match." }, wait_until_ready: { type: "boolean", description: "If true (default), blocks until provisioning completes and the project is ready for all operations. If false, returns immediately with status='provisioning' — caller must poll get_project before using.", default: true } }, required: ["name"] } },
|
|
120
150
|
{ name: "get_app_credentials", description: "Lists available credentials in the current application. Returns API keys, anon key, service role key, and engine URL needed for SDK configuration.", inputSchema: { type: "object", properties: { project_id: { type: "string" } }, required: [] } },
|
|
121
151
|
|
|
@@ -354,7 +384,7 @@ Notes:
|
|
|
354
384
|
// ── Triggers (Schedules + Webhooks) ──────────────────────────────────────
|
|
355
385
|
// These tools OBSERVE + CONTROL trigger runtime state. The DEFINITION lives
|
|
356
386
|
// in the endpoint YAML (`trigger.schedule` / `trigger.webhook`) — to change
|
|
357
|
-
// a cron expression or webhook path, edit the YAML and
|
|
387
|
+
// a cron expression or webhook path, edit the YAML and \`dypai_push\`.
|
|
358
388
|
{
|
|
359
389
|
name: "manage_schedules",
|
|
360
390
|
description: `Observe + control cron schedules at runtime, by endpoint name.
|
|
@@ -437,15 +467,16 @@ endpoint YAML and \`dypai_push\`. This tool does NOT modify the definition.`,
|
|
|
437
467
|
// ── Observability ─────────────────────────────────────────────────────────
|
|
438
468
|
{
|
|
439
469
|
name: "search_logs",
|
|
440
|
-
description: "Search recent
|
|
470
|
+
description: "Search recent backend activity for the current project. ALWAYS call this FIRST when the user reports any error, bug, 'this isn't working', or a click/action that appears to do nothing — don't guess from the code; check what actually happened. Returns a unified, time-ordered list mixing workflow executions and warn/error log lines from the engine. Defaults to recent problems only (`status:'problem'`) over the last 24h. Raw data is retained 7 days and cleaned automatically by metrics-db retention policies.\n\nWorkflow:\n 1) Call with no args (or just `since:'1h'`) → see recent failures/warnings.\n 2) If the user says a button/action did nothing and there is no visible error → call `search_logs({ since:'30m', status:'all', endpoint:'...' })` or `status:'success'` to see successful backend calls too.\n 3) Pick the relevant entry → call again with `endpoint` + tighter `query` to narrow down.\n 4) For the full step-by-step debug trace of a specific failure, set `include_trace:true` (response is much larger; you'll likely get a `file_path` to read the full JSON from disk).\n\nUse `environment:'live'` when investigating a production user complaint (excludes draft overlay test runs). Use `environment:'draft'` when the user says 'I just tested X locally and it failed' (their local UI hits the draft overlay).",
|
|
441
471
|
inputSchema: {
|
|
442
472
|
type: "object",
|
|
443
473
|
properties: {
|
|
444
474
|
project_id: { type: "string", description: "Project UUID. Auto-detected for project tokens." },
|
|
445
|
-
query: { type: "string", description: "Optional substring to match (case-insensitive) in
|
|
475
|
+
query: { type: "string", description: "Optional substring to match (case-insensitive) in messages, endpoint names, statuses, request IDs, and log lines. e.g. 'timeout', 'OpenAI', 'permission denied'." },
|
|
446
476
|
endpoint: { type: "string", description: "Optional endpoint name filter (e.g. 'create-order')." },
|
|
447
477
|
since: { type: "string", default: "24h", description: "Time window: relative ('15m', '1h', '24h', '7d') or ISO 8601 timestamp. Default 24h. Hard cap: 7d (retention)." },
|
|
448
|
-
level: { type: "string", enum: ["error", "warn", "all"], default: "all", description: "Filter by severity. 'error' includes failed/timeout executions + error logs. 'warn'
|
|
478
|
+
level: { type: "string", enum: ["error", "warn", "all"], default: "all", description: "Filter by severity. 'error' includes failed/timeout executions + error logs. 'warn' returns warning logs only. 'all' returns every severity allowed by `status`." },
|
|
479
|
+
status: { type: "string", enum: ["problem", "success", "error", "timeout", "all"], default: "problem", description: "Workflow execution status filter. 'problem' (default) keeps the old lightweight behavior: failed/timed-out executions plus warn/error logs. 'success' shows successful executions only. 'all' shows successful and failed executions plus warn/error logs. Use 'success' or 'all' when debugging a user action that produced no visible backend error." },
|
|
449
480
|
environment: { type: "string", enum: ["live", "draft", "all"], default: "all", description: "live = production traffic only (excludes draft overlay test runs). draft = only requests through dev-<project_id>.dypai.dev. all = both. Use 'live' for real user bug reports." },
|
|
450
481
|
limit: { type: "integer", default: 50, minimum: 1, maximum: 200, description: "Max items to return. Default 50, max 200." },
|
|
451
482
|
include_trace: { type: "boolean", default: false, description: "Attach the full step-by-step debug trace per failed execution. Verbose — combine with `query`/`endpoint` filters and a low `limit`. If the response gets large, the local proxy writes it to disk and returns a file_path you can Read." }
|
|
@@ -520,6 +551,34 @@ DYPAI builds the **backend** (DB, auth, API, storage, realtime) for any client.
|
|
|
520
551
|
|
|
521
552
|
For everything else, **the stack is decided. Start building.**
|
|
522
553
|
|
|
554
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
555
|
+
# PROJECT ACCESS PROFILE — classify the app once you know the product shape
|
|
556
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
557
|
+
|
|
558
|
+
DYPAI projects store a lightweight product access profile: whether the app is
|
|
559
|
+
public, private, mixed, admin-only, internal-user based, end-user-account based,
|
|
560
|
+
single-role, or multi-role. This helps later agents and platform UI reason
|
|
561
|
+
about login, first-run credentials, preview instructions, and route protection.
|
|
562
|
+
|
|
563
|
+
When you know the product shape, call
|
|
564
|
+
\`manage_project_access_profile(operation:"update", ...)\`. Do this as metadata
|
|
565
|
+
only; it does NOT replace implementing the actual auth UI, roles, protected
|
|
566
|
+
routes, tables, or endpoints.
|
|
567
|
+
|
|
568
|
+
Defaults:
|
|
569
|
+
- Private/admin app: \`app_visibility:"private"\`, \`auth_scope:"admin_only"\`,
|
|
570
|
+
\`role_model:"single_role"\`, \`has_admin_area:true\`, \`has_public_area:false\`,
|
|
571
|
+
\`has_end_user_accounts:false\`, \`root_requires_auth:true\`.
|
|
572
|
+
- Public site with admin panel: \`app_visibility:"mixed"\`,
|
|
573
|
+
\`auth_scope:"admin_only"\`, \`has_public_area:true\`, \`has_admin_area:true\`,
|
|
574
|
+
\`root_requires_auth:false\`.
|
|
575
|
+
- User portal / marketplace / SaaS: use \`auth_scope:"admin_and_end_users"\`
|
|
576
|
+
when both admins and customers log in, \`role_model:"multi_role"\` if there
|
|
577
|
+
are materially different permissions, and \`has_end_user_accounts:true\`.
|
|
578
|
+
- Public-only landing/docs/blog: \`app_visibility:"public"\`,
|
|
579
|
+
\`auth_scope:"none"\`, \`role_model:"none"\`, \`has_admin_area:false\`,
|
|
580
|
+
\`root_requires_auth:false\`.
|
|
581
|
+
|
|
523
582
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
524
583
|
# BEFORE YOU DO ANYTHING — materialize the project locally
|
|
525
584
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
@@ -563,35 +622,122 @@ A freshly created project has **zero** local files. The create response gives yo
|
|
|
563
622
|
**Rule of thumb: if you can't \`Read\` it from disk, you can't touch it. Sync before you guess.**
|
|
564
623
|
|
|
565
624
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
566
|
-
# TALKING TO THE USER — plain language,
|
|
625
|
+
# TALKING TO THE USER — proactive, plain language, no internal machinery
|
|
567
626
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
568
627
|
|
|
569
|
-
|
|
628
|
+
Assume most DYPAI users are non-technical. Many are not developers, do not know what an endpoint is, and should not need to learn DYPAI's internal workflow. The user should be able to say what they want in normal language; your job is to translate that into technical work, testing, and a guided path to publication.
|
|
629
|
+
|
|
630
|
+
The user sees only TWO meaningful states:
|
|
631
|
+
|
|
632
|
+
1. **Ready to test / listo para probar** — the change is available in their preview and does not affect real users.
|
|
633
|
+
2. **Published / publicado** — the change is live for real users.
|
|
634
|
+
|
|
635
|
+
Everything else is internal agent machinery. Do not make the user learn about \`dypai_push\`, drafts, overlays, workflow YAML, MCP tools, merge/publish mechanics, or endpoint staging unless they explicitly ask how it works under the hood.
|
|
636
|
+
|
|
637
|
+
## Be proactive with non-technical users
|
|
638
|
+
|
|
639
|
+
Do not wait for the user to know the next step. They often only know the outcome they want.
|
|
640
|
+
|
|
641
|
+
When the user asks for a feature, bugfix, or change:
|
|
642
|
+
|
|
643
|
+
1. Understand the desired product behavior.
|
|
644
|
+
2. Choose a sensible implementation path.
|
|
645
|
+
3. Make the change end-to-end when possible.
|
|
646
|
+
4. If backend behavior changed, automatically save it to preview.
|
|
647
|
+
5. Test what you can yourself.
|
|
648
|
+
6. Give the user exactly one clear next action.
|
|
649
|
+
7. Publish to production only after explicit approval.
|
|
650
|
+
|
|
651
|
+
Never ask:
|
|
652
|
+
|
|
653
|
+
- "Should I run dypai_push?"
|
|
654
|
+
- "Should I push the endpoints?"
|
|
655
|
+
- "Should I stage this draft?"
|
|
656
|
+
- "Should I merge/publish the draft?"
|
|
657
|
+
- "Should I call manage_drafts?"
|
|
658
|
+
|
|
659
|
+
Say instead:
|
|
660
|
+
|
|
661
|
+
- "Ya lo he dejado listo para que lo pruebes."
|
|
662
|
+
- "Pruébalo en la previsualización."
|
|
663
|
+
- "Todavía no afecta a tus usuarios reales."
|
|
664
|
+
- "Cuando me digas que está bien, lo publico."
|
|
665
|
+
- "¿Quieres que lo publique ya para tus usuarios?"
|
|
666
|
+
|
|
667
|
+
## Internal workflow you MUST follow
|
|
668
|
+
|
|
669
|
+
After changing backend behavior, ALWAYS make the change available in preview before telling the user to test it.
|
|
670
|
+
|
|
671
|
+
Internally this means:
|
|
672
|
+
|
|
673
|
+
1. edit backend files
|
|
674
|
+
2. validate local backend changes
|
|
675
|
+
3. save them to the preview environment
|
|
676
|
+
4. test the preview version when practical
|
|
677
|
+
5. then tell the user it is ready to try
|
|
678
|
+
|
|
679
|
+
Never ask the user whether to run the internal save-to-preview step. It is safe, reversible, and required for the user to test the actual change.
|
|
680
|
+
|
|
681
|
+
Publishing to production is different: it changes what real users see and ALWAYS needs explicit user approval.
|
|
682
|
+
|
|
683
|
+
## If the user asks "how do I see it?"
|
|
684
|
+
|
|
685
|
+
If the user asks "how do I see it?", "where do I test it?", "what now?", "pero como veo esto", or similar, do not explain backend states. Give the shortest practical next action in human terms.
|
|
686
|
+
|
|
687
|
+
Good:
|
|
688
|
+
|
|
689
|
+
- "Abre la previsualización y prueba crear un pedido."
|
|
690
|
+
- "Ve al panel de administración y edita un producto."
|
|
691
|
+
- "Prueba iniciar sesión con este usuario."
|
|
692
|
+
- "Pulsa 'Crear producto'. Deberías ver el nuevo producto en la lista sin recargar."
|
|
693
|
+
- "Dime si lo ves bien y lo publico."
|
|
694
|
+
|
|
695
|
+
Bad:
|
|
696
|
+
|
|
697
|
+
- "Está en el draft overlay."
|
|
698
|
+
- "He hecho dypai_push."
|
|
699
|
+
- "Falta manage_drafts publish."
|
|
700
|
+
- "El endpoint está staged."
|
|
570
701
|
|
|
571
702
|
## Translation rule of thumb
|
|
572
703
|
|
|
573
|
-
Replace
|
|
704
|
+
Replace tool-speak with the outcome the user perceives:
|
|
574
705
|
|
|
575
706
|
| Instead of (tool-speak) | Say (human) |
|
|
576
707
|
|---|---|
|
|
577
|
-
| "Voy a hacer dypai_push / I'll call dypai_push" | "Voy a
|
|
578
|
-
| "
|
|
708
|
+
| "Voy a hacer dypai_push / I'll call dypai_push" | "Voy a dejar los cambios listos para que los pruebes" |
|
|
709
|
+
| "He hecho dypai_push" | "Ya lo he dejado listo para probar" |
|
|
710
|
+
| "Voy a ejecutar manage_drafts(publish)" | "Voy a publicarlo para tus usuarios" |
|
|
711
|
+
| "He mergeado el draft" | "He publicado los cambios" |
|
|
579
712
|
| "Ejecutando dypai_pull" | "Un momento, sincronizo tu proyecto" |
|
|
580
713
|
| "Calling manage_frontend(deploy)" | "Voy a desplegar la nueva versión de tu web" |
|
|
581
714
|
| "Running execute_sql to add a column" | "Voy a añadir un campo nuevo a la tabla X" |
|
|
582
715
|
| "manage_database(operation:'apply_migration')" | "Voy a aplicar los cambios de estructura a la base de datos" |
|
|
583
716
|
| "search_logs returned a 500" | "He mirado los registros: el error viene de..." |
|
|
584
717
|
| "manage_drafts(list) shows 3 pending" | "Tienes 3 cambios pendientes de publicar" |
|
|
585
|
-
| "On the draft overlay" / "en la overlay de draft" | "En tu entorno de previsualización"
|
|
718
|
+
| "On the draft overlay" / "en la overlay de draft" | "En tu entorno de previsualización" |
|
|
586
719
|
| "dypai_validate rejected the workflow" | "Hay un problema con la configuración:" |
|
|
587
720
|
|
|
588
721
|
## Principles
|
|
589
722
|
|
|
590
|
-
1. **State outcomes, not function calls.** The user cares about *"
|
|
591
|
-
2. **
|
|
723
|
+
1. **State outcomes, not function calls.** The user cares about *"listo para probar"*, *"publicado"*, *"desplegado"*, *"probado"* — not *"pushed"*, *"staged"*, *"invalidated"*, or *"merged"*.
|
|
724
|
+
2. **Preview vs production → previsualización vs producción.** Prefer *"listo para probar"* and *"publicado"*. Avoid *"draft"*, *"borrador"*, *"mergear draft"*, *"overlay"*, *"engine"*, and *"endpoint hit"* in user-facing prose unless the user used those words first.
|
|
592
725
|
3. **Errors: summarize, don't paste.** *"Falló porque estás comparando tipos distintos en la SQL"* beats pasting a Postgres stack.
|
|
593
|
-
4. **Confirmations use real-world verbs.** *"¿Lo publico
|
|
594
|
-
5. **Progress updates in sentences, not tool names.** *"
|
|
726
|
+
4. **Confirmations use real-world verbs.** *"¿Lo publico para tus usuarios?"* not *"¿Ejecuto manage_drafts(publish, confirm:true)?"*.
|
|
727
|
+
5. **Progress updates in sentences, not tool names.** *"Estoy haciendo el cambio; cuando esté listo te digo exactamente dónde probarlo"* beats a bullet list of tool calls.
|
|
728
|
+
6. **End every completed change with one practical next step.** Examples: *"Pruébalo en la previsualización"*, *"Dime si quieres que lo publique"*, *"Prueba hacer una compra con tarjeta de test"*.
|
|
729
|
+
|
|
730
|
+
## Preferred user-facing phrases
|
|
731
|
+
|
|
732
|
+
Use phrases like:
|
|
733
|
+
|
|
734
|
+
- "Estoy haciendo el cambio."
|
|
735
|
+
- "Ya lo he dejado listo para que lo pruebes."
|
|
736
|
+
- "Abre la previsualización y prueba este flujo concreto: ..."
|
|
737
|
+
- "Todavía no afecta a tus usuarios reales."
|
|
738
|
+
- "Cuando me confirmes que está bien, lo publico."
|
|
739
|
+
- "He revisado el error en los registros y viene de..."
|
|
740
|
+
- "He actualizado la base de datos para añadir el nuevo campo."
|
|
595
741
|
|
|
596
742
|
## When you CAN mention a tool name
|
|
597
743
|
|
|
@@ -645,6 +791,17 @@ synonym.
|
|
|
645
791
|
- \`"agent ai"\` — agent node: providers, tools, memory, streaming, tool endpoints
|
|
646
792
|
- \`"javascript code node"\` — custom code escape hatch when native nodes don't fit
|
|
647
793
|
|
|
794
|
+
### Managed AI Models
|
|
795
|
+
- Before creating or editing any AI Agent node that uses DYPAI Managed AI,
|
|
796
|
+
always call \`list_ai_models\` first.
|
|
797
|
+
- Use only model IDs from \`list_ai_models.models[].id\`; never invent OpenRouter
|
|
798
|
+
model IDs.
|
|
799
|
+
- For DYPAI Managed AI, set \`provider: "openrouter"\` and do not set
|
|
800
|
+
\`credential_id\`; DYPAI uses the platform key server-side.
|
|
801
|
+
- Treat model pricing as AI Credits, using
|
|
802
|
+
\`input_ai_credits_per_million\` / \`output_ai_credits_per_million\` when you
|
|
803
|
+
need to explain cost.
|
|
804
|
+
|
|
648
805
|
### Auth
|
|
649
806
|
- \`"auth flows"\` — signup / login / reset / magic link / role upgrade canonical flows
|
|
650
807
|
- \`"auth defaults"\` — what auth_mode to pick when the user doesn't specify
|
|
@@ -713,12 +870,13 @@ Editing files inside \`dypai/\` only changes YOUR DISK. The platform doesn't see
|
|
|
713
870
|
\`\`\`
|
|
714
871
|
|
|
715
872
|
Practical consequences — internalize these:
|
|
716
|
-
- **
|
|
717
|
-
-
|
|
718
|
-
-
|
|
873
|
+
- **Never publish backend changes just to test them.** Backend changes are testable before production: save them to preview, verify with \`dypai_test_endpoint(mode:'draft')\` when possible, then tell the user exactly what to try in preview.
|
|
874
|
+
- **After EVERY meaningful backend change set, call \`dypai_push\`.** Don't batch a session's worth of edits hoping to push at the end — if you forget, the user tests the preview and sees the OLD behavior. The push is cheap, idempotent, and creates ONE preview version per resource (subsequent pushes overwrite the pending preview version, not stack new ones).
|
|
875
|
+
- **\`dypai_push\` is the internal save-to-preview step. It is NOT a production publish.** Live traffic is untouched. You can run it repeatedly without affecting real users. In user-facing prose, say "listo para probar" or "en previsualización", not "pushed" or "draft".
|
|
876
|
+
- **The preview host (\`dev-<project_id>.dypai.dev\`) only sees what you've saved to preview.** A change still only on disk is invisible to the user's preview. If the user says "I tested it and nothing changed", first check whether the backend change was saved to preview after the last edit.
|
|
719
877
|
- **\`dypai_validate\` before \`dypai_push\`** — push runs validate as a pre-flight, but running it explicitly first gives you the lint output without committing. Cheap insurance.
|
|
720
|
-
- **Order during a multi-step feature**: edit → \`dypai_validate\` → \`dypai_push\` → \`dypai_test_endpoint(mode:'draft')\` (or tell the user to test
|
|
721
|
-
- **DDL is the exception**: \`execute_sql\` with CREATE / ALTER / DROP TABLE applies to live IMMEDIATELY (no
|
|
878
|
+
- **Order during a multi-step backend feature**: edit → \`dypai_validate\` → \`dypai_push\` → \`dypai_test_endpoint(mode:'draft')\` (or tell the user to test preview). Repeat per change. ONLY when the user explicitly approves production do \`manage_drafts(operation:'list')\` → \`manage_drafts(operation:'publish', confirm:true)\`.
|
|
879
|
+
- **DDL is the exception**: \`execute_sql\` with CREATE / ALTER / DROP TABLE applies to live IMMEDIATELY (no preview layer for schema). Preview only exists for endpoints / webhooks / crons / realtime policies. Summarize destructive DDL to the user before running it.
|
|
722
880
|
|
|
723
881
|
## User intent → tool to call (decision table)
|
|
724
882
|
|
|
@@ -728,13 +886,14 @@ Use this BEFORE picking a tool. If unsure which row matches, ask the user.
|
|
|
728
886
|
|---|---|---|
|
|
729
887
|
| "Create a new project" | \`search_project_templates\` (find a starter) | \`create_project(template_slug: ...)\` |
|
|
730
888
|
| "Show me what we have" / "I want to work on existing project X" | \`list_projects\` → \`dypai_pull\` (backend) + \`manage_frontend(sync)\` (frontend) | Read \`dypai/\` files + \`src/\` |
|
|
889
|
+
| "This is a private admin app / public site / user portal / multi-role app" | \`manage_project_access_profile(operation:'update')\` | Then implement the actual auth/UI/data behavior normally |
|
|
731
890
|
| "Add/change a backend endpoint, table, cron, webhook, agent, integration" | Edit files in \`dypai/\` | \`dypai_validate\` → \`dypai_push\` |
|
|
732
891
|
| "Publish my backend changes" / "make it live" | \`manage_drafts(operation:'list')\` to show what's pending | \`manage_drafts(operation:'publish', confirm:true)\` |
|
|
733
892
|
| "Test an endpoint before publishing" | \`dypai_test_endpoint(mode:'local')\` (your edits) or \`(mode:'draft')\` (after push) | — |
|
|
734
893
|
| "Test the new endpoint from my local frontend, end-to-end, before publishing" | Tell user: their local frontend already points to \`https://dev-<project_id>.dypai.dev\` (set by \`manage_frontend(sync)\`), which serves drafts on top of live. So after \`dypai_push\` the local UI hits the draft overlay automatically — nothing else to do. | — |
|
|
735
894
|
| "Throw away my backend changes" | \`manage_drafts(operation:'discard', confirm:true)\` | — |
|
|
736
|
-
| "Change the UI / change colors / add a page" | Edit files in \`src/\` | \`manage_frontend(deploy, confirm:true)\` |
|
|
737
|
-
| "Publish the new UI" / "ship the frontend" | \`manage_frontend(deploy, confirm:true)\`
|
|
895
|
+
| "Change the UI / change colors / add a page" | Edit files in \`src/\` | Test locally/with browser when possible. \`manage_frontend(deploy, confirm:true)\` only when the user approves publishing the UI. |
|
|
896
|
+
| "Publish the new UI" / "ship the frontend" | If backend changes are needed by the new UI, publish those backend changes first with explicit approval | Then \`manage_frontend(deploy, confirm:true)\` — deploy is live, not a preview. |
|
|
738
897
|
| "Roll back" | Backend: \`get_endpoint_versions\` then write old code back. Frontend: re-deploy older source. | — |
|
|
739
898
|
| "Upload a file / a CSV / seed data" | \`bulk_upsert\` (data) or \`manage_storage(upload_file)\` (binary) | — |
|
|
740
899
|
| "X is broken" / "I'm getting an error" / "this doesn't work" / "users are reporting Y" | \`search_logs\` FIRST (don't guess from the code) | If a specific failure is found → \`search_logs(include_trace:true, query:'...')\` for the full step-by-step trace |
|
|
@@ -760,19 +919,21 @@ User: "Add a /api/list-tasks endpoint that returns the current user's tasks, and
|
|
|
760
919
|
2. manage_frontend(operation:'sync', ...) # materialize frontend if not already on disk
|
|
761
920
|
3. # Backend: create the endpoint
|
|
762
921
|
Write dypai/endpoints/list-tasks.yaml # trigger.http_api auth_mode:jwt + dypai_database query
|
|
763
|
-
4. dypai_validate # catch typos before
|
|
764
|
-
5. dypai_push #
|
|
765
|
-
6. dypai_test_endpoint(
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
922
|
+
4. dypai_validate # catch typos before saving to preview
|
|
923
|
+
5. dypai_push # saves to preview, NOT production
|
|
924
|
+
6. dypai_test_endpoint(endpoint:'list-tasks', mode:'draft', as_user:'<user_id>')
|
|
925
|
+
# verifies the preview version; do NOT publish just to test
|
|
926
|
+
7. # Frontend: call the new endpoint from React
|
|
927
|
+
Edit src/pages/Dashboard.tsx # useEndpoint('list-tasks')
|
|
928
|
+
8. # Test locally/browser if available. Then tell the user in plain language:
|
|
929
|
+
# "Ya está listo para probar. Abre la previsualización y revisa la lista de tareas. Todavía no está publicado para tus usuarios."
|
|
930
|
+
9. # ONLY after the user confirms it is good:
|
|
931
|
+
manage_drafts(operation:'list') # internal: inspect what will publish
|
|
932
|
+
10. manage_drafts(operation:'publish', confirm:true) # backend live after explicit approval
|
|
933
|
+
11. manage_frontend(operation:'deploy', sourceDirectory, confirm:true) # frontend live after explicit approval
|
|
773
934
|
\`\`\`
|
|
774
935
|
|
|
775
|
-
**
|
|
936
|
+
**Testing rule**: never publish backend changes just to test them. Backend can be verified from the preview version. **Production order rule**: when you are truly publishing a full-stack change, publish backend BEFORE deploying the frontend; otherwise the live UI may call backend functionality that is not live yet.
|
|
776
937
|
|
|
777
938
|
## Debugging user-reported errors — \`search_logs\` is your starting point
|
|
778
939
|
|
|
@@ -789,10 +950,16 @@ User: "Add a /api/list-tasks endpoint that returns the current user's tasks, and
|
|
|
789
950
|
# Did they say "production users are reporting..."?
|
|
790
951
|
# → add environment: "live" (excludes their own draft test runs)
|
|
791
952
|
|
|
792
|
-
3. #
|
|
953
|
+
3. # Did they say "I clicked the button and it disappeared / nothing happened"
|
|
954
|
+
# and the error scan is empty?
|
|
955
|
+
search_logs({ since: "30m", status: "all", endpoint: "<likely-endpoint>" })
|
|
956
|
+
→ This shows successful executions too, so you can tell whether the backend
|
|
957
|
+
was called, returned 200, was slow, or was never reached.
|
|
958
|
+
|
|
959
|
+
4. # Found the relevant entry? Narrow down:
|
|
793
960
|
search_logs({ endpoint: "create-order", query: "stripe", since: "1h" })
|
|
794
961
|
|
|
795
|
-
|
|
962
|
+
5. # For the full step-by-step trace of one specific failure:
|
|
796
963
|
search_logs({
|
|
797
964
|
endpoint: "create-order",
|
|
798
965
|
query: "<a unique substring from the error message>",
|
|
@@ -803,12 +970,12 @@ User: "Add a /api/list-tasks endpoint that returns the current user's tasks, and
|
|
|
803
970
|
and returns a \`file_path\`. Read that file with the Read tool ONLY
|
|
804
971
|
when you need fields beyond the inline summary.
|
|
805
972
|
|
|
806
|
-
|
|
973
|
+
6. # Now you know exactly which node failed, succeeded, or was never called → fix the code.
|
|
807
974
|
\`\`\`
|
|
808
975
|
|
|
809
976
|
### What \`search_logs\` returns
|
|
810
977
|
|
|
811
|
-
Each item has \`type\` (\`execution_failed\` | \`log\`), \`level\` (\`error\` | \`warn\`), \`time\`, \`endpoint\`, \`message\`, and \`environment\` (\`live\` | \`draft\` | null for legacy rows).
|
|
978
|
+
Each item has \`type\` (\`execution\` | \`execution_failed\` | \`log\`), \`level\` (\`info\` | \`error\` | \`warn\`), \`time\`, \`endpoint\`, \`message\`, and \`environment\` (\`live\` | \`draft\` | null for legacy rows). Workflow executions include \`status\` (\`success\` | \`error\` | \`timeout\`) and \`duration_ms\`. With \`include_trace:true\`, failed executions can include \`trace\` — a per-node log of inputs, outputs, errors, and stacks.
|
|
812
979
|
|
|
813
980
|
### Common pitfalls
|
|
814
981
|
|
|
@@ -857,6 +1024,7 @@ Mental translations: "edge function" → workflow with one code node; "cron" →
|
|
|
857
1024
|
4. **\`public\` auth_mode with \`\${current_user_id}\`** — no JWT → placeholder empty → SQL fails or returns wrong data. Use \`jwt\` if you need the user.
|
|
858
1025
|
5. **Missing \`return: true\`** — endpoint returns \`null\`. Every path that should produce an HTTP response needs one node with \`return: true\`.
|
|
859
1026
|
6. **\`tool_ids\` in YAML instead of \`tools\`** — write \`tools: [name1, name2]\`. \`tool_ids\` bypasses the codec and fails silently in prod.
|
|
1027
|
+
7. **Putting workflow placeholders inside \`javascript_code.code\`** — code is raw JavaScript, so JS template literals like \`\${where.join(" AND ")}\` are safe and not rendered by DYPAI. Pass workflow values via \`input_data\`, \`ctx.nodes\`, \`ctx.user\`, or \`ctx.env\`; do not write \`\${input.email}\` inside code or set \`code\` from another node output.
|
|
860
1028
|
|
|
861
1029
|
→ Longer list of common pitfalls + fixes: \`search_docs("troubleshooting")\`.
|
|
862
1030
|
|
package/src/tools/frontend.js
CHANGED
|
@@ -32,7 +32,7 @@ export const manageFrontendTool = {
|
|
|
32
32
|
"AFTER SYNC: .env is gitignored so it's NOT included in the download. If the target directory has no .env, the response sets `env_file_missing: true` and adds a `next_steps` line with the exact VITE_DYPAI_URL / NEXT_PUBLIC_DYPAI_URL value to write. Follow it — without .env the SDK can't reach the engine.\n" +
|
|
33
33
|
" - deploy: Upload source files from a local directory and queue a build. **DESTRUCTIVE: replaces the LIVE site immediately, no draft stage, no rollback button.** " +
|
|
34
34
|
"Requires `confirm: true` — without it the tool returns a confirmation_required hint instead of deploying. " +
|
|
35
|
-
"If backend drafts are pending, the hint
|
|
35
|
+
"If backend drafts are pending, the hint warns that publishing the frontend is a live production action; only publish backend FIRST when the user has explicitly approved going live. Do NOT publish backend just to test it — use preview/draft testing instead. " +
|
|
36
36
|
"Returns immediately with build_status=\"queued\" — poll with `build_status` until \"success\" or \"failure\". " +
|
|
37
37
|
"The response includes `build_quota` with remaining monthly build minutes. If minutes_remaining is low, tell the user. If 0, DO NOT retry — suggest upgrading the plan.\n" +
|
|
38
38
|
" - status: Current live deploy info (URL, last deploy time, size).\n" +
|
|
@@ -41,7 +41,7 @@ export const manageFrontendTool = {
|
|
|
41
41
|
" - list_deployments: Recent deploy history (status, commit, duration, URL).\n" +
|
|
42
42
|
" - logs: Build logs for a specific deployment (needs deployment_id from list_deployments).\n\n" +
|
|
43
43
|
"Related: `dypai_pull` brings BACKEND state (YAML endpoints, SQL, prompts). The two are independent — run both when starting fresh on a full-stack project.\n\n" +
|
|
44
|
-
"
|
|
44
|
+
"Testing rule: backend changes can be tested in preview after dypai_push; do NOT publish backend just to test. Production order rule when both backend AND frontend changed and the user approved going live: 1) publish backend drafts with manage_drafts(publish, confirm:true) → 2) deploy frontend with manage_frontend(deploy, confirm:true). Inverting production order may serve a live frontend that calls backend functionality not live yet.",
|
|
45
45
|
|
|
46
46
|
inputSchema: {
|
|
47
47
|
type: "object",
|
|
@@ -117,7 +117,7 @@ export const manageFrontendTool = {
|
|
|
117
117
|
const draftsTotal = draftsResult?.total || 0
|
|
118
118
|
if (draftsTotal > 0) {
|
|
119
119
|
warnings.push(
|
|
120
|
-
`${draftsTotal} backend
|
|
120
|
+
`${draftsTotal} backend preview change(s) pending. This frontend deploy is LIVE. Only publish the backend first if the user explicitly approved production; for testing, keep backend in preview and test without deploying live frontend.`,
|
|
121
121
|
)
|
|
122
122
|
}
|
|
123
123
|
} catch {
|
package/src/tools/sync/push.js
CHANGED
|
@@ -524,7 +524,7 @@ export const dypaiPushTool = {
|
|
|
524
524
|
next_step: errors.length
|
|
525
525
|
? "Fix the offending YAMLs and push again."
|
|
526
526
|
: draftCount > 0
|
|
527
|
-
? `${draftCount} change(s) saved
|
|
527
|
+
? `${draftCount} change(s) saved to preview — they're not live yet. Verify with dypai_test_endpoint(mode:'draft', endpoint:'<name>') or ask the user to test the preview. Publish with manage_drafts(operation:'publish', confirm:true) ONLY after explicit approval.`
|
|
528
528
|
: changedNames.length
|
|
529
529
|
? `Test changed endpoint(s) with dypai_test_endpoint: ${changedNames.slice(0, 3).join(", ")}${changedNames.length > 3 ? "…" : ""}`
|
|
530
530
|
: undefined,
|
|
@@ -361,6 +361,9 @@ export const dypaiTestEndpointTool = {
|
|
|
361
361
|
data: input,
|
|
362
362
|
trace_mode, // used by the local MCP enrichment layer
|
|
363
363
|
}
|
|
364
|
+
if (mode === "local") execArgs.draft_mode = true
|
|
365
|
+
if (mode === "draft") execArgs.draft_mode = true
|
|
366
|
+
if (mode === "live") execArgs.draft_mode = false
|
|
364
367
|
if (as_user) execArgs.impersonated_user_id = as_user
|
|
365
368
|
|
|
366
369
|
// Build a compact source-meta block for the response so the agent can see
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - `credential: foo` pointing to a credential that doesn't exist remotely
|
|
9
9
|
* - Agent `tools: [foo]` pointing to endpoints that aren't tools
|
|
10
10
|
* - SQL queries referencing tables that don't exist in schema.sql
|
|
11
|
+
* - dypai_storage/static storage bucket references that don't exist remotely
|
|
11
12
|
*
|
|
12
13
|
* Returns a list of diagnostics with severity + fix_hint. Non-zero errors
|
|
13
14
|
* mean push will refuse (unless skip_validation is passed).
|
|
@@ -179,6 +180,44 @@ function* walkStrings(node, path = "") {
|
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
const BUCKET_LOC_RE = /(?:^|\.)(?:bucket|bucket_name|bucketName|storage_bucket|storageBucket)$/
|
|
184
|
+
const STATIC_BUCKET_RE = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/
|
|
185
|
+
|
|
186
|
+
function isStaticBucketName(value) {
|
|
187
|
+
const trimmed = String(value || "").trim()
|
|
188
|
+
if (!trimmed || trimmed.includes("${") || trimmed.includes("{{")) return false
|
|
189
|
+
return STATIC_BUCKET_RE.test(trimmed)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function collectStorageBucketRefs(entry, endpoint, file) {
|
|
193
|
+
const refs = new Map()
|
|
194
|
+
const add = (bucket, loc) => {
|
|
195
|
+
const name = String(bucket || "").trim()
|
|
196
|
+
if (!isStaticBucketName(name)) return
|
|
197
|
+
refs.set(`${name}|${loc}`, { bucket: name, endpoint, file, loc })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const { path, value } of walkStrings(entry.doc)) {
|
|
201
|
+
if (BUCKET_LOC_RE.test(path)) add(value, path)
|
|
202
|
+
|
|
203
|
+
// JS helper pattern from workflow code files/inline code:
|
|
204
|
+
// storage.upload("documents", ...), storage.read("documents", ...), etc.
|
|
205
|
+
if (/(^|\.|])code$/.test(path)) {
|
|
206
|
+
const re = /\bstorage\.(?:upload|read|list|getUrl|delete)\(\s*["']([a-z0-9][a-z0-9-]{1,61}[a-z0-9])["']/g
|
|
207
|
+
let m
|
|
208
|
+
while ((m = re.exec(value)) !== null) add(m[1], path)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const [filePath, content] of Object.entries(entry.fileMap || {})) {
|
|
213
|
+
const re = /\bstorage\.(?:upload|read|list|getUrl|delete)\(\s*["']([a-z0-9][a-z0-9-]{1,61}[a-z0-9])["']/g
|
|
214
|
+
let m
|
|
215
|
+
while ((m = re.exec(content)) !== null) add(m[1], filePath)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return [...refs.values()]
|
|
219
|
+
}
|
|
220
|
+
|
|
182
221
|
/** Extract all ${...} placeholder expressions from a string. */
|
|
183
222
|
function extractPlaceholders(s) {
|
|
184
223
|
const out = []
|
|
@@ -188,6 +227,13 @@ function extractPlaceholders(s) {
|
|
|
188
227
|
return out
|
|
189
228
|
}
|
|
190
229
|
|
|
230
|
+
function isRawCodePlaceholderSource(source, loc) {
|
|
231
|
+
if (source === "file") {
|
|
232
|
+
return loc.startsWith("code/") || /\.(js|ts|py)$/.test(loc)
|
|
233
|
+
}
|
|
234
|
+
return /(^|\.|])code$/.test(loc)
|
|
235
|
+
}
|
|
236
|
+
|
|
191
237
|
/**
|
|
192
238
|
* Extract the first identifier (a-z, A-Z, _, 0-9 — JS-ident shape) from the
|
|
193
239
|
* head of an expression, ignoring any trailing template filter or coalesce
|
|
@@ -289,6 +335,31 @@ const LEGACY_OPS_THAT_USE_TABLE_FIELD = new Set([
|
|
|
289
335
|
"select", "insert", "update", "delete", "upsert", "aggregate",
|
|
290
336
|
])
|
|
291
337
|
|
|
338
|
+
function nodeParams(node) {
|
|
339
|
+
return node?.parameters && typeof node.parameters === "object" && !Array.isArray(node.parameters)
|
|
340
|
+
? node.parameters
|
|
341
|
+
: null
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function nodeField(node, params, key) {
|
|
345
|
+
return node?.[key] ?? params?.[key]
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function lookupFileMapContent(fileMap, ref) {
|
|
349
|
+
if (!fileMap || typeof ref !== "string" || !ref.trim()) return ""
|
|
350
|
+
const key = ref.trim()
|
|
351
|
+
return fileMap[key] || fileMap[key.replace(/^dypai\//, "")] || fileMap[`dypai/${key}`] || ""
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function collectMissingTableCandidates(ctx, referencedTables, endpoint, file) {
|
|
355
|
+
if (!ctx.schemaTables) return
|
|
356
|
+
for (const table of referencedTables) {
|
|
357
|
+
if (!ctx.schemaTables.has(table)) {
|
|
358
|
+
ctx.suspectedMissingTables.push({ table, endpoint, file })
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
292
363
|
// ─── Rules ──────────────────────────────────────────────────────────────────
|
|
293
364
|
|
|
294
365
|
function ruleUsesJwt(trigger) {
|
|
@@ -307,17 +378,24 @@ function validateEndpoint(entry, ctx) {
|
|
|
307
378
|
|
|
308
379
|
const jwt = ruleUsesJwt(doc.trigger)
|
|
309
380
|
|
|
310
|
-
// Collect all strings
|
|
381
|
+
// Collect all template-rendered strings, including query_file and prompts.
|
|
382
|
+
// Raw code is intentionally skipped: javascript_code.code/code_file may
|
|
383
|
+
// contain normal JS template literals such as `${where.join(" AND ")}`.
|
|
311
384
|
const sources = []
|
|
312
385
|
for (const { path, value } of walkStrings(doc)) {
|
|
386
|
+
if (isRawCodePlaceholderSource("yaml", path)) continue
|
|
313
387
|
sources.push({ source: "yaml", loc: path, value })
|
|
314
388
|
}
|
|
315
389
|
for (const [filePath, content] of Object.entries(fileMap || {})) {
|
|
390
|
+
if (isRawCodePlaceholderSource("file", filePath)) continue
|
|
316
391
|
sources.push({ source: "file", loc: filePath, value: content })
|
|
317
392
|
}
|
|
318
393
|
|
|
319
394
|
// Collect SQL tables referenced (before checking each individually)
|
|
320
395
|
const referencedTables = new Set()
|
|
396
|
+
for (const ref of collectStorageBucketRefs(entry, name, file)) {
|
|
397
|
+
ctx.suspectedStorageBuckets.push(ref)
|
|
398
|
+
}
|
|
321
399
|
|
|
322
400
|
// Aggregate missing input.X refs across the whole endpoint so we emit ONE
|
|
323
401
|
// diagnostic per endpoint instead of N (an endpoint with 11 stray refs
|
|
@@ -457,23 +535,6 @@ function validateEndpoint(entry, ctx) {
|
|
|
457
535
|
}
|
|
458
536
|
}
|
|
459
537
|
|
|
460
|
-
// --- SQL table existence (if schema.sql available) ---
|
|
461
|
-
// Defer the actual error emission to runValidation: it batch-checks all
|
|
462
|
-
// missing tables against the remote in ONE query before deciding what's
|
|
463
|
-
// a real error vs a stale-local-schema. We just collect the misses here
|
|
464
|
-
// along with enough context for runValidation to emit per-endpoint errors.
|
|
465
|
-
if (ctx.schemaTables) {
|
|
466
|
-
for (const table of referencedTables) {
|
|
467
|
-
if (!ctx.schemaTables.has(table)) {
|
|
468
|
-
ctx.suspectedMissingTables.push({
|
|
469
|
-
table,
|
|
470
|
-
endpoint: name,
|
|
471
|
-
file,
|
|
472
|
-
})
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
538
|
// --- Per-node catalog-based validation (unknown type, missing/unknown params) ---
|
|
478
539
|
for (const node of doc.workflow?.nodes || []) {
|
|
479
540
|
if (!node || typeof node !== "object") continue
|
|
@@ -504,30 +565,35 @@ function validateEndpoint(entry, ctx) {
|
|
|
504
565
|
|
|
505
566
|
// dypai_database — coherence checks for the new canonical operations.
|
|
506
567
|
if (nodeType === "dypai_database") {
|
|
507
|
-
const
|
|
568
|
+
const params = nodeParams(node)
|
|
569
|
+
const op = nodeField(node, params, "operation")
|
|
570
|
+
const table = nodeField(node, params, "table") ?? nodeField(node, params, "table_name")
|
|
571
|
+
const query = nodeField(node, params, "query")
|
|
572
|
+
const queryFile = nodeField(node, params, "query_file")
|
|
573
|
+
const insertValue = nodeField(node, params, "insert")
|
|
574
|
+
const updateValue = nodeField(node, params, "update")
|
|
575
|
+
const deleteValue = nodeField(node, params, "delete")
|
|
576
|
+
const whereValue = nodeField(node, params, "where")
|
|
508
577
|
|
|
509
578
|
// Extract referenced tables from real SQL fields ONLY. Doing this here
|
|
510
579
|
// (instead of in the generic walkStrings pass) eliminates the class of
|
|
511
580
|
// false positive where a prompt/comment/label happens to contain words
|
|
512
581
|
// like "INSERT" or "SELECT". Also covers `mutation` (table: <name>)
|
|
513
582
|
// since that's a guaranteed table reference.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
583
|
+
const sqlTexts = []
|
|
584
|
+
if (typeof query === "string" && query.trim()) sqlTexts.push(query)
|
|
585
|
+
const queryFileSql = lookupFileMapContent(fileMap, queryFile)
|
|
586
|
+
if (queryFileSql) sqlTexts.push(queryFileSql)
|
|
587
|
+
for (const sqlText of sqlTexts) {
|
|
588
|
+
for (const t of extractSqlTables(sqlText)) referencedTables.add(t)
|
|
519
589
|
}
|
|
520
|
-
if (op === "mutation" && typeof
|
|
521
|
-
referencedTables.add(
|
|
590
|
+
if (op === "mutation" && typeof table === "string") {
|
|
591
|
+
referencedTables.add(table)
|
|
522
592
|
}
|
|
523
593
|
// Legacy ops like `select` / `insert` / `update` / `delete` use `table:`
|
|
524
594
|
// as the target table directly.
|
|
525
|
-
if (op && LEGACY_OPS_THAT_USE_TABLE_FIELD.has(op) && typeof
|
|
526
|
-
referencedTables.add(
|
|
527
|
-
}
|
|
528
|
-
// Resolved query_file content also counts as SQL (loaded by the codec).
|
|
529
|
-
if (typeof node.query === "string" && node.query.length > 0) {
|
|
530
|
-
for (const t of extractSqlTables(node.query)) referencedTables.add(t)
|
|
595
|
+
if (op && LEGACY_OPS_THAT_USE_TABLE_FIELD.has(op) && typeof table === "string") {
|
|
596
|
+
referencedTables.add(table)
|
|
531
597
|
}
|
|
532
598
|
const LEGACY_OPS = new Set(["select", "insert", "update", "delete", "upsert", "aggregate", "copy_to", "custom_query"])
|
|
533
599
|
if (op && LEGACY_OPS.has(op)) {
|
|
@@ -549,7 +615,7 @@ function validateEndpoint(entry, ctx) {
|
|
|
549
615
|
// Fields like `query`, `query_file`, `params` belong to `operation: query`
|
|
550
616
|
// and are silently ignored here — surface as ERROR so the agent reroutes.
|
|
551
617
|
if (op === "mutation") {
|
|
552
|
-
if (!
|
|
618
|
+
if (!table) {
|
|
553
619
|
diagnostics.push({
|
|
554
620
|
severity: "error",
|
|
555
621
|
rule: "mutation_missing_table",
|
|
@@ -558,9 +624,9 @@ function validateEndpoint(entry, ctx) {
|
|
|
558
624
|
fix_hint: `Add 'table: <name>'. mutation also needs exactly one of: insert / update / delete.`,
|
|
559
625
|
})
|
|
560
626
|
}
|
|
561
|
-
const wantsInsert =
|
|
562
|
-
const wantsUpdate =
|
|
563
|
-
const wantsDelete =
|
|
627
|
+
const wantsInsert = insertValue !== undefined && insertValue !== null
|
|
628
|
+
const wantsUpdate = updateValue !== undefined && updateValue !== null
|
|
629
|
+
const wantsDelete = deleteValue === true
|
|
564
630
|
const opCount = [wantsInsert, wantsUpdate, wantsDelete].filter(Boolean).length
|
|
565
631
|
if (opCount === 0) {
|
|
566
632
|
diagnostics.push({
|
|
@@ -582,7 +648,7 @@ function validateEndpoint(entry, ctx) {
|
|
|
582
648
|
if (wantsUpdate || wantsDelete) {
|
|
583
649
|
// `where: {}` is just as dangerous as omitting `where:` entirely — both
|
|
584
650
|
// produce an unconstrained UPDATE/DELETE in the engine.
|
|
585
|
-
const whereVal =
|
|
651
|
+
const whereVal = whereValue
|
|
586
652
|
const whereIsEmpty =
|
|
587
653
|
whereVal === undefined ||
|
|
588
654
|
whereVal === null ||
|
|
@@ -603,7 +669,7 @@ function validateEndpoint(entry, ctx) {
|
|
|
603
669
|
// Foreign fields that belong to `operation: query`
|
|
604
670
|
const QUERY_ONLY = ["query", "query_file", "params"]
|
|
605
671
|
for (const k of QUERY_ONLY) {
|
|
606
|
-
if (node[k] !== undefined) {
|
|
672
|
+
if (node[k] !== undefined || params?.[k] !== undefined) {
|
|
607
673
|
diagnostics.push({
|
|
608
674
|
severity: "error",
|
|
609
675
|
rule: "mutation_with_query_field",
|
|
@@ -620,7 +686,7 @@ function validateEndpoint(entry, ctx) {
|
|
|
620
686
|
// Fields like `table`, `insert`, `update`, `delete`, `where`, `on_conflict`,
|
|
621
687
|
// `returning` belong to `operation: mutation` and are ignored here.
|
|
622
688
|
if (op === "query") {
|
|
623
|
-
if (!
|
|
689
|
+
if (!query && !queryFile) {
|
|
624
690
|
diagnostics.push({
|
|
625
691
|
severity: "error",
|
|
626
692
|
rule: "query_missing_sql",
|
|
@@ -630,7 +696,10 @@ function validateEndpoint(entry, ctx) {
|
|
|
630
696
|
})
|
|
631
697
|
}
|
|
632
698
|
const MUTATION_ONLY = ["table", "insert", "update", "delete", "where", "on_conflict", "returning"]
|
|
633
|
-
const foreign = MUTATION_ONLY.filter(k =>
|
|
699
|
+
const foreign = MUTATION_ONLY.filter(k => {
|
|
700
|
+
const value = node[k] ?? params?.[k]
|
|
701
|
+
return value !== undefined && value !== false
|
|
702
|
+
})
|
|
634
703
|
if (foreign.length > 0) {
|
|
635
704
|
diagnostics.push({
|
|
636
705
|
severity: "error",
|
|
@@ -855,6 +924,11 @@ function validateEndpoint(entry, ctx) {
|
|
|
855
924
|
}
|
|
856
925
|
}
|
|
857
926
|
|
|
927
|
+
// --- SQL table existence (if schema.sql available) ---
|
|
928
|
+
// Defer actual error emission to runValidation: it batch-checks all missing
|
|
929
|
+
// tables against the remote before deciding whether local schema.sql is stale.
|
|
930
|
+
collectMissingTableCandidates(ctx, referencedTables, name, file)
|
|
931
|
+
|
|
858
932
|
// --- Credential references ---
|
|
859
933
|
for (const node of doc.workflow?.nodes || []) {
|
|
860
934
|
if (!node || typeof node !== "object") continue
|
|
@@ -982,14 +1056,15 @@ function validateEndpoint(entry, ctx) {
|
|
|
982
1056
|
for (const node of allNodes) {
|
|
983
1057
|
const nodeType = node?.type ?? node?.node_type
|
|
984
1058
|
if (nodeType !== "dypai_database") continue
|
|
985
|
-
const
|
|
1059
|
+
const params = nodeParams(node)
|
|
1060
|
+
const op = nodeField(node, params, "operation")
|
|
986
1061
|
let writeKind = null
|
|
987
1062
|
if (op === "mutation") {
|
|
988
|
-
if (node
|
|
989
|
-
else if (node
|
|
990
|
-
else if (node
|
|
1063
|
+
if (nodeField(node, params, "insert") !== undefined && nodeField(node, params, "insert") !== null) writeKind = "INSERT"
|
|
1064
|
+
else if (nodeField(node, params, "update") !== undefined && nodeField(node, params, "update") !== null) writeKind = "UPDATE"
|
|
1065
|
+
else if (nodeField(node, params, "delete") === true) writeKind = "DELETE"
|
|
991
1066
|
} else if (op === "query") {
|
|
992
|
-
const sql = String(node
|
|
1067
|
+
const sql = String(nodeField(node, params, "query") || "")
|
|
993
1068
|
if (/\b(INSERT|UPDATE|DELETE|TRUNCATE|DROP|ALTER|CREATE)\b/i.test(sql)) {
|
|
994
1069
|
writeKind = "write SQL"
|
|
995
1070
|
}
|
|
@@ -1069,6 +1144,68 @@ async function verifyTablesInRemote(missingTables, projectId) {
|
|
|
1069
1144
|
}
|
|
1070
1145
|
}
|
|
1071
1146
|
|
|
1147
|
+
function bucketNamesFromStorageList(payload) {
|
|
1148
|
+
const names = new Set()
|
|
1149
|
+
const add = (value) => {
|
|
1150
|
+
if (typeof value === "string" && value.trim()) names.add(value.trim())
|
|
1151
|
+
}
|
|
1152
|
+
const candidates = []
|
|
1153
|
+
if (Array.isArray(payload)) {
|
|
1154
|
+
candidates.push(payload)
|
|
1155
|
+
} else if (payload && typeof payload === "object") {
|
|
1156
|
+
for (const key of ["buckets", "data", "items", "results"]) {
|
|
1157
|
+
if (Array.isArray(payload[key])) candidates.push(payload[key])
|
|
1158
|
+
}
|
|
1159
|
+
if (!candidates.length) candidates.push([payload])
|
|
1160
|
+
}
|
|
1161
|
+
for (const list of candidates) {
|
|
1162
|
+
for (const item of list) {
|
|
1163
|
+
if (typeof item === "string") {
|
|
1164
|
+
add(item)
|
|
1165
|
+
} else if (item && typeof item === "object") {
|
|
1166
|
+
add(item.name)
|
|
1167
|
+
add(item.bucket)
|
|
1168
|
+
add(item.bucket_name)
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return names
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
async function verifyStorageBucketsInRemote(bucketRefs, projectId) {
|
|
1176
|
+
if (!bucketRefs.length) return []
|
|
1177
|
+
const uniqueRefs = new Map(bucketRefs.map(ref => [`${ref.bucket}|${ref.endpoint}|${ref.file}|${ref.loc}`, ref]))
|
|
1178
|
+
const refs = [...uniqueRefs.values()]
|
|
1179
|
+
const bucketNames = [...new Set(refs.map(ref => ref.bucket))].sort()
|
|
1180
|
+
try {
|
|
1181
|
+
const args = { operation: "list" }
|
|
1182
|
+
if (projectId) args.project_id = projectId
|
|
1183
|
+
const result = await proxyToolCall("manage_storage", args)
|
|
1184
|
+
const existing = bucketNamesFromStorageList(result)
|
|
1185
|
+
return refs
|
|
1186
|
+
.filter(ref => !existing.has(ref.bucket))
|
|
1187
|
+
.map(ref => ({
|
|
1188
|
+
severity: "error",
|
|
1189
|
+
rule: "storage_bucket_missing",
|
|
1190
|
+
endpoint: ref.endpoint,
|
|
1191
|
+
file: ref.file,
|
|
1192
|
+
loc: ref.loc,
|
|
1193
|
+
message: `Storage bucket '${ref.bucket}' is referenced by endpoint '${ref.endpoint}', but it does not exist in this project.`,
|
|
1194
|
+
fix_hint:
|
|
1195
|
+
`Create it before testing/pushing this endpoint: ` +
|
|
1196
|
+
`manage_storage({ operation: "create", name: "${ref.bucket}", public: false }). ` +
|
|
1197
|
+
`Use public: true only for public assets; user documents/attachments should stay private.`,
|
|
1198
|
+
}))
|
|
1199
|
+
} catch (e) {
|
|
1200
|
+
return [{
|
|
1201
|
+
severity: "warn",
|
|
1202
|
+
rule: "storage_bucket_check_failed",
|
|
1203
|
+
message: `Could not verify storage buckets: ${bucketNames.join(", ")}.`,
|
|
1204
|
+
fix_hint: `Retry manage_storage({ operation: "list" }) or create the expected bucket before testing upload/storage endpoints. Detail: ${e.message}`,
|
|
1205
|
+
}]
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1072
1209
|
/**
|
|
1073
1210
|
* Refresh dypai/schema.sql from the remote. Best-effort — if it fails the
|
|
1074
1211
|
* caller continues with the existing local schema (just logs the warning).
|
|
@@ -1116,6 +1253,7 @@ export async function runValidation(rootDir, projectId) {
|
|
|
1116
1253
|
// Collected during per-endpoint pass; resolved against the remote AFTER
|
|
1117
1254
|
// all endpoints are checked (one batch query instead of N).
|
|
1118
1255
|
suspectedMissingTables: [],
|
|
1256
|
+
suspectedStorageBuckets: [],
|
|
1119
1257
|
}
|
|
1120
1258
|
|
|
1121
1259
|
const diagnostics = []
|
|
@@ -1185,6 +1323,7 @@ export async function runValidation(rootDir, projectId) {
|
|
|
1185
1323
|
|
|
1186
1324
|
// Realtime YAML rules
|
|
1187
1325
|
diagnostics.push(...await validateRealtime(rootDir, ctx))
|
|
1326
|
+
diagnostics.push(...await verifyStorageBucketsInRemote(ctx.suspectedStorageBuckets, targetProjectId))
|
|
1188
1327
|
|
|
1189
1328
|
// Surface any file-read errors too
|
|
1190
1329
|
for (const err of local.errors || []) {
|
|
@@ -1219,7 +1358,7 @@ export const dypaiValidateTool = {
|
|
|
1219
1358
|
name: "dypai_validate",
|
|
1220
1359
|
description:
|
|
1221
1360
|
"Lint the local dypai/ folder BEFORE pushing. Catches ${input.x} / ${nodes.x.y} / ${current_user_*} refs that don't resolve, " +
|
|
1222
|
-
"SQL tables not in schema.sql, credentials that don't exist remotely, and agent tool refs to non-tool endpoints. " +
|
|
1361
|
+
"SQL tables not in schema.sql, missing storage buckets, credentials that don't exist remotely, and agent tool refs to non-tool endpoints. " +
|
|
1223
1362
|
"Run this after editing YAMLs to catch typos pre-flight. Push already calls it by default — pass skip_validation:true to override.",
|
|
1224
1363
|
inputSchema: {
|
|
1225
1364
|
type: "object",
|