@ductape/mcp 0.1.50 → 0.1.52

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/dist/index.js CHANGED
@@ -932,7 +932,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
932
932
  "storage.create", "storage.update"
933
933
 
934
934
  When to use this tool:
935
- - Before calling ductape_execute to create or update any asset — use this to discover required fields
935
+ - Before constructing an administrative asset file for ductape_cli or Workbench — use this to discover required fields
936
936
  - To enumerate valid enum values (e.g. which DatabaseTypes or AuthTypes are accepted)
937
937
  - To understand nested object shapes without reading SDK docs
938
938
  `;
@@ -973,6 +973,9 @@ const payloadGenerateInputSchema = z.object({
973
973
  'For messaging: { broker: "broker_tag", topic?: "topic_tag" }.'),
974
974
  include_session: z.boolean().optional().default(true).describe('Include a session placeholder inside the generated input object. ' +
975
975
  'The placeholder is named "<session_tag_token>" to indicate it expects the runtime JWT, not the tag name.'),
976
+ execution_context: z.enum(['user', 'delegated', 'system']).optional().default('user').describe('Actor intent for this runtime operation. "user" means an active request initiated by the authenticated user; ' +
977
+ '"delegated" means work acting on behalf of a user with a short-lived delegated identity or application-owned ' +
978
+ 'immutable actor context; "system" means intentionally unattributed background work. This drives session warnings.'),
976
979
  include_cache: z.boolean().optional().default(true).describe('Include the cache tag inside the generated input object so the caller knows which cache to reference for this query.'),
977
980
  schema_mode: z.enum(['strict', 'best_effort']).optional().default('best_effort').describe('"strict" — fail if any required field cannot be resolved. ' +
978
981
  '"best_effort" — fill what is known, leave unknowns as null/placeholder. Use best_effort when exploring.'),
@@ -1061,6 +1064,43 @@ function buildSdkInvocationArgs(payload) {
1061
1064
  ...(payload?.cache ? { cache: payload.cache } : {}),
1062
1065
  };
1063
1066
  }
1067
+ function operationAcceptsSession(operationFamily, method) {
1068
+ const family = operationFamily.toLowerCase();
1069
+ const sessionFamilies = new Set([
1070
+ 'action', 'features', 'feature', 'database', 'graph', 'vector', 'storage',
1071
+ 'notification', 'messaging', 'broker', 'events', 'event', 'quota', 'fallback',
1072
+ ]);
1073
+ if (!sessionFamilies.has(family))
1074
+ return false;
1075
+ return !['consume', 'status', 'check', 'fetch', 'list'].includes(method.toLowerCase());
1076
+ }
1077
+ function addSessionAwarenessMetadata(generated, args) {
1078
+ const acceptsSession = operationAcceptsSession(args.operation_family, args.method);
1079
+ const executionContext = args.execution_context ?? 'user';
1080
+ const payloadHasSession = Boolean(generated?.payload?.session || generated?.payload?.input?.session);
1081
+ const sessionRequested = args.include_session !== false;
1082
+ const warnings = [];
1083
+ if (acceptsSession && executionContext !== 'system' && (!sessionRequested || !payloadHasSession)) {
1084
+ warnings.push(`Session attribution is missing for a ${executionContext}-context operation. ` +
1085
+ 'Pass the original full session token for immediate user work, or an approved delegated identity/immutable actor context for delayed work.');
1086
+ }
1087
+ if (executionContext === 'system' && payloadHasSession) {
1088
+ warnings.push('This operation is marked system-context but contains a session placeholder. Remove it unless the work is actually user/delegated.');
1089
+ }
1090
+ generated.meta = {
1091
+ ...(generated?.meta ?? {}),
1092
+ session_awareness: {
1093
+ accepts_session: acceptsSession,
1094
+ execution_context: executionContext,
1095
+ appears_user_context: executionContext === 'user',
1096
+ session_requested: sessionRequested,
1097
+ session_present_in_payload: payloadHasSession,
1098
+ intentionally_system_context: executionContext === 'system',
1099
+ warnings,
1100
+ },
1101
+ };
1102
+ return generated;
1103
+ }
1064
1104
  function buildTypeScriptSnippet(payload, operationFamily, method) {
1065
1105
  const callPath = resolveSdkCallPath(operationFamily, method);
1066
1106
  const invocationArgs = buildSdkInvocationArgs(payload);
@@ -1405,33 +1445,9 @@ DUCTAPE DATABASE MIGRATIONS
1405
1445
 
1406
1446
  Migrations are versioned SQL/NoSQL schema change scripts managed per database component.
1407
1447
 
1408
- Create a migration:
1409
- ductape_execute("databases.migration.create", [{
1410
- product: "my-product",
1411
- database: "core-db",
1412
- data: {
1413
- name: "add users table",
1414
- tag: "001-add-users",
1415
- value: {
1416
- up: ["CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE)"],
1417
- down: ["DROP TABLE users"],
1418
- },
1419
- },
1420
- }])
1421
-
1422
- Run migrations (applies all pending):
1423
- ductape_execute("databases.migration.run", [migrations, { env: "prd" }])
1424
-
1425
- Rollback:
1426
- ductape_execute("databases.migration.rollback", [migrations, 1]) // roll back 1
1427
-
1428
- Check status:
1429
- ductape_execute("databases.migration.status", [migrations])
1430
-
1431
- History:
1432
- ductape_execute("databases.migration.history", [])
1448
+ Create migrations through the project migration files and ductape_cli, never ductape_execute.
1433
1449
 
1434
- Via CLI:
1450
+ Use the access-key administrative CLI for running, rolling back, and inspecting migrations:
1435
1451
  ductape_cli("db migrate") // run pending
1436
1452
  ductape_cli("db migrate rollback") // roll back last
1437
1453
  ductape_cli("db migrate rollback -n 3")
@@ -1441,25 +1457,13 @@ MongoDB: migrations run as raw Mongo shell commands in the up/down arrays.
1441
1457
  indexes: `
1442
1458
  DUCTAPE DATABASE INDEXES
1443
1459
 
1444
- Create an index:
1445
- ductape_execute("databases.schema.createIndex", [
1446
- "collection_name",
1447
- ["field1", "field2"], // simple array of field names
1448
- { unique: true, name: "idx_email_unique" } // options (optional)
1449
- ])
1460
+ Create or update indexes through migration files and ductape_cli("db migrate"), not the
1461
+ publishable-key runtime proxy. Example definitions may use ["field1", "field2"] with
1462
+ { unique: true, name: "idx_email_unique" }, or ordered fields such as
1463
+ [{ field: "createdAt", order: "DESC" }, { field: "userId", order: "ASC" }].
1450
1464
 
1451
- // Ordered index:
1452
- ductape_execute("databases.schema.createIndex", [
1453
- "orders",
1454
- [{ field: "createdAt", order: "DESC" }, { field: "userId", order: "ASC" }],
1455
- { name: "idx_orders_date_user" }
1456
- ])
1457
-
1458
- Drop an index:
1459
- ductape_execute("databases.schema.dropIndex", ["collection_name", "index_name"])
1460
-
1461
- List indexes on a collection:
1462
- ductape_execute("databases.schema.indexes", ["collection_name"])
1465
+ Drop and inspect indexes through migration/CLI tooling or the database administration surface,
1466
+ not ductape_execute.
1463
1467
 
1464
1468
  Performance guidance:
1465
1469
  - Index fields used in WHERE clauses, JOIN conditions, and ORDER BY.
@@ -1503,18 +1507,7 @@ DUCTAPE DATABASE ACTIONS
1503
1507
  A database action is a saved query or mutation (SQL string or NoSQL command) stored
1504
1508
  on the Ductape product and executed by tag at runtime.
1505
1509
 
1506
- Create an action (admin):
1507
- ductape_execute("databases.action.create", [{
1508
- product: "my-product",
1509
- database: "core-db",
1510
- data: {
1511
- tag: "get-active-users",
1512
- name: "Get active users",
1513
- description: "Returns all users with status=active",
1514
- type: "sql", // "sql" or "nosql"
1515
- query: "SELECT * FROM users WHERE status = :status",
1516
- },
1517
- }])
1510
+ Create/update actions in Workbench (administrative access), never through ductape_execute.
1518
1511
 
1519
1512
  Dispatch an action at runtime:
1520
1513
  → CALL ductape_generate_payload FIRST to get the canonical input shape.
@@ -1529,10 +1522,9 @@ Dispatch an action at runtime:
1529
1522
  List actions for a database:
1530
1523
  ductape_execute("databases.action.list", ["database_tag"])
1531
1524
 
1532
- Fetch / update / delete:
1525
+ Fetch:
1533
1526
  ductape_execute("databases.action.fetch", ["action_tag"])
1534
- ductape_execute("databases.action.update", ["my-product", "action_tag", { query: "..." }])
1535
- ductape_execute("databases.action.delete", ["action_tag"])
1527
+ Update/delete are administrative and must be performed in Workbench.
1536
1528
 
1537
1529
  Actions are the preferred way to encapsulate complex or reused queries — they can be
1538
1530
  scheduled, dispatched with retries, and audited via logs.
@@ -1790,16 +1782,8 @@ DUCTAPE SECRETS
1790
1782
  Secrets are workspace-level encrypted key-value pairs. They are referenced in resource configs,
1791
1783
  connection URLs, and any string field using the $Secret{KEY_NAME} syntax.
1792
1784
 
1793
- Create (admin ductape_cli or ductape_execute):
1794
- ductape_execute("secrets.create", [{
1795
- key: "STRIPE_API_KEY",
1796
- value: "sk_live_...", // plaintext — encrypted AES-256-GCM client-side before send
1797
- description: "Stripe live key",
1798
- token_type: "api", // "api" | "password" | "certificate"
1799
- scope: ["my-product"], // which products can read this secret
1800
- envs: ["prd"], // which env slugs can read this secret
1801
- expires_at: 1800000000, // optional epoch ms
1802
- }])
1785
+ Create/update secrets with ductape_cli or Workbench (administrative access), never ductape_execute.
1786
+ Definition fields include key, value, description, token_type, scope, envs, and expires_at.
1803
1787
  The server never receives the plaintext value. Encryption uses the workspace private key.
1804
1788
 
1805
1789
  Fetch / resolve:
@@ -1814,10 +1798,7 @@ $Secret{} reference syntax:
1814
1798
  - The in-memory cache stores the encrypted form only; decryption happens on each cache hit.
1815
1799
  - Cache TTL: 5 minutes. Clear with: secrets.clearCache()
1816
1800
 
1817
- Lifecycle:
1818
- ductape_execute("secrets.revoke", ["KEY"]) → disables without deleting (recoverable)
1819
- ductape_execute("secrets.delete", ["KEY"]) → permanent deletion (irreversible)
1820
- ductape_execute("secrets.update", ["KEY", { value: "new_value", expires_at: ... }])
1801
+ Lifecycle mutations (revoke/delete/update) are administrative: use ductape_cli or Workbench.
1821
1802
 
1822
1803
  List all secrets (keys only — values are not returned in list):
1823
1804
  ductape_execute("secrets.list", [])
@@ -1867,9 +1848,7 @@ Import from a file:
1867
1848
  ductape_cli("apps import <file.json> -t postman|openapi")
1868
1849
  Supports Postman v2.1 collection and OpenAPI 3.0 spec.
1869
1850
 
1870
- Manage environments (base URLs per stage):
1871
- ductape_execute("app.environments.create", [app_tag, { slug, env_name, base_url }])
1872
- ductape_execute("app.environments.list", [app_tag])
1851
+ Manage environments (base URLs per stage) in Workbench; this currently has no CLI command.
1873
1852
 
1874
1853
  Discover apps in a product and their actions (ALWAYS do this before writing any ctx.api.run call):
1875
1854
  Step 1 — list apps connected to the product:
@@ -1886,16 +1865,8 @@ Discover apps in a product and their actions (ALWAYS do this before writing any
1886
1865
  app resources (not for runtime input — use actions.fetch or ductape_generate_payload for that)
1887
1866
  NEVER assume action input field names. Always fetch the action definition first.
1888
1867
 
1889
- Manage actions (individual API endpoints):
1890
- ductape_execute("actions.create", [app_tag, {
1891
- tag, name, resource: "/users/{id}", method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE",
1892
- body?: { fieldName: { type, required? } },
1893
- params?: { id: { type: "string" } },
1894
- query?: { filter: { type: "string" } },
1895
- headers?: { Authorization: { type: "string" } },
1896
- response?: { status_code: 200, success: true, body: { ... }, response_format: "json" },
1897
- }])
1898
- ductape_execute("actions.update", [app_tag, action_tag, data])
1868
+ Manage actions (individual API endpoints) in Workbench or import an OpenAPI/Postman file.
1869
+ Creation/update are administrative and must never use ductape_execute.
1899
1870
  ductape_execute("actions.list", [app_tag])
1900
1871
  ductape_execute("actions.fetch", [app_tag, action_tag])
1901
1872
 
@@ -1916,16 +1887,14 @@ Run an action at runtime:
1916
1887
 
1917
1888
  Auth schemes (how the app authenticates outbound requests):
1918
1889
  Setup types: header | bearer | basic | oauth2 | apikey
1919
- ductape_execute("auths.create", [app_tag, { tag, name, setup_type, expiry, period, action_tag? }])
1890
+ Configure auth in Workbench (administrative).
1920
1891
  ductape_execute("auths.list", [app_tag])
1921
1892
 
1922
1893
  Webhooks (inbound events from the external service):
1923
- ductape_execute("webhooks.create", [app_tag, { tag, name, description, envs: [{ slug, registration_url?, method? }] }])
1924
- ductape_execute("webhooks.events.create", [app_tag, { tag, name, selector, description, sample }])
1894
+ Configure webhooks and webhook events in Workbench (administrative).
1925
1895
 
1926
1896
  Variables (per-env mutable values) and Constants (fixed values):
1927
- ductape_execute("app.variables.create", [app_tag, { key, value, env_slug }])
1928
- ductape_execute("app.constants.create", [app_tag, { key, value }])
1897
+ Configure variables and constants in Workbench (administrative).
1929
1898
 
1930
1899
  Connecting an app to a product (after creation):
1931
1900
  NOTE: All product.* module methods require the access key and CANNOT use ductape_execute.
@@ -1933,14 +1902,13 @@ Connecting an app to a product (after creation):
1933
1902
 
1934
1903
  FULL FLOW to make an app callable from a product:
1935
1904
  1. Create the app: ductape_cli("apps create --name \\"Service Name\\" --description \\"...\\"")
1936
- 2. Add environments: ductape_execute("app.environments.create", [app_tag, { slug: "prd", env_name: "Production", base_url: "https://api.example.com" }])
1937
- 3. Configure auth: ductape_execute("auths.create", [app_tag, { tag, name, setup_type: "apikey"|"bearer"|"basic"|"oauth2", expiry, period }])
1938
- 4. Define actions: ductape_execute("actions.create", [app_tag, { tag, name, resource, method, body?, params?, query?, headers?, response? }])
1905
+ 2. Add environments: Workbench
1906
+ 3. Configure auth: Workbench
1907
+ 4. Define actions: Workbench
1939
1908
  OR import: ductape_cli("apps import <file.json> -t postman|openapi")
1940
1909
  5. Connect to product: Requires the product_id (from ductape_cli("products get --tag <tag> --json")).
1941
1910
  There is no CLI command for this step — the SDK product.apps.add method requires
1942
- an access key which only the backend can provide. Use ductape_execute via an
1943
- admin-authenticated context, or connect via the Workbench UI.
1911
+ an access key which only the backend can provide. Connect via Workbench.
1944
1912
  6. Verify: ductape_cli("products get --tag <product_tag> --json") → check apps[] contains the app
1945
1913
  ductape_execute("actions.list", [app_tag]) → verify actions are registered
1946
1914
  `.trim(),
@@ -2015,8 +1983,8 @@ IMPORTANT — selector must be "$Session{fieldName}" format:
2015
1983
  CORRECT: selector: "$Session{playerId}"
2016
1984
  INCORRECT: selector: "playerId" ← WILL FAIL with "Selector should be in the format $Session{...}{key}"
2017
1985
 
2018
- Example (game product player identity in JWT):
2019
- ductape_execute("sessions.create", [product_tag, {
1986
+ Example definition (configure via declarative apply or Workbench, not ductape_execute):
1987
+ {
2020
1988
  tag: "player-session",
2021
1989
  name: "Player Session",
2022
1990
  expiry: 24,
@@ -2028,7 +1996,7 @@ Example (game product — player identity in JWT):
2028
1996
  role: "player",
2029
1997
  accountId: "acct_xyz",
2030
1998
  },
2031
- }])
1999
+ }
2032
2000
 
2033
2001
  Runtime — create a session (sign a JWT):
2034
2002
  → CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
@@ -2068,10 +2036,33 @@ Token format: "session_tag:jwt_token" — pass this format verbatim wherever ses
2068
2036
  JWT is signed with the product private key (not a symmetric shared secret).
2069
2037
 
2070
2038
  SESSION PROPAGATION
2071
- Preserve the original full token on every user-context database, Event, Feature, notification,
2072
- storage, graph, vector, action, and other runtime operation that accepts session. Do not replace
2073
- it with the session tag, user ID, decoded claims, or refresh token. Durable user-initiated work
2074
- should retain the initiating token; intentional system/background work should be sessionless.
2039
+ Extract the original full "session_tag:jwt" token once at the HTTP/transport boundary and retain
2040
+ it separately from verified/decoded claims. Claims are authorization data; they are not a token.
2041
+ Store a request ActorContext in a NestJS request-scoped provider or AsyncLocalStorage:
2042
+ type ActorContext =
2043
+ | { kind: "user"; session: string; actorId: string }
2044
+ | { kind: "delegated"; delegatedIdentity: string; actorId: string; initiatedAt: string }
2045
+ | { kind: "system"; reason: string };
2046
+ Pass ActorContext explicitly through service boundaries. At each immediate Ductape runtime call,
2047
+ pass actor.kind === "user" ? actor.session : the SDK-approved delegated identity. Put only
2048
+ non-secret actor metadata (actorId, kind, initiatedAt, correlationId) in Event envelopes.
2049
+ Feature execution should receive the same explicit actor classification. Never log, serialize
2050
+ into business payloads, or attach the raw session/refresh token to traces or error messages.
2051
+
2052
+ SECURITY BOUNDARY FOR DURABLE WORK
2053
+ The current SDK accepts a session string on dispatch, but it does not expose an immutable
2054
+ attribution snapshot API, an expired-token attribution contract, or a general delegated-session
2055
+ issuer. Therefore do NOT persist reusable JWTs indefinitely or assume an expired initiating token
2056
+ will remain valid when delayed work executes. Pass the full token only for immediate work within
2057
+ its validity window. For delayed/recurring work, classify execution as system-context, or use an
2058
+ application-owned immutable actor-context envelope plus a short-lived delegated identity issued
2059
+ by an explicitly approved auth design. Until the SDK provides that design, actor metadata is for
2060
+ audit/correlation and must not be treated as authorization.
2061
+
2062
+ Distinguish contexts deliberately:
2063
+ user — active request; original valid full token is required when the operation accepts it
2064
+ delegated — delayed/on-behalf-of work; approved short-lived credential + immutable actor metadata
2065
+ system — scheduler/maintenance with no user authority; omit session and record a reason
2075
2066
 
2076
2067
  Backend propagation attributes component activity to an actor, but it does not record frontend
2077
2068
  pageviews, navigation, UI intent, funnels, or client failures. Continue immediately with
@@ -2082,14 +2073,12 @@ DUCTAPE CACHES
2082
2073
 
2083
2074
  Caches are product-level Redis (or in-memory) stores for temporary key-value data with optional TTL.
2084
2075
 
2085
- Registration (admin — ductape_cli or SDK):
2076
+ Registration (admin — ductape_cli):
2086
2077
  ductape_cli("resources caches create -f cache.json")
2087
2078
  File: { name, tag, description?, expiry: <milliseconds> }
2088
2079
  No type or envs — Ductape manages the store infrastructure.
2089
2080
  expiry is in MILLISECONDS: 3600000 = 1 hour, 86400000 = 1 day, 604800000 = 1 week.
2090
2081
 
2091
- SDK: ductape_execute("caches.create", [product_tag, { name, tag, description?, expiry: 3600000 }])
2092
-
2093
2082
  Operations:
2094
2083
  caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
2095
2084
  expiry is an ABSOLUTE TIMESTAMP (not a duration).
@@ -2137,20 +2126,20 @@ DUCTAPE NOTIFICATIONS
2137
2126
 
2138
2127
  Notifications send messages across multiple channels: email, SMS, push, or HTTP callback.
2139
2128
 
2140
- Create a notification (admin ductape_execute):
2141
- ductape_execute("notifications.create", [product_tag, {
2129
+ Create a notification through declarative apply or Workbench (administrative), using:
2130
+ {
2142
2131
  tag: "welcome-email",
2143
2132
  name: "Welcome Email",
2144
2133
  type: "email", // optional hint; actual channels configured per env
2145
- }])
2134
+ }
2146
2135
 
2147
- Create a message template:
2148
- ductape_execute("notifications.messages.create", [product_tag, {
2136
+ Create a message template through declarative apply or Workbench:
2137
+ {
2149
2138
  tag: "welcome-email:default", // format: "notification_tag:message_tag"
2150
2139
  notification: "welcome-email",
2151
2140
  subject: { template: "Welcome, {{name}}!", data: { name: "" } },
2152
2141
  body: { template: "Hi {{name}}, thanks for signing up.", data: { name: "" } },
2153
- }])
2142
+ }
2154
2143
 
2155
2144
  Send at runtime (one channel at a time):
2156
2145
  → CALL ductape_generate_payload FIRST (operation_family="notification", method="email.send")
@@ -2188,8 +2177,15 @@ DUCTAPE RESILIENCE
2188
2177
  Resilience covers three mechanisms: quotas (rate-limited provider pools), fallbacks (automatic
2189
2178
  provider switching), and healthchecks (continuous probe monitoring with failure actions).
2190
2179
 
2180
+ CONFIGURATION BOUNDARY
2181
+ Quotas, fallbacks, and health checks are administrative product configuration. Configure them in
2182
+ Workbench (or a future access-key administrative tool explicitly documented for the asset).
2183
+ Never route administrative create/update methods through ductape_execute: its publishable-key
2184
+ runtime proxy will fail.
2185
+
2191
2186
  QUOTAS — rate-limited multi-provider pools:
2192
- ductape_execute("quotas.create", [product_tag, {
2187
+ Workbench definition shape:
2188
+ {
2193
2189
  tag: "sms-quota",
2194
2190
  name: "SMS Provider Pool",
2195
2191
  input: { to: { type: "string", required: true }, message: { type: "string" } },
@@ -2203,7 +2199,7 @@ QUOTAS — rate-limited multi-provider pools:
2203
2199
  input: { "body:to": "$Input{to}", "body:body": "$Input{message}" },
2204
2200
  output: {} },
2205
2201
  ],
2206
- }])
2202
+ }
2207
2203
  Providers are tried in order until quota is not exhausted.
2208
2204
  quotas.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
2209
2205
  quotas.dispatch [{ product, env, tag, input, schedule? }]
@@ -2211,12 +2207,13 @@ QUOTAS — rate-limited multi-provider pools:
2211
2207
  FALLBACKS — automatic provider switching on failure:
2212
2208
  Same schema as quotas but options are ordered: primary first, then fallback(s).
2213
2209
  Primary is used first; on failure, the next provider is tried automatically.
2214
- ductape_execute("fallback.create", [product_tag, { tag, name, input: { ... }, options: [...] }])
2210
+ Configure in Workbench: { tag, name, input: { ... }, options: [...] }
2215
2211
  fallback.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
2216
2212
  fallback.dispatch [{ product, env, tag, input, schedule? }]
2217
2213
 
2218
2214
  HEALTHCHECKS — continuous probe with failure notifications:
2219
- ductape_execute("health.create", [product_tag, {
2215
+ Workbench definition shape:
2216
+ {
2220
2217
  tag: "payment-health",
2221
2218
  name: "Payment Service Health",
2222
2219
  probe: { type: "app", app: "stripe-app", event: "ping" },
@@ -2228,7 +2225,7 @@ HEALTHCHECKS — continuous probe with failure notifications:
2228
2225
  channels: { email: { recipients: ["ops@example.com"] } } }],
2229
2226
  webhooks: [{ url: "https://hooks.example.com/alert", method: "POST" }],
2230
2227
  },
2231
- }])
2228
+ }
2232
2229
  health.run [{ product, env, tag }] → triggers an immediate probe
2233
2230
  health.check [{ product, env, tag }] → same as run
2234
2231
  health.status [{ product, env, tag }] → current health status
@@ -2238,6 +2235,23 @@ Failure actions: notification channels, HTTP webhooks, and/or message broker emi
2238
2235
  be configured simultaneously on the same healthcheck.
2239
2236
  Input template references: $Input{field} → maps declared input to the probe's action input.
2240
2237
  Provider status: available | unavailable
2238
+
2239
+ DECISION MATRIX
2240
+ Rate/capacity allocation across providers → quota
2241
+ Equivalent provider after operational failure → fallback
2242
+ Detect failure before routing provider traffic → health check
2243
+ Transient failure of one operation → bounded retry + idempotency policy
2244
+ Multi-step business recovery/compensation → Feature
2245
+ Database atomicity → database transaction, not Feature rollback
2246
+ Scheduled single operation → that component's dispatch
2247
+ Scheduled multi-step process → Feature dispatch
2248
+
2249
+ COMBINING MECHANISMS
2250
+ A health check may keep an unhealthy provider out of a fallback/quota pool; the pool controls
2251
+ provider selection; each operation may use bounded retries and an idempotency key; a Feature
2252
+ coordinates business steps and compensation around those resilient operations. Do not stack them
2253
+ reflexively: each layer needs a distinct failure it owns, bounded retry budgets, and observable
2254
+ terminal behavior. Database transactions remain the atomicity boundary for related DB writes.
2241
2255
  `.trim(),
2242
2256
  'frontend-analytics': `
2243
2257
  DUCTAPE FRONTEND PRODUCT ANALYTICS AND SESSION ACTIVITY
@@ -2245,10 +2259,12 @@ DUCTAPE FRONTEND PRODUCT ANALYTICS AND SESSION ACTIVITY
2245
2259
  Frontend analytics and backend session attribution are complementary; neither replaces the other.
2246
2260
 
2247
2261
  BACKEND OPERATION ATTRIBUTION
2248
- Pass the original full Ductape session token to every user-initiated database, Event, Feature,
2249
- notification, storage, graph, vector, action, and other runtime operation that accepts session.
2250
- This attributes server work to the authenticated actor. Durable work should retain the initiating
2251
- session; intentional system/background work should remain sessionless.
2262
+ Immediate user-initiated backend work should pass the original full Ductape session token to every
2263
+ database, Event, Feature, notification, storage, graph, vector, action, and other runtime operation
2264
+ that accepts session. This attributes immediate server work to the authenticated actor.
2265
+ For delayed/durable work, do not persist raw JWTs indefinitely. Use system context, or an approved
2266
+ delegated actor design with immutable non-secret actor metadata. Actor metadata supports audit and
2267
+ correlation; it is not authorization. Intentional system/background work should remain sessionless.
2252
2268
 
2253
2269
  FRONTEND PRODUCT ANALYTICS
2254
2270
  Use the browser Analytics service for anonymous visits, authenticated page views, navigation,
@@ -2517,9 +2533,11 @@ STEP 4 — PRESENT the plan and get approval BEFORE writing any code or creating
2517
2533
  Wait for confirmation before writing code or calling any create tool.
2518
2534
 
2519
2535
  STEP 5 — CREATE missing components (only with user approval)
2520
- Use ductape_execute for any missing databases, apps, database actions, notification events, etc.
2521
- For a missing database action:
2522
- ductape_execute("databases.action.create", [product_tag, db_tag, { tag, name, type, query }])
2536
+ Administrative assets must never be created through ductape_execute. Use ductape_cli for products,
2537
+ apps, supported resources, broker topics, cloud connections, secrets, and declarative apply flows.
2538
+ App actions, auths, Features, quotas, fallbacks, health checks, and other assets for which the CLI
2539
+ has no command must be configured in Workbench. Do not generate an impossible publishable-key
2540
+ create/update call. For a missing database action, configure it in Workbench, then verify it exists.
2523
2541
  For a missing child feature, recursively apply this same workflow.
2524
2542
  Tell the user what you are about to create before each tool call.
2525
2543
 
@@ -2643,7 +2661,11 @@ When you call features.define({ handler }), the handler runs TWICE:
2643
2661
 
2644
2662
  Invoke internal application business logic (your own NestJS/backend service code):
2645
2663
  → produce a broker event (ctx.messaging.produce or ductape.events.produce)
2646
- consume it in your NestJS service with events.consume in onModuleInit
2664
+ follow ductape_docs({ topic: "events" }) and use the canonical NestJS decorator:
2665
+ @Events.Consumer({ event: "broker-tag:topic-tag" })
2666
+ async handle(message: MessageShape) { /* injected-service business logic; throw to nack */ }
2667
+ → DuctapeModule auto-registers the decorated consumer; no manual onModuleInit is needed
2668
+ → events.consume() remains a supported lower-level alternative for plain TypeScript/Node.js
2647
2669
  → your service method runs with full access to DI, DB transactions, etc.
2648
2670
  Do NOT create an App Action just to call your own service over HTTP.
2649
2671
 
@@ -3689,6 +3711,22 @@ async function main() {
3689
3711
  };
3690
3712
  const executeHandler = async (args) => {
3691
3713
  try {
3714
+ const runtimeMutationMethods = {
3715
+ databases: new Set(['insert', 'update', 'delete', 'upsert']),
3716
+ graph: new Set(['insert', 'update', 'delete']),
3717
+ vector: new Set(['insert', 'upsert', 'upsertOne', 'delete']),
3718
+ sessions: new Set(['revoke']),
3719
+ };
3720
+ const isRuntimeDataMutation = runtimeMutationMethods[args.module]?.has(args.method) === true;
3721
+ const isAdministrativeMutation = (/^migration\./.test(args.method) ||
3722
+ /^schema\.(create|drop|add|remove|update)/.test(args.method) ||
3723
+ /(^|\.)(create|update|delete|configure|add|remove|revoke)$/.test(args.method)) &&
3724
+ !isRuntimeDataMutation;
3725
+ if (isAdministrativeMutation) {
3726
+ throw new Error(`Administrative operation "${args.module}.${args.method}" is blocked in ductape_execute. ` +
3727
+ 'Use ductape_cli when that resource is supported by the CLI; otherwise configure it in Workbench. ' +
3728
+ 'The runtime proxy uses a publishable key and cannot administer platform assets.');
3729
+ }
3692
3730
  const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
3693
3731
  if (!key) {
3694
3732
  throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
@@ -3735,7 +3773,7 @@ async function main() {
3735
3773
  if (!key) {
3736
3774
  throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
3737
3775
  }
3738
- const result = await generateExecutablePayload({ ...args, publishable_key: key });
3776
+ const result = addSessionAwarenessMetadata(await generateExecutablePayload({ ...args, publishable_key: key }), args);
3739
3777
  let text = JSON.stringify(result ?? null, null, 2);
3740
3778
  if (args.operation_family === 'database') {
3741
3779
  const meta = result?.meta ?? {};
@@ -3762,7 +3800,7 @@ async function main() {
3762
3800
  throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
3763
3801
  }
3764
3802
  ensureSupportedSnippetOperation(args.operation_family, args.method);
3765
- const generated = await generateExecutablePayload({ ...args, publishable_key: key });
3803
+ const generated = addSessionAwarenessMetadata(await generateExecutablePayload({ ...args, publishable_key: key }), args);
3766
3804
  const payload = generated?.payload ?? {};
3767
3805
  const snippet = buildSnippet(args.language, payload, args.operation_family, args.method);
3768
3806
  return {
@@ -3915,7 +3953,7 @@ async function main() {
3915
3953
  ' Step 2 — check if the database component already exists:\n' +
3916
3954
  ' ductape_cli("resources databases list <product_tag> --json")\n' +
3917
3955
  ' If a component already uses the same Atlas cluster, do NOT re-import — instead update it:\n' +
3918
- ' ductape_execute("databases.updateDatabase", [product_tag, db_tag, { envs: [...updated envs...] }])\n' +
3956
+ ' use ductape_cli resource update when supported; otherwise update it in Workbench\n' +
3919
3957
  ' Add or change the dbName in the env\'s connection_url to switch databases on the same cluster.\n' +
3920
3958
  ' Step 3 — import (only if no existing component uses this cluster):\n' +
3921
3959
  ' Use import-persist-all with one entry per product env. Required fields per entry:\n' +
@@ -15,6 +15,7 @@ export interface IGenerateExecutablePayloadRequest {
15
15
  method: string;
16
16
  targets?: Record<string, unknown>;
17
17
  include_session?: boolean;
18
+ execution_context?: 'user' | 'delegated' | 'system';
18
19
  include_cache?: boolean;
19
20
  schema_mode?: 'strict' | 'best_effort';
20
21
  input_hint?: Record<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"proxy-client.d.ts","sourceRoot":"","sources":["../src/proxy-client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,eAAO,MAAM,YAAY,4BAA4B,CAAC;AAEtD,MAAM,MAAM,SAAS,GACjB,SAAS,GACT,KAAK,GACL,WAAW,GACX,OAAO,GACP,UAAU,GACV,eAAe,GACf,gBAAgB,GAChB,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,SAAS,GACT,UAAU,GACV,MAAM,GACN,MAAM,GACN,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,SAAS,CAAC;AAUd;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,GAAG,OAAO,EAC/C,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,CAAC,CAAC,CAuBZ;AAED,MAAM,WAAW,iCAAiC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kCAAkC;IACjD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AASD,wBAAsB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAYvE;AA4BD,wBAAsB,yBAAyB,CAAC,CAAC,GAAG,kCAAkC,EACpF,OAAO,EAAE,iCAAiC,GACzC,OAAO,CAAC,CAAC,CAAC,CAwBZ"}
1
+ {"version":3,"file":"proxy-client.d.ts","sourceRoot":"","sources":["../src/proxy-client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,eAAO,MAAM,YAAY,4BAA4B,CAAC;AAEtD,MAAM,MAAM,SAAS,GACjB,SAAS,GACT,KAAK,GACL,WAAW,GACX,OAAO,GACP,UAAU,GACV,eAAe,GACf,gBAAgB,GAChB,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,SAAS,GACT,UAAU,GACV,MAAM,GACN,MAAM,GACN,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,SAAS,CAAC;AAUd;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,GAAG,OAAO,EAC/C,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,CAAC,CAAC,CAuBZ;AAED,MAAM,WAAW,iCAAiC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACpD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kCAAkC;IACjD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AASD,wBAAsB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAYvE;AA4BD,wBAAsB,yBAAyB,CAAC,CAAC,GAAG,kCAAkC,EACpF,OAAO,EAAE,iCAAiC,GACzC,OAAO,CAAC,CAAC,CAAC,CAwBZ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "description": "MCP server that exposes Ductape SDK operations via the backend proxy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,4 +34,25 @@ assert.doesNotMatch(
34
34
  'MCP guidance must not use the obsolete identify({ userId, traits }) signature',
35
35
  );
36
36
 
37
- console.log(`frontend analytics guidance: ${checks.length + 1} acceptance checks passed`);
37
+ const safetyChecks = [
38
+ ['admin execute calls removed', /ductape_execute\("[^"\n]*(?:create|update)/],
39
+ ['obsolete NestJS consumer removed', /events\.consume in onModuleInit/],
40
+ ['canonical NestJS consumer present', /@Events\.Consumer\(\{ event:/],
41
+ ['request actor context documented', /AsyncLocalStorage[\s\S]*ActorContext/],
42
+ ['raw token logging prohibited', /Never log,[\s\S]*raw session\/refresh token/],
43
+ ['durable token expiry semantics documented', /does not expose an immutable[\s\S]*expired-token attribution contract/],
44
+ ['frontend durable work prohibits persisted JWTs', /For delayed\/durable work, do not persist raw JWTs indefinitely/],
45
+ ['resilience decision matrix present', /DECISION MATRIX[\s\S]*Database atomicity/],
46
+ ['payload session-awareness metadata present', /session_awareness:[\s\S]*accepts_session/],
47
+ ['execute blocks administrative mutations', /Administrative operation.*is blocked in ductape_execute/],
48
+ ];
49
+
50
+ for (const [name, pattern] of safetyChecks) {
51
+ if (name === 'admin execute calls removed' || name === 'obsolete NestJS consumer removed') {
52
+ assert.doesNotMatch(source, pattern, `Unsafe MCP guidance remains: ${name}`);
53
+ } else {
54
+ assert.match(source, pattern, `Missing MCP safety guidance: ${name}`);
55
+ }
56
+ }
57
+
58
+ console.log(`MCP guidance: ${checks.length + 1 + safetyChecks.length} acceptance checks passed`);
package/src/index.ts CHANGED
@@ -943,7 +943,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
943
943
  "storage.create", "storage.update"
944
944
 
945
945
  When to use this tool:
946
- - Before calling ductape_execute to create or update any asset — use this to discover required fields
946
+ - Before constructing an administrative asset file for ductape_cli or Workbench — use this to discover required fields
947
947
  - To enumerate valid enum values (e.g. which DatabaseTypes or AuthTypes are accepted)
948
948
  - To understand nested object shapes without reading SDK docs
949
949
  `;
@@ -998,6 +998,11 @@ const payloadGenerateInputSchema = z.object({
998
998
  'Include a session placeholder inside the generated input object. ' +
999
999
  'The placeholder is named "<session_tag_token>" to indicate it expects the runtime JWT, not the tag name.'
1000
1000
  ),
1001
+ execution_context: z.enum(['user', 'delegated', 'system']).optional().default('user').describe(
1002
+ 'Actor intent for this runtime operation. "user" means an active request initiated by the authenticated user; ' +
1003
+ '"delegated" means work acting on behalf of a user with a short-lived delegated identity or application-owned ' +
1004
+ 'immutable actor context; "system" means intentionally unattributed background work. This drives session warnings.'
1005
+ ),
1001
1006
  include_cache: z.boolean().optional().default(true).describe(
1002
1007
  'Include the cache tag inside the generated input object so the caller knows which cache to reference for this query.'
1003
1008
  ),
@@ -1091,6 +1096,53 @@ function buildSdkInvocationArgs(payload: Record<string, unknown>): Record<string
1091
1096
  };
1092
1097
  }
1093
1098
 
1099
+ function operationAcceptsSession(operationFamily: string, method: string): boolean {
1100
+ const family = operationFamily.toLowerCase();
1101
+ const sessionFamilies = new Set([
1102
+ 'action', 'features', 'feature', 'database', 'graph', 'vector', 'storage',
1103
+ 'notification', 'messaging', 'broker', 'events', 'event', 'quota', 'fallback',
1104
+ ]);
1105
+ if (!sessionFamilies.has(family)) return false;
1106
+ return !['consume', 'status', 'check', 'fetch', 'list'].includes(method.toLowerCase());
1107
+ }
1108
+
1109
+ function addSessionAwarenessMetadata(
1110
+ generated: any,
1111
+ args: z.infer<typeof payloadGenerateInputSchema>,
1112
+ ): any {
1113
+ const acceptsSession = operationAcceptsSession(args.operation_family, args.method);
1114
+ const executionContext = args.execution_context ?? 'user';
1115
+ const payloadHasSession = Boolean(generated?.payload?.session || generated?.payload?.input?.session);
1116
+ const sessionRequested = args.include_session !== false;
1117
+ const warnings: string[] = [];
1118
+
1119
+ if (acceptsSession && executionContext !== 'system' && (!sessionRequested || !payloadHasSession)) {
1120
+ warnings.push(
1121
+ `Session attribution is missing for a ${executionContext}-context operation. ` +
1122
+ 'Pass the original full session token for immediate user work, or an approved delegated identity/immutable actor context for delayed work.',
1123
+ );
1124
+ }
1125
+ if (executionContext === 'system' && payloadHasSession) {
1126
+ warnings.push(
1127
+ 'This operation is marked system-context but contains a session placeholder. Remove it unless the work is actually user/delegated.',
1128
+ );
1129
+ }
1130
+
1131
+ generated.meta = {
1132
+ ...(generated?.meta ?? {}),
1133
+ session_awareness: {
1134
+ accepts_session: acceptsSession,
1135
+ execution_context: executionContext,
1136
+ appears_user_context: executionContext === 'user',
1137
+ session_requested: sessionRequested,
1138
+ session_present_in_payload: payloadHasSession,
1139
+ intentionally_system_context: executionContext === 'system',
1140
+ warnings,
1141
+ },
1142
+ };
1143
+ return generated;
1144
+ }
1145
+
1094
1146
  function buildTypeScriptSnippet(
1095
1147
  payload: Record<string, unknown>,
1096
1148
  operationFamily: string,
@@ -1462,33 +1514,9 @@ DUCTAPE DATABASE MIGRATIONS
1462
1514
 
1463
1515
  Migrations are versioned SQL/NoSQL schema change scripts managed per database component.
1464
1516
 
1465
- Create a migration:
1466
- ductape_execute("databases.migration.create", [{
1467
- product: "my-product",
1468
- database: "core-db",
1469
- data: {
1470
- name: "add users table",
1471
- tag: "001-add-users",
1472
- value: {
1473
- up: ["CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE)"],
1474
- down: ["DROP TABLE users"],
1475
- },
1476
- },
1477
- }])
1478
-
1479
- Run migrations (applies all pending):
1480
- ductape_execute("databases.migration.run", [migrations, { env: "prd" }])
1517
+ Create migrations through the project migration files and ductape_cli, never ductape_execute.
1481
1518
 
1482
- Rollback:
1483
- ductape_execute("databases.migration.rollback", [migrations, 1]) // roll back 1
1484
-
1485
- Check status:
1486
- ductape_execute("databases.migration.status", [migrations])
1487
-
1488
- History:
1489
- ductape_execute("databases.migration.history", [])
1490
-
1491
- Via CLI:
1519
+ Use the access-key administrative CLI for running, rolling back, and inspecting migrations:
1492
1520
  ductape_cli("db migrate") // run pending
1493
1521
  ductape_cli("db migrate rollback") // roll back last
1494
1522
  ductape_cli("db migrate rollback -n 3")
@@ -1499,25 +1527,13 @@ MongoDB: migrations run as raw Mongo shell commands in the up/down arrays.
1499
1527
  indexes: `
1500
1528
  DUCTAPE DATABASE INDEXES
1501
1529
 
1502
- Create an index:
1503
- ductape_execute("databases.schema.createIndex", [
1504
- "collection_name",
1505
- ["field1", "field2"], // simple array of field names
1506
- { unique: true, name: "idx_email_unique" } // options (optional)
1507
- ])
1530
+ Create or update indexes through migration files and ductape_cli("db migrate"), not the
1531
+ publishable-key runtime proxy. Example definitions may use ["field1", "field2"] with
1532
+ { unique: true, name: "idx_email_unique" }, or ordered fields such as
1533
+ [{ field: "createdAt", order: "DESC" }, { field: "userId", order: "ASC" }].
1508
1534
 
1509
- // Ordered index:
1510
- ductape_execute("databases.schema.createIndex", [
1511
- "orders",
1512
- [{ field: "createdAt", order: "DESC" }, { field: "userId", order: "ASC" }],
1513
- { name: "idx_orders_date_user" }
1514
- ])
1515
-
1516
- Drop an index:
1517
- ductape_execute("databases.schema.dropIndex", ["collection_name", "index_name"])
1518
-
1519
- List indexes on a collection:
1520
- ductape_execute("databases.schema.indexes", ["collection_name"])
1535
+ Drop and inspect indexes through migration/CLI tooling or the database administration surface,
1536
+ not ductape_execute.
1521
1537
 
1522
1538
  Performance guidance:
1523
1539
  - Index fields used in WHERE clauses, JOIN conditions, and ORDER BY.
@@ -1563,18 +1579,7 @@ DUCTAPE DATABASE ACTIONS
1563
1579
  A database action is a saved query or mutation (SQL string or NoSQL command) stored
1564
1580
  on the Ductape product and executed by tag at runtime.
1565
1581
 
1566
- Create an action (admin):
1567
- ductape_execute("databases.action.create", [{
1568
- product: "my-product",
1569
- database: "core-db",
1570
- data: {
1571
- tag: "get-active-users",
1572
- name: "Get active users",
1573
- description: "Returns all users with status=active",
1574
- type: "sql", // "sql" or "nosql"
1575
- query: "SELECT * FROM users WHERE status = :status",
1576
- },
1577
- }])
1582
+ Create/update actions in Workbench (administrative access), never through ductape_execute.
1578
1583
 
1579
1584
  Dispatch an action at runtime:
1580
1585
  → CALL ductape_generate_payload FIRST to get the canonical input shape.
@@ -1589,10 +1594,9 @@ Dispatch an action at runtime:
1589
1594
  List actions for a database:
1590
1595
  ductape_execute("databases.action.list", ["database_tag"])
1591
1596
 
1592
- Fetch / update / delete:
1597
+ Fetch:
1593
1598
  ductape_execute("databases.action.fetch", ["action_tag"])
1594
- ductape_execute("databases.action.update", ["my-product", "action_tag", { query: "..." }])
1595
- ductape_execute("databases.action.delete", ["action_tag"])
1599
+ Update/delete are administrative and must be performed in Workbench.
1596
1600
 
1597
1601
  Actions are the preferred way to encapsulate complex or reused queries — they can be
1598
1602
  scheduled, dispatched with retries, and audited via logs.
@@ -1856,16 +1860,8 @@ DUCTAPE SECRETS
1856
1860
  Secrets are workspace-level encrypted key-value pairs. They are referenced in resource configs,
1857
1861
  connection URLs, and any string field using the $Secret{KEY_NAME} syntax.
1858
1862
 
1859
- Create (admin ductape_cli or ductape_execute):
1860
- ductape_execute("secrets.create", [{
1861
- key: "STRIPE_API_KEY",
1862
- value: "sk_live_...", // plaintext — encrypted AES-256-GCM client-side before send
1863
- description: "Stripe live key",
1864
- token_type: "api", // "api" | "password" | "certificate"
1865
- scope: ["my-product"], // which products can read this secret
1866
- envs: ["prd"], // which env slugs can read this secret
1867
- expires_at: 1800000000, // optional epoch ms
1868
- }])
1863
+ Create/update secrets with ductape_cli or Workbench (administrative access), never ductape_execute.
1864
+ Definition fields include key, value, description, token_type, scope, envs, and expires_at.
1869
1865
  The server never receives the plaintext value. Encryption uses the workspace private key.
1870
1866
 
1871
1867
  Fetch / resolve:
@@ -1880,10 +1876,7 @@ $Secret{} reference syntax:
1880
1876
  - The in-memory cache stores the encrypted form only; decryption happens on each cache hit.
1881
1877
  - Cache TTL: 5 minutes. Clear with: secrets.clearCache()
1882
1878
 
1883
- Lifecycle:
1884
- ductape_execute("secrets.revoke", ["KEY"]) → disables without deleting (recoverable)
1885
- ductape_execute("secrets.delete", ["KEY"]) → permanent deletion (irreversible)
1886
- ductape_execute("secrets.update", ["KEY", { value: "new_value", expires_at: ... }])
1879
+ Lifecycle mutations (revoke/delete/update) are administrative: use ductape_cli or Workbench.
1887
1880
 
1888
1881
  List all secrets (keys only — values are not returned in list):
1889
1882
  ductape_execute("secrets.list", [])
@@ -1934,9 +1927,7 @@ Import from a file:
1934
1927
  ductape_cli("apps import <file.json> -t postman|openapi")
1935
1928
  Supports Postman v2.1 collection and OpenAPI 3.0 spec.
1936
1929
 
1937
- Manage environments (base URLs per stage):
1938
- ductape_execute("app.environments.create", [app_tag, { slug, env_name, base_url }])
1939
- ductape_execute("app.environments.list", [app_tag])
1930
+ Manage environments (base URLs per stage) in Workbench; this currently has no CLI command.
1940
1931
 
1941
1932
  Discover apps in a product and their actions (ALWAYS do this before writing any ctx.api.run call):
1942
1933
  Step 1 — list apps connected to the product:
@@ -1953,16 +1944,8 @@ Discover apps in a product and their actions (ALWAYS do this before writing any
1953
1944
  app resources (not for runtime input — use actions.fetch or ductape_generate_payload for that)
1954
1945
  NEVER assume action input field names. Always fetch the action definition first.
1955
1946
 
1956
- Manage actions (individual API endpoints):
1957
- ductape_execute("actions.create", [app_tag, {
1958
- tag, name, resource: "/users/{id}", method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE",
1959
- body?: { fieldName: { type, required? } },
1960
- params?: { id: { type: "string" } },
1961
- query?: { filter: { type: "string" } },
1962
- headers?: { Authorization: { type: "string" } },
1963
- response?: { status_code: 200, success: true, body: { ... }, response_format: "json" },
1964
- }])
1965
- ductape_execute("actions.update", [app_tag, action_tag, data])
1947
+ Manage actions (individual API endpoints) in Workbench or import an OpenAPI/Postman file.
1948
+ Creation/update are administrative and must never use ductape_execute.
1966
1949
  ductape_execute("actions.list", [app_tag])
1967
1950
  ductape_execute("actions.fetch", [app_tag, action_tag])
1968
1951
 
@@ -1983,16 +1966,14 @@ Run an action at runtime:
1983
1966
 
1984
1967
  Auth schemes (how the app authenticates outbound requests):
1985
1968
  Setup types: header | bearer | basic | oauth2 | apikey
1986
- ductape_execute("auths.create", [app_tag, { tag, name, setup_type, expiry, period, action_tag? }])
1969
+ Configure auth in Workbench (administrative).
1987
1970
  ductape_execute("auths.list", [app_tag])
1988
1971
 
1989
1972
  Webhooks (inbound events from the external service):
1990
- ductape_execute("webhooks.create", [app_tag, { tag, name, description, envs: [{ slug, registration_url?, method? }] }])
1991
- ductape_execute("webhooks.events.create", [app_tag, { tag, name, selector, description, sample }])
1973
+ Configure webhooks and webhook events in Workbench (administrative).
1992
1974
 
1993
1975
  Variables (per-env mutable values) and Constants (fixed values):
1994
- ductape_execute("app.variables.create", [app_tag, { key, value, env_slug }])
1995
- ductape_execute("app.constants.create", [app_tag, { key, value }])
1976
+ Configure variables and constants in Workbench (administrative).
1996
1977
 
1997
1978
  Connecting an app to a product (after creation):
1998
1979
  NOTE: All product.* module methods require the access key and CANNOT use ductape_execute.
@@ -2000,14 +1981,13 @@ Connecting an app to a product (after creation):
2000
1981
 
2001
1982
  FULL FLOW to make an app callable from a product:
2002
1983
  1. Create the app: ductape_cli("apps create --name \\"Service Name\\" --description \\"...\\"")
2003
- 2. Add environments: ductape_execute("app.environments.create", [app_tag, { slug: "prd", env_name: "Production", base_url: "https://api.example.com" }])
2004
- 3. Configure auth: ductape_execute("auths.create", [app_tag, { tag, name, setup_type: "apikey"|"bearer"|"basic"|"oauth2", expiry, period }])
2005
- 4. Define actions: ductape_execute("actions.create", [app_tag, { tag, name, resource, method, body?, params?, query?, headers?, response? }])
1984
+ 2. Add environments: Workbench
1985
+ 3. Configure auth: Workbench
1986
+ 4. Define actions: Workbench
2006
1987
  OR import: ductape_cli("apps import <file.json> -t postman|openapi")
2007
1988
  5. Connect to product: Requires the product_id (from ductape_cli("products get --tag <tag> --json")).
2008
1989
  There is no CLI command for this step — the SDK product.apps.add method requires
2009
- an access key which only the backend can provide. Use ductape_execute via an
2010
- admin-authenticated context, or connect via the Workbench UI.
1990
+ an access key which only the backend can provide. Connect via Workbench.
2011
1991
  6. Verify: ductape_cli("products get --tag <product_tag> --json") → check apps[] contains the app
2012
1992
  ductape_execute("actions.list", [app_tag]) → verify actions are registered
2013
1993
  `.trim(),
@@ -2084,8 +2064,8 @@ IMPORTANT — selector must be "$Session{fieldName}" format:
2084
2064
  CORRECT: selector: "$Session{playerId}"
2085
2065
  INCORRECT: selector: "playerId" ← WILL FAIL with "Selector should be in the format $Session{...}{key}"
2086
2066
 
2087
- Example (game product player identity in JWT):
2088
- ductape_execute("sessions.create", [product_tag, {
2067
+ Example definition (configure via declarative apply or Workbench, not ductape_execute):
2068
+ {
2089
2069
  tag: "player-session",
2090
2070
  name: "Player Session",
2091
2071
  expiry: 24,
@@ -2097,7 +2077,7 @@ Example (game product — player identity in JWT):
2097
2077
  role: "player",
2098
2078
  accountId: "acct_xyz",
2099
2079
  },
2100
- }])
2080
+ }
2101
2081
 
2102
2082
  Runtime — create a session (sign a JWT):
2103
2083
  → CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
@@ -2137,10 +2117,33 @@ Token format: "session_tag:jwt_token" — pass this format verbatim wherever ses
2137
2117
  JWT is signed with the product private key (not a symmetric shared secret).
2138
2118
 
2139
2119
  SESSION PROPAGATION
2140
- Preserve the original full token on every user-context database, Event, Feature, notification,
2141
- storage, graph, vector, action, and other runtime operation that accepts session. Do not replace
2142
- it with the session tag, user ID, decoded claims, or refresh token. Durable user-initiated work
2143
- should retain the initiating token; intentional system/background work should be sessionless.
2120
+ Extract the original full "session_tag:jwt" token once at the HTTP/transport boundary and retain
2121
+ it separately from verified/decoded claims. Claims are authorization data; they are not a token.
2122
+ Store a request ActorContext in a NestJS request-scoped provider or AsyncLocalStorage:
2123
+ type ActorContext =
2124
+ | { kind: "user"; session: string; actorId: string }
2125
+ | { kind: "delegated"; delegatedIdentity: string; actorId: string; initiatedAt: string }
2126
+ | { kind: "system"; reason: string };
2127
+ Pass ActorContext explicitly through service boundaries. At each immediate Ductape runtime call,
2128
+ pass actor.kind === "user" ? actor.session : the SDK-approved delegated identity. Put only
2129
+ non-secret actor metadata (actorId, kind, initiatedAt, correlationId) in Event envelopes.
2130
+ Feature execution should receive the same explicit actor classification. Never log, serialize
2131
+ into business payloads, or attach the raw session/refresh token to traces or error messages.
2132
+
2133
+ SECURITY BOUNDARY FOR DURABLE WORK
2134
+ The current SDK accepts a session string on dispatch, but it does not expose an immutable
2135
+ attribution snapshot API, an expired-token attribution contract, or a general delegated-session
2136
+ issuer. Therefore do NOT persist reusable JWTs indefinitely or assume an expired initiating token
2137
+ will remain valid when delayed work executes. Pass the full token only for immediate work within
2138
+ its validity window. For delayed/recurring work, classify execution as system-context, or use an
2139
+ application-owned immutable actor-context envelope plus a short-lived delegated identity issued
2140
+ by an explicitly approved auth design. Until the SDK provides that design, actor metadata is for
2141
+ audit/correlation and must not be treated as authorization.
2142
+
2143
+ Distinguish contexts deliberately:
2144
+ user — active request; original valid full token is required when the operation accepts it
2145
+ delegated — delayed/on-behalf-of work; approved short-lived credential + immutable actor metadata
2146
+ system — scheduler/maintenance with no user authority; omit session and record a reason
2144
2147
 
2145
2148
  Backend propagation attributes component activity to an actor, but it does not record frontend
2146
2149
  pageviews, navigation, UI intent, funnels, or client failures. Continue immediately with
@@ -2152,14 +2155,12 @@ DUCTAPE CACHES
2152
2155
 
2153
2156
  Caches are product-level Redis (or in-memory) stores for temporary key-value data with optional TTL.
2154
2157
 
2155
- Registration (admin — ductape_cli or SDK):
2158
+ Registration (admin — ductape_cli):
2156
2159
  ductape_cli("resources caches create -f cache.json")
2157
2160
  File: { name, tag, description?, expiry: <milliseconds> }
2158
2161
  No type or envs — Ductape manages the store infrastructure.
2159
2162
  expiry is in MILLISECONDS: 3600000 = 1 hour, 86400000 = 1 day, 604800000 = 1 week.
2160
2163
 
2161
- SDK: ductape_execute("caches.create", [product_tag, { name, tag, description?, expiry: 3600000 }])
2162
-
2163
2164
  Operations:
2164
2165
  caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
2165
2166
  expiry is an ABSOLUTE TIMESTAMP (not a duration).
@@ -2208,20 +2209,20 @@ DUCTAPE NOTIFICATIONS
2208
2209
 
2209
2210
  Notifications send messages across multiple channels: email, SMS, push, or HTTP callback.
2210
2211
 
2211
- Create a notification (admin ductape_execute):
2212
- ductape_execute("notifications.create", [product_tag, {
2212
+ Create a notification through declarative apply or Workbench (administrative), using:
2213
+ {
2213
2214
  tag: "welcome-email",
2214
2215
  name: "Welcome Email",
2215
2216
  type: "email", // optional hint; actual channels configured per env
2216
- }])
2217
+ }
2217
2218
 
2218
- Create a message template:
2219
- ductape_execute("notifications.messages.create", [product_tag, {
2219
+ Create a message template through declarative apply or Workbench:
2220
+ {
2220
2221
  tag: "welcome-email:default", // format: "notification_tag:message_tag"
2221
2222
  notification: "welcome-email",
2222
2223
  subject: { template: "Welcome, {{name}}!", data: { name: "" } },
2223
2224
  body: { template: "Hi {{name}}, thanks for signing up.", data: { name: "" } },
2224
- }])
2225
+ }
2225
2226
 
2226
2227
  Send at runtime (one channel at a time):
2227
2228
  → CALL ductape_generate_payload FIRST (operation_family="notification", method="email.send")
@@ -2260,8 +2261,15 @@ DUCTAPE RESILIENCE
2260
2261
  Resilience covers three mechanisms: quotas (rate-limited provider pools), fallbacks (automatic
2261
2262
  provider switching), and healthchecks (continuous probe monitoring with failure actions).
2262
2263
 
2264
+ CONFIGURATION BOUNDARY
2265
+ Quotas, fallbacks, and health checks are administrative product configuration. Configure them in
2266
+ Workbench (or a future access-key administrative tool explicitly documented for the asset).
2267
+ Never route administrative create/update methods through ductape_execute: its publishable-key
2268
+ runtime proxy will fail.
2269
+
2263
2270
  QUOTAS — rate-limited multi-provider pools:
2264
- ductape_execute("quotas.create", [product_tag, {
2271
+ Workbench definition shape:
2272
+ {
2265
2273
  tag: "sms-quota",
2266
2274
  name: "SMS Provider Pool",
2267
2275
  input: { to: { type: "string", required: true }, message: { type: "string" } },
@@ -2275,7 +2283,7 @@ QUOTAS — rate-limited multi-provider pools:
2275
2283
  input: { "body:to": "$Input{to}", "body:body": "$Input{message}" },
2276
2284
  output: {} },
2277
2285
  ],
2278
- }])
2286
+ }
2279
2287
  Providers are tried in order until quota is not exhausted.
2280
2288
  quotas.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
2281
2289
  quotas.dispatch [{ product, env, tag, input, schedule? }]
@@ -2283,12 +2291,13 @@ QUOTAS — rate-limited multi-provider pools:
2283
2291
  FALLBACKS — automatic provider switching on failure:
2284
2292
  Same schema as quotas but options are ordered: primary first, then fallback(s).
2285
2293
  Primary is used first; on failure, the next provider is tried automatically.
2286
- ductape_execute("fallback.create", [product_tag, { tag, name, input: { ... }, options: [...] }])
2294
+ Configure in Workbench: { tag, name, input: { ... }, options: [...] }
2287
2295
  fallback.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
2288
2296
  fallback.dispatch [{ product, env, tag, input, schedule? }]
2289
2297
 
2290
2298
  HEALTHCHECKS — continuous probe with failure notifications:
2291
- ductape_execute("health.create", [product_tag, {
2299
+ Workbench definition shape:
2300
+ {
2292
2301
  tag: "payment-health",
2293
2302
  name: "Payment Service Health",
2294
2303
  probe: { type: "app", app: "stripe-app", event: "ping" },
@@ -2300,7 +2309,7 @@ HEALTHCHECKS — continuous probe with failure notifications:
2300
2309
  channels: { email: { recipients: ["ops@example.com"] } } }],
2301
2310
  webhooks: [{ url: "https://hooks.example.com/alert", method: "POST" }],
2302
2311
  },
2303
- }])
2312
+ }
2304
2313
  health.run [{ product, env, tag }] → triggers an immediate probe
2305
2314
  health.check [{ product, env, tag }] → same as run
2306
2315
  health.status [{ product, env, tag }] → current health status
@@ -2310,6 +2319,23 @@ Failure actions: notification channels, HTTP webhooks, and/or message broker emi
2310
2319
  be configured simultaneously on the same healthcheck.
2311
2320
  Input template references: $Input{field} → maps declared input to the probe's action input.
2312
2321
  Provider status: available | unavailable
2322
+
2323
+ DECISION MATRIX
2324
+ Rate/capacity allocation across providers → quota
2325
+ Equivalent provider after operational failure → fallback
2326
+ Detect failure before routing provider traffic → health check
2327
+ Transient failure of one operation → bounded retry + idempotency policy
2328
+ Multi-step business recovery/compensation → Feature
2329
+ Database atomicity → database transaction, not Feature rollback
2330
+ Scheduled single operation → that component's dispatch
2331
+ Scheduled multi-step process → Feature dispatch
2332
+
2333
+ COMBINING MECHANISMS
2334
+ A health check may keep an unhealthy provider out of a fallback/quota pool; the pool controls
2335
+ provider selection; each operation may use bounded retries and an idempotency key; a Feature
2336
+ coordinates business steps and compensation around those resilient operations. Do not stack them
2337
+ reflexively: each layer needs a distinct failure it owns, bounded retry budgets, and observable
2338
+ terminal behavior. Database transactions remain the atomicity boundary for related DB writes.
2313
2339
  `.trim(),
2314
2340
 
2315
2341
  'frontend-analytics': `
@@ -2318,10 +2344,12 @@ DUCTAPE FRONTEND PRODUCT ANALYTICS AND SESSION ACTIVITY
2318
2344
  Frontend analytics and backend session attribution are complementary; neither replaces the other.
2319
2345
 
2320
2346
  BACKEND OPERATION ATTRIBUTION
2321
- Pass the original full Ductape session token to every user-initiated database, Event, Feature,
2322
- notification, storage, graph, vector, action, and other runtime operation that accepts session.
2323
- This attributes server work to the authenticated actor. Durable work should retain the initiating
2324
- session; intentional system/background work should remain sessionless.
2347
+ Immediate user-initiated backend work should pass the original full Ductape session token to every
2348
+ database, Event, Feature, notification, storage, graph, vector, action, and other runtime operation
2349
+ that accepts session. This attributes immediate server work to the authenticated actor.
2350
+ For delayed/durable work, do not persist raw JWTs indefinitely. Use system context, or an approved
2351
+ delegated actor design with immutable non-secret actor metadata. Actor metadata supports audit and
2352
+ correlation; it is not authorization. Intentional system/background work should remain sessionless.
2325
2353
 
2326
2354
  FRONTEND PRODUCT ANALYTICS
2327
2355
  Use the browser Analytics service for anonymous visits, authenticated page views, navigation,
@@ -2591,9 +2619,11 @@ STEP 4 — PRESENT the plan and get approval BEFORE writing any code or creating
2591
2619
  Wait for confirmation before writing code or calling any create tool.
2592
2620
 
2593
2621
  STEP 5 — CREATE missing components (only with user approval)
2594
- Use ductape_execute for any missing databases, apps, database actions, notification events, etc.
2595
- For a missing database action:
2596
- ductape_execute("databases.action.create", [product_tag, db_tag, { tag, name, type, query }])
2622
+ Administrative assets must never be created through ductape_execute. Use ductape_cli for products,
2623
+ apps, supported resources, broker topics, cloud connections, secrets, and declarative apply flows.
2624
+ App actions, auths, Features, quotas, fallbacks, health checks, and other assets for which the CLI
2625
+ has no command must be configured in Workbench. Do not generate an impossible publishable-key
2626
+ create/update call. For a missing database action, configure it in Workbench, then verify it exists.
2597
2627
  For a missing child feature, recursively apply this same workflow.
2598
2628
  Tell the user what you are about to create before each tool call.
2599
2629
 
@@ -2717,7 +2747,11 @@ When you call features.define({ handler }), the handler runs TWICE:
2717
2747
 
2718
2748
  Invoke internal application business logic (your own NestJS/backend service code):
2719
2749
  → produce a broker event (ctx.messaging.produce or ductape.events.produce)
2720
- consume it in your NestJS service with events.consume in onModuleInit
2750
+ follow ductape_docs({ topic: "events" }) and use the canonical NestJS decorator:
2751
+ @Events.Consumer({ event: "broker-tag:topic-tag" })
2752
+ async handle(message: MessageShape) { /* injected-service business logic; throw to nack */ }
2753
+ → DuctapeModule auto-registers the decorated consumer; no manual onModuleInit is needed
2754
+ → events.consume() remains a supported lower-level alternative for plain TypeScript/Node.js
2721
2755
  → your service method runs with full access to DI, DB transactions, etc.
2722
2756
  Do NOT create an App Action just to call your own service over HTTP.
2723
2757
 
@@ -3789,6 +3823,25 @@ async function main() {
3789
3823
 
3790
3824
  const executeHandler = async (args: { publishable_key?: string; module: SDKModule; method: string; params: unknown[] }) => {
3791
3825
  try {
3826
+ const runtimeMutationMethods: Partial<Record<SDKModule, Set<string>>> = {
3827
+ databases: new Set(['insert', 'update', 'delete', 'upsert']),
3828
+ graph: new Set(['insert', 'update', 'delete']),
3829
+ vector: new Set(['insert', 'upsert', 'upsertOne', 'delete']),
3830
+ sessions: new Set(['revoke']),
3831
+ };
3832
+ const isRuntimeDataMutation = runtimeMutationMethods[args.module]?.has(args.method) === true;
3833
+ const isAdministrativeMutation =
3834
+ (/^migration\./.test(args.method) ||
3835
+ /^schema\.(create|drop|add|remove|update)/.test(args.method) ||
3836
+ /(^|\.)(create|update|delete|configure|add|remove|revoke)$/.test(args.method)) &&
3837
+ !isRuntimeDataMutation;
3838
+ if (isAdministrativeMutation) {
3839
+ throw new Error(
3840
+ `Administrative operation "${args.module}.${args.method}" is blocked in ductape_execute. ` +
3841
+ 'Use ductape_cli when that resource is supported by the CLI; otherwise configure it in Workbench. ' +
3842
+ 'The runtime proxy uses a publishable key and cannot administer platform assets.',
3843
+ );
3844
+ }
3792
3845
  const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
3793
3846
  if (!key) {
3794
3847
  throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
@@ -3839,7 +3892,10 @@ async function main() {
3839
3892
  if (!key) {
3840
3893
  throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
3841
3894
  }
3842
- const result = await generateExecutablePayload({ ...args, publishable_key: key });
3895
+ const result = addSessionAwarenessMetadata(
3896
+ await generateExecutablePayload({ ...args, publishable_key: key }),
3897
+ args,
3898
+ );
3843
3899
 
3844
3900
  let text = JSON.stringify(result ?? null, null, 2);
3845
3901
 
@@ -3871,7 +3927,10 @@ async function main() {
3871
3927
  throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
3872
3928
  }
3873
3929
  ensureSupportedSnippetOperation(args.operation_family, args.method);
3874
- const generated = await generateExecutablePayload({ ...args, publishable_key: key });
3930
+ const generated = addSessionAwarenessMetadata(
3931
+ await generateExecutablePayload({ ...args, publishable_key: key }),
3932
+ args,
3933
+ );
3875
3934
  const payload = (generated as any)?.payload ?? {};
3876
3935
  const snippet = buildSnippet(args.language, payload, args.operation_family, args.method);
3877
3936
  return {
@@ -4055,7 +4114,7 @@ async function main() {
4055
4114
  ' Step 2 — check if the database component already exists:\n' +
4056
4115
  ' ductape_cli("resources databases list <product_tag> --json")\n' +
4057
4116
  ' If a component already uses the same Atlas cluster, do NOT re-import — instead update it:\n' +
4058
- ' ductape_execute("databases.updateDatabase", [product_tag, db_tag, { envs: [...updated envs...] }])\n' +
4117
+ ' use ductape_cli resource update when supported; otherwise update it in Workbench\n' +
4059
4118
  ' Add or change the dbName in the env\'s connection_url to switch databases on the same cluster.\n' +
4060
4119
  ' Step 3 — import (only if no existing component uses this cluster):\n' +
4061
4120
  ' Use import-persist-all with one entry per product env. Required fields per entry:\n' +
@@ -76,6 +76,7 @@ export interface IGenerateExecutablePayloadRequest {
76
76
  method: string;
77
77
  targets?: Record<string, unknown>;
78
78
  include_session?: boolean;
79
+ execution_context?: 'user' | 'delegated' | 'system';
79
80
  include_cache?: boolean;
80
81
  schema_mode?: 'strict' | 'best_effort';
81
82
  input_hint?: Record<string, unknown>;