@agent-native/core 0.131.6 → 0.131.7

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.
Files changed (76) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/docs/content/deployment.mdx +12 -10
  4. package/corpus/core/docs/content/security.mdx +2 -2
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/a2a/invoke.ts +4 -2
  7. package/corpus/core/src/agent/action-continuation-guidance.ts +2 -1
  8. package/corpus/core/src/agent/engine/registry.ts +61 -9
  9. package/corpus/core/src/guards/no-env-credentials.ts +2 -0
  10. package/corpus/core/src/scripts/call-agent.ts +4 -2
  11. package/corpus/core/src/secrets/crypto.ts +44 -15
  12. package/corpus/core/src/secrets/storage.ts +6 -4
  13. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +2 -0
  14. package/corpus/core/src/templates/default/.agents/skills/secrets/SKILL.md +14 -9
  15. package/corpus/core/src/templates/headless/.agents/skills/secrets/SKILL.md +14 -9
  16. package/corpus/core/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +14 -9
  17. package/corpus/templates/analytics/AGENTS.md +1 -0
  18. package/corpus/templates/analytics/changelog/2026-07-30-delegated-analytics-questions-now-return-a-useful-current-me.md +6 -0
  19. package/corpus/templates/analytics/server/lib/real-data-actions.ts +10 -0
  20. package/corpus/templates/plan/actions/get-visual-plan.ts +10 -6
  21. package/corpus/templates/plan/actions/patch-visual-plan-source.ts +117 -1
  22. package/corpus/templates/plan/actions/update-visual-plan.ts +56 -14
  23. package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -1
  24. package/corpus/templates/plan/changelog/2026-07-30-plan-agents-recover-with-targeted-writes.md +6 -0
  25. package/corpus/templates/plan/shared/plan-content.ts +35 -0
  26. package/corpus/templates/slides/actions/create-deck.ts +1 -1
  27. package/corpus/templates/slides/changelog/2026-07-30-new-decks-now-start-with-an-empty-slide-list-when-no-slides-.md +6 -0
  28. package/dist/a2a/invoke.d.ts.map +1 -1
  29. package/dist/a2a/invoke.js +4 -2
  30. package/dist/a2a/invoke.js.map +1 -1
  31. package/dist/agent/action-continuation-guidance.d.ts.map +1 -1
  32. package/dist/agent/action-continuation-guidance.js +2 -1
  33. package/dist/agent/action-continuation-guidance.js.map +1 -1
  34. package/dist/agent/engine/registry.d.ts.map +1 -1
  35. package/dist/agent/engine/registry.js +59 -12
  36. package/dist/agent/engine/registry.js.map +1 -1
  37. package/dist/collab/struct-routes.d.ts +1 -1
  38. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  39. package/dist/guards/no-env-credentials.d.ts.map +1 -1
  40. package/dist/guards/no-env-credentials.js +2 -0
  41. package/dist/guards/no-env-credentials.js.map +1 -1
  42. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  43. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  44. package/dist/notifications/routes.d.ts +6 -6
  45. package/dist/scripts/call-agent.d.ts.map +1 -1
  46. package/dist/scripts/call-agent.js +4 -2
  47. package/dist/scripts/call-agent.js.map +1 -1
  48. package/dist/secrets/crypto.d.ts +14 -9
  49. package/dist/secrets/crypto.d.ts.map +1 -1
  50. package/dist/secrets/crypto.js +42 -16
  51. package/dist/secrets/crypto.js.map +1 -1
  52. package/dist/secrets/storage.d.ts +6 -4
  53. package/dist/secrets/storage.d.ts.map +1 -1
  54. package/dist/secrets/storage.js +6 -4
  55. package/dist/secrets/storage.js.map +1 -1
  56. package/dist/server/agent-chat/action-filters-a2a.d.ts +1 -1
  57. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  58. package/dist/server/agent-chat/action-filters-a2a.js +2 -0
  59. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  60. package/dist/templates/default/.agents/skills/secrets/SKILL.md +14 -9
  61. package/dist/templates/headless/.agents/skills/secrets/SKILL.md +14 -9
  62. package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +14 -9
  63. package/docs/content/deployment.mdx +12 -10
  64. package/docs/content/security.mdx +2 -2
  65. package/package.json +3 -3
  66. package/src/a2a/invoke.ts +4 -2
  67. package/src/agent/action-continuation-guidance.ts +2 -1
  68. package/src/agent/engine/registry.ts +61 -9
  69. package/src/guards/no-env-credentials.ts +2 -0
  70. package/src/scripts/call-agent.ts +4 -2
  71. package/src/secrets/crypto.ts +44 -15
  72. package/src/secrets/storage.ts +6 -4
  73. package/src/server/agent-chat/action-filters-a2a.ts +2 -0
  74. package/src/templates/default/.agents/skills/secrets/SKILL.md +14 -9
  75. package/src/templates/headless/.agents/skills/secrets/SKILL.md +14 -9
  76. package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +14 -9
@@ -227,15 +227,20 @@ Core routes plugin mounts these under `/_agent-native/secrets/` automatically:
227
227
  prefer `<APP_NAME>_SECRETS_ENCRYPTION_KEY` (for example,
228
228
  `ANALYTICS_SECRETS_ENCRYPTION_KEY`), then `SECRETS_ENCRYPTION_KEY`, then
229
229
  `BETTER_AUTH_SECRET`.
230
- - Workspace-shared `app_secrets` prefer `SECRETS_ENCRYPTION_KEY`, then a
231
- purpose-derived key from `A2A_SECRET`. Better Auth and app-scoped keys remain
232
- legacy read candidates; a successful legacy decrypt is compare-and-swap
233
- migrated to the preferred shared key without changing the row timestamp.
234
- - Set the same stable `SECRETS_ENCRYPTION_KEY` in every app that reads a shared
235
- vault. If the vault relies on `A2A_SECRET`, rotating A2A material also rotates
236
- its encryption key. Before rotating A2A, add the stable vault key everywhere
237
- and read/migrate every existing row while the old A2A material is still
238
- available.
230
+ - Workspace-shared `app_secrets` prefer
231
+ `WORKSPACE_SECRETS_ENCRYPTION_KEY`, then the legacy shared
232
+ `SECRETS_ENCRYPTION_KEY`, then a purpose-derived key from `A2A_SECRET`.
233
+ Better Auth and app-scoped keys remain legacy read candidates; a successful
234
+ legacy decrypt is compare-and-swap migrated to the preferred shared key
235
+ without changing the row timestamp.
236
+ - Set the same stable `WORKSPACE_SECRETS_ENCRYPTION_KEY` in every app that
237
+ reads a shared vault. It is intentionally separate from app-local OAuth
238
+ encryption. If the vault still relies on `A2A_SECRET`, rotating A2A material
239
+ also rotates its encryption key. Add the stable vault key everywhere and
240
+ read/migrate existing rows before rotating A2A.
241
+ - To rotate the dedicated workspace key itself, deploy the new value together
242
+ with `WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS=<old value>`, let reads
243
+ migrate ciphertext, then remove the previous key after the migration window.
239
244
  - If no configured key material exists, development uses a machine-local
240
245
  fallback and logs a one-time warning. Production fails closed.
241
246
 
@@ -536,14 +536,16 @@ Database connection variables (`DATABASE_URL`, `DATABASE_AUTH_TOKEN`, per-app `<
536
536
 
537
537
  These must be set before promoting an app to a real prod deploy. Missing values either fail-closed (the framework refuses to start / refuses to handle requests) or fall back to weaker behavior with a loud warning.
538
538
 
539
- | Variable | Description |
540
- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
541
- | `BETTER_AUTH_SECRET` | 32+ char random string. Signs session cookies and is the fallback for `OAUTH_STATE_SECRET` and app-local encrypted values. It is also a legacy read candidate for existing shared vault ciphertext. Hard-required: the framework throws on startup if missing in production. |
542
- | `BETTER_AUTH_URL` | Public origin of this app (e.g. `https://mail.example.com`). Used for cookie domain and OAuth redirect construction. |
543
- | `ANTHROPIC_API_KEY` | API key for the embedded production agent. **In multi-tenant deploys**, the framework refuses to fall back to this when the user has no per-user key — bring-your-own-key is required. Single-tenant self-hosted installs use it as a global key. |
544
- | `OAUTH_STATE_SECRET` | Dedicated HMAC key for OAuth state envelopes (Google, Atlassian, Zoom). Falls back to `BETTER_AUTH_SECRET` when unset, but a dedicated value is recommended so rotating one doesn't invalidate the other. Generate via `openssl rand -hex 32`. |
545
- | `A2A_SECRET` | Shared HMAC for inter-app A2A JSON-RPC. Without it, every A2A endpoint and the `/_agent-native/integrations/process-task` self-fire endpoint return 503 in production. When `SECRETS_ENCRYPTION_KEY` is unset, shared vault ciphertext uses a purpose-derived key from this value, so rotating it also rotates the vault key. |
546
- | `SECRETS_ENCRYPTION_KEY` | Preferred stable AES-256-GCM key for the shared encrypted-at-rest secrets vault. Set the same value in every app that reads the vault. Shared vault reads fall back to purpose-derived `A2A_SECRET`, then legacy Better Auth and app-scoped keys. Before rotating A2A, add this key everywhere and read/migrate every existing row while the old A2A material remains available. |
539
+ | Variable | Description |
540
+ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
541
+ | `BETTER_AUTH_SECRET` | 32+ char random string. Signs session cookies and is the fallback for `OAUTH_STATE_SECRET` and app-local encrypted values. It is also a legacy read candidate for existing shared vault ciphertext. Hard-required: the framework throws on startup if missing in production. |
542
+ | `BETTER_AUTH_URL` | Public origin of this app (e.g. `https://mail.example.com`). Used for cookie domain and OAuth redirect construction. |
543
+ | `ANTHROPIC_API_KEY` | API key for the embedded production agent. **In multi-tenant deploys**, the framework refuses to fall back to this when the user has no per-user key — bring-your-own-key is required. Single-tenant self-hosted installs use it as a global key. |
544
+ | `OAUTH_STATE_SECRET` | Dedicated HMAC key for OAuth state envelopes (Google, Atlassian, Zoom). Falls back to `BETTER_AUTH_SECRET` when unset, but a dedicated value is recommended so rotating one doesn't invalidate the other. Generate via `openssl rand -hex 32`. |
545
+ | `A2A_SECRET` | Shared HMAC for inter-app A2A JSON-RPC. Without it, every A2A endpoint and the `/_agent-native/integrations/process-task` self-fire endpoint return 503 in production. When no dedicated workspace vault key is set, shared vault ciphertext uses a purpose-derived key from this value, so rotating it also rotates the vault key. |
546
+ | `WORKSPACE_SECRETS_ENCRYPTION_KEY` | Preferred stable AES-256-GCM key for the shared encrypted-at-rest secrets vault. Set the same value in every app that reads the vault. It is separate from app-local OAuth/credential encryption, so A2A trust can rotate without stranding vault rows. Existing A2A-, Better Auth-, and app-key ciphertext is migrated on successful reads. |
547
+ | `WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS` | Optional previous workspace vault key used only during a rotation window. Deploy it alongside the new `WORKSPACE_SECRETS_ENCRYPTION_KEY`, let successful reads re-encrypt rows, then remove it after migration. |
548
+ | `SECRETS_ENCRYPTION_KEY` | Legacy combined key for app-local encryption and the shared vault. It remains supported, but new multi-app deployments should use `WORKSPACE_SECRETS_ENCRYPTION_KEY` for the shared vault so changing workspace encryption does not change app-local OAuth ciphertext. |
547
549
 
548
550
  ### Auth & Identity {#env-auth}
549
551
 
@@ -563,13 +565,13 @@ Inside a workspace, the root `.env` is loaded into every app automatically, so s
563
565
 
564
566
  ### Generating Strong Secrets {#env-generate-secrets}
565
567
 
566
- For any secret marked "32+ char random" (`BETTER_AUTH_SECRET`, `OAUTH_STATE_SECRET`, `A2A_SECRET`, `SECRETS_ENCRYPTION_KEY`, `AGENT_NATIVE_BUILDER_RELAY_SECRET`), generate fresh values with:
568
+ For any secret marked "32+ char random" (`BETTER_AUTH_SECRET`, `OAUTH_STATE_SECRET`, `A2A_SECRET`, `WORKSPACE_SECRETS_ENCRYPTION_KEY`, `SECRETS_ENCRYPTION_KEY`, `AGENT_NATIVE_BUILDER_RELAY_SECRET`), generate fresh values with:
567
569
 
568
570
  ```bash
569
571
  openssl rand -hex 32
570
572
  ```
571
573
 
572
- Rotate them by replacing the env var on every instance and redeploying — sessions / OAuth state envelopes signed under the old key become invalid, so users may need to sign in again.
574
+ Rotate signing/session keys by replacing the env var on every instance and redeploying — sessions / OAuth state envelopes signed under the old key become invalid, so users may need to sign in again. Rotate `WORKSPACE_SECRETS_ENCRYPTION_KEY` with the `..._PREVIOUS` window described above so encrypted vault rows remain readable while they migrate.
573
575
 
574
576
  ## Production Agent Tools {#production-agent-tools}
575
577
 
@@ -320,7 +320,7 @@ pnpm action db-check-scoping --require-org # Also require org_id
320
320
  | OAuth tokens (Google, GitHub) | `oauth_tokens` store via `saveOAuthTokens()` |
321
321
  | Session tokens | Automatic (Better Auth handles this) |
322
322
 
323
- Per-user/per-org credentials and the vault are encrypted at rest with AES-256-GCM. For a per-user/per-org credential, the key precedence is `<APP_NAME>_SECRETS_ENCRYPTION_KEY` (app-scoped — useful in a multi-app workspace where apps shouldn't share one key) → `SECRETS_ENCRYPTION_KEY` → `BETTER_AUTH_SECRET`; production refuses to start without one of these set. The shared workspace vault (`app_secrets`, meant to decrypt the same in sibling apps) checks `SECRETS_ENCRYPTION_KEY` / `BETTER_AUTH_SECRET` first, then on a hosted workspace deploy falls back further to material derived from `A2A_SECRET`, and only then to the app-scoped key as a last resort. To encrypt any pre-existing plaintext credential rows in place, run `pnpm action db-migrate-encrypt-credentials` (idempotent, non-destructive).
323
+ Per-user/per-org credentials and the vault are encrypted at rest with AES-256-GCM. For a per-user/per-org credential, the key precedence is `<APP_NAME>_SECRETS_ENCRYPTION_KEY` (app-scoped — useful in a multi-app workspace where apps shouldn't share one key) → `SECRETS_ENCRYPTION_KEY` → `BETTER_AUTH_SECRET`; production refuses to start without one of these set. The shared workspace vault (`app_secrets`, meant to decrypt the same in sibling apps) prefers `WORKSPACE_SECRETS_ENCRYPTION_KEY`, then the legacy combined `SECRETS_ENCRYPTION_KEY`, then material derived from `A2A_SECRET`, with `BETTER_AUTH_SECRET` and the app-scoped key retained as compatibility candidates. Successful fallback reads compare-and-swap re-encrypt the shared column under the preferred key. During a workspace-key rotation, set `WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS` to the old value until reads have migrated existing rows. To encrypt any pre-existing plaintext credential rows in place, run `pnpm action db-migrate-encrypt-credentials` (idempotent, non-destructive).
324
324
 
325
325
  Never store secrets in `settings`, `application_state`, source code, or action responses. Use the credential / vault APIs above — they handle both encryption and per-user scoping.
326
326
 
@@ -365,7 +365,7 @@ Google Docs Pub/Sub push notifications don't go through that generic adapter gat
365
365
  - [ ] `BETTER_AUTH_SECRET` set to a random 32+ char string (`openssl rand -hex 32`), unless this is a hosted workspace deploy deriving it from `A2A_SECRET`
366
366
  - [ ] `OAUTH_STATE_SECRET` set to a separate random 32+ char string (don't reuse `BETTER_AUTH_SECRET`) — see [OAuth State Signing](#oauth-state)
367
367
  - [ ] `A2A_SECRET` set on every app that calls or receives A2A traffic — see [A2A Identity Verification](#a2a-identity)
368
- - [ ] `SECRETS_ENCRYPTION_KEY` set (or rely on the `BETTER_AUTH_SECRET` fallback) — see [Secrets Management](#secrets)
368
+ - [ ] `WORKSPACE_SECRETS_ENCRYPTION_KEY` set consistently for multi-app shared vaults; app-local encryption still has `SECRETS_ENCRYPTION_KEY` / `BETTER_AUTH_SECRET` fallbacks — see [Secrets Management](#secrets)
369
369
  - [ ] `AUTH_SKIP_EMAIL_VERIFICATION` is **not** set in production (or set only on QA preview deploys)
370
370
 
371
371
  ### Webhook secrets (set the ones for integrations you use)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.131.6",
3
+ "version": "0.131.7",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -395,8 +395,8 @@
395
395
  "y-protocols": "^1.0.7",
396
396
  "yjs": "^13.6.31",
397
397
  "zod": "^4.3.6",
398
- "@agent-native/recap-cli": "0.5.1",
399
- "@agent-native/toolkit": "^0.12.0"
398
+ "@agent-native/toolkit": "^0.12.0",
399
+ "@agent-native/recap-cli": "0.5.1"
400
400
  },
401
401
  "devDependencies": {
402
402
  "@ai-sdk/anthropic": "^3.0.71",
package/src/a2a/invoke.ts CHANGED
@@ -263,9 +263,11 @@ export function buildAgentInvocationPrompt(
263
263
  ): string {
264
264
  return (
265
265
  `${prompt}\n\n` +
266
- `[Note: this request comes from another app via A2A. The caller cannot see your local UI, resource list, or navigation - only the literal text you put in your reply. ` +
266
+ `<a2a-caller-hint>\n` +
267
+ `This request comes from another app via A2A. The caller cannot see your local UI, resource list, or navigation - only the literal text you put in your reply. ` +
267
268
  `If you create or reference a deck/document/design/dashboard/resource, include its FULLY-QUALIFIED URL (e.g. ${agentUrl.replace(/\/$/, "")}/<path>/<id>) in your reply, not a relative path. ` +
268
- `Use only artifact IDs and URL paths returned by successful actions - never invent slugs, IDs, or hosts.]`
269
+ `Use only artifact IDs and URL paths returned by successful actions - never invent slugs, IDs, or hosts.\n` +
270
+ `</a2a-caller-hint>`
269
271
  );
270
272
  }
271
273
 
@@ -21,8 +21,9 @@ export function incrementalActionGuidance(tool: string): string | undefined {
21
21
  case "create-prototype-plan":
22
22
  return "create the plan with its core sections or first screen, then expand it with `update-visual-plan`/`patch-visual-plan-source` follow-up edits";
23
23
  case "update-visual-plan":
24
+ return "apply one or a few small `update-visual-plan` `contentPatches` such as `append-block`, `update-rich-text`, or a text patch; do not resend the full plan or switch to a source-control workflow";
24
25
  case "patch-visual-plan-source":
25
- return "apply smaller, targeted `patch-visual-plan-source` edits rather than rewriting the whole plan in one call";
26
+ return "apply small targeted `patch-visual-plan-source` MDX AST edits; do not use `replace-file`, and do not use this source action for a live plan edit";
26
27
  case "update-dashboard":
27
28
  return "save a small dashboard first, then add panels one at a time with `update-dashboard` incremental `ops` edits instead of authoring the whole config in one call";
28
29
  default:
@@ -600,6 +600,41 @@ async function resolveUsableProviderSecret(
600
600
  return authFailure ? null : value;
601
601
  }
602
602
 
603
+ /**
604
+ * Return true only when the supplied key can be positively identified as a
605
+ * usable credential for a different registered provider.
606
+ *
607
+ * `ResolveEngineConfig.apiKey` is public and may be an opaque caller-provided
608
+ * key, so automatic selection must not silently replace it merely because a
609
+ * stored credential also exists. Delegated/internal callers, however,
610
+ * historically pass the current "active" provider key before the registry
611
+ * selects an app-default engine. Exact in-memory comparison lets us correct
612
+ * that proven mismatch without guessing from provider-specific key prefixes
613
+ * or exposing either value.
614
+ */
615
+ async function apiKeyBelongsToDifferentProvider(
616
+ apiKey: string,
617
+ selectedEntry: AgentEngineEntry,
618
+ ): Promise<boolean> {
619
+ const selectedEnvVars = new Set(selectedEntry.requiredEnvVars);
620
+ const checkedEnvVars = new Set<string>();
621
+ for (const entry of _registry.values()) {
622
+ if (entry === selectedEntry || entry.name === "builder") continue;
623
+ for (const key of entry.requiredEnvVars) {
624
+ if (selectedEnvVars.has(key) || checkedEnvVars.has(key)) continue;
625
+ checkedEnvVars.add(key);
626
+ try {
627
+ if ((await resolveUsableProviderSecret(key)) === apiKey) return true;
628
+ } catch {
629
+ // An unrelated provider store failure is not proof that the explicit
630
+ // key belongs elsewhere. Preserve the caller's key and let the
631
+ // selected provider's own preflight report any relevant failure.
632
+ }
633
+ }
634
+ }
635
+ return false;
636
+ }
637
+
603
638
  async function engineCreateConfigForEntry(
604
639
  entry: AgentEngineEntry,
605
640
  apiKey: string | undefined,
@@ -612,27 +647,44 @@ async function engineCreateConfigForEntry(
612
647
  // Callers historically passed one untagged "active" key before the registry
613
648
  // chose an engine, which could hand an Anthropic key to an app-default
614
649
  // OpenAI engine (or vice versa). Explicit engineOption branches retain their
615
- // paired key; automatic branches opt into matching the chosen provider here.
650
+ // paired key. Automatic branches replace only a key proven to belong to a
651
+ // different configured provider; opaque caller-supplied keys keep the public
652
+ // `ResolveEngineConfig.apiKey` contract.
616
653
  if (
617
654
  preferResolvedCredential &&
618
655
  entry.name !== "builder" &&
619
656
  entry.requiredEnvVars.length > 0
620
657
  ) {
621
- let foundMatchingCredential = false;
658
+ let resolvedMatchingCredential: string | undefined;
659
+ let matchingCredentialUsesDeployFallback = false;
622
660
  for (const key of entry.requiredEnvVars) {
623
661
  const resolved = (await resolveUsableProviderSecret(key)) ?? undefined;
624
662
  if (!resolved) continue;
625
- foundMatchingCredential = true;
626
- const deployFallback =
663
+ resolvedMatchingCredential = resolved;
664
+ matchingCredentialUsesDeployFallback =
627
665
  canUseDeployCredentialFallbackForRequest(key) &&
628
666
  readDeployCredentialEnv(key) === resolved;
667
+ break;
668
+ }
669
+
670
+ const suppliedKeyMatchesSelected =
671
+ matchingApiKey !== undefined &&
672
+ matchingApiKey === resolvedMatchingCredential;
673
+ const suppliedKeyBelongsElsewhere =
674
+ matchingApiKey !== undefined &&
675
+ !suppliedKeyMatchesSelected &&
676
+ (await apiKeyBelongsToDifferentProvider(matchingApiKey, entry));
677
+
678
+ if (matchingApiKey === undefined || suppliedKeyBelongsElsewhere) {
629
679
  // Keep deploy-only credentials implicit so provider SDKs retain their
630
680
  // established env-fallback behavior. Scoped credentials must be passed
631
- // explicitly, and replace an unrelated preselected provider key.
632
- if (matchingApiKey || !deployFallback) matchingApiKey = resolved;
633
- break;
681
+ // explicitly. A proven different-provider key is replaced or cleared;
682
+ // an opaque caller-supplied key is preserved by the branch above.
683
+ matchingApiKey =
684
+ resolvedMatchingCredential && !matchingCredentialUsesDeployFallback
685
+ ? resolvedMatchingCredential
686
+ : undefined;
634
687
  }
635
- if (!foundMatchingCredential) matchingApiKey = undefined;
636
688
  }
637
689
  if (entry.name === "ai-sdk:openai") {
638
690
  if (typeof safeExtra.baseURL === "string" && safeExtra.baseUrl == null) {
@@ -924,7 +976,7 @@ export async function resolveEngine(
924
976
  );
925
977
  }
926
978
  return anthropicEntry.create(
927
- await engineCreateConfigForEntry(anthropicEntry, apiKey),
979
+ await engineCreateConfigForEntry(anthropicEntry, apiKey, undefined, true),
928
980
  );
929
981
  }
930
982
 
@@ -65,6 +65,8 @@ const ALLOWLIST_EXACT = new Set([
65
65
  "PORT",
66
66
  "HOST",
67
67
  "SECRETS_ENCRYPTION_KEY",
68
+ "WORKSPACE_SECRETS_ENCRYPTION_KEY",
69
+ "WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS",
68
70
  ]);
69
71
 
70
72
  const ALLOWLIST_PREFIX = [
@@ -464,10 +464,12 @@ export async function run(
464
464
  const messageWithHint = taskId
465
465
  ? ""
466
466
  : `${message}${integrationSourceContextHint(sourceContext)}\n\n` +
467
- `[Note: this request comes from another app via A2A. The caller cannot see your local UI, deck list, or navigation — only the literal text you put in your reply. ` +
467
+ `<a2a-caller-hint>\n` +
468
+ `This request comes from another app via A2A. The caller cannot see your local UI, deck list, or navigation — only the literal text you put in your reply. ` +
468
469
  `If you create or reference a deck/document/design/dashboard, include its FULLY-QUALIFIED URL (e.g. ${agent.url}/deck/<id>) in your reply, not a relative path. ` +
469
470
  `Use only artifact IDs and URL paths returned by successful actions — never invent slugs, IDs, or hosts. ` +
470
- `Return a concise caller-ready synthesis rather than raw tool output or full transcripts; preserve source counts, IDs, short supporting quotes, and URLs needed to substantiate the answer.]`;
471
+ `Return a concise caller-ready synthesis rather than raw tool output or full transcripts; preserve source counts, IDs, short supporting quotes, and URLs needed to substantiate the answer.\n` +
472
+ `</a2a-caller-hint>`;
471
473
 
472
474
  const invocationId = randomUUID();
473
475
  const invocationStartedAt = Date.now();
@@ -13,10 +13,13 @@
13
13
  * The generic encryption key is derived from
14
14
  * `<APP_NAME>_SECRETS_ENCRYPTION_KEY` when set, then
15
15
  * `SECRETS_ENCRYPTION_KEY`, then `BETTER_AUTH_SECRET`. Workspace-shared vault
16
- * rows use a different precedence: `SECRETS_ENCRYPTION_KEY`, then material
17
- * derived from the workspace-wide `A2A_SECRET`, then the app-local auth/key
18
- * fallbacks retained for backward-compatible reads. This keeps sibling apps
19
- * on one key even when each app correctly has a different auth secret.
16
+ * rows use a different precedence: `WORKSPACE_SECRETS_ENCRYPTION_KEY`, then
17
+ * the legacy shared `SECRETS_ENCRYPTION_KEY`, then material derived from the
18
+ * workspace-wide `A2A_SECRET`, then the app-local auth/key fallbacks retained
19
+ * for backward-compatible reads. A previous workspace key can be supplied
20
+ * during rotation with `WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS`. This keeps
21
+ * sibling apps on one stable key even when each app correctly has a different
22
+ * auth secret, and prevents A2A trust rotation from stranding vault data.
20
23
  *
21
24
  * In production we refuse to start without configured key material — a
22
25
  * CWD-derived fallback would be effectively static (e.g. `/var/task` on
@@ -95,6 +98,18 @@ function genericEncryptionKeyMaterial(): string | undefined {
95
98
  );
96
99
  }
97
100
 
101
+ function workspaceSharedEncryptionKeyMaterial(): string | undefined {
102
+ if (typeof process === "undefined") return undefined;
103
+ return process.env.WORKSPACE_SECRETS_ENCRYPTION_KEY?.trim() || undefined;
104
+ }
105
+
106
+ function previousWorkspaceSharedEncryptionKeyMaterial(): string | undefined {
107
+ if (typeof process === "undefined") return undefined;
108
+ return (
109
+ process.env.WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS?.trim() || undefined
110
+ );
111
+ }
112
+
98
113
  /**
99
114
  * Vault sharing follows configured A2A trust even on independently deployed
100
115
  * sibling apps that are not launched by the workspace wrapper. The wrapper's
@@ -116,14 +131,17 @@ function a2aSharedEncryptionKeyMaterial(): string | undefined {
116
131
  * Stable materials that may be used for workspace-shared vault rows, ordered
117
132
  * from current preference to legacy compatibility fallbacks.
118
133
  *
119
- * `BETTER_AUTH_SECRET` deliberately comes after the A2A-derived material:
120
- * sibling apps normally have different auth secrets but share A2A trust.
134
+ * `BETTER_AUTH_SECRET` deliberately comes after the workspace and A2A-derived
135
+ * materials: sibling apps normally have different auth secrets but share
136
+ * workspace trust.
121
137
  */
122
138
  function sharedEncryptionKeyMaterials(): string[] {
123
139
  if (typeof process === "undefined") return [];
124
140
  const candidates = [
141
+ workspaceSharedEncryptionKeyMaterial(),
125
142
  process.env.SECRETS_ENCRYPTION_KEY,
126
143
  a2aSharedEncryptionKeyMaterial(),
144
+ previousWorkspaceSharedEncryptionKeyMaterial(),
127
145
  process.env.BETTER_AUTH_SECRET,
128
146
  appScopedEncryptionKey(),
129
147
  ].filter((value): value is string => Boolean(value));
@@ -131,7 +149,14 @@ function sharedEncryptionKeyMaterials(): string[] {
131
149
  }
132
150
 
133
151
  function preferredSharedEncryptionKeyMaterial(): string | undefined {
134
- return sharedEncryptionKeyMaterials()[0];
152
+ if (typeof process === "undefined") return undefined;
153
+ return (
154
+ workspaceSharedEncryptionKeyMaterial() ||
155
+ process.env.SECRETS_ENCRYPTION_KEY ||
156
+ a2aSharedEncryptionKeyMaterial() ||
157
+ process.env.BETTER_AUTH_SECRET ||
158
+ appScopedEncryptionKey()
159
+ );
135
160
  }
136
161
 
137
162
  function hashSecretEncryptionKey(material: string): Buffer {
@@ -187,20 +212,23 @@ export function getSecretEncryptionKey(): Buffer {
187
212
  /**
188
213
  * Derive the preferred workspace-shared key used by `app_secrets` rows.
189
214
  * Unlike generic column-level encryption, workspace vault data must decrypt
190
- * in sibling apps. An explicit `SECRETS_ENCRYPTION_KEY` wins; hosted
191
- * workspaces otherwise derive stable material from their shared `A2A_SECRET`
192
- * before considering app-local `BETTER_AUTH_SECRET` or app-scoped key
193
- * fallbacks. Reads try every configured legacy candidate and report when the
194
- * ciphertext should be refreshed under the preferred key.
215
+ * in sibling apps. An explicit `WORKSPACE_SECRETS_ENCRYPTION_KEY` wins without
216
+ * changing the key for app-local OAuth and credential ciphertext. The legacy
217
+ * shared `SECRETS_ENCRYPTION_KEY` remains supported; hosted workspaces
218
+ * otherwise derive material from their shared `A2A_SECRET` before considering
219
+ * app-local `BETTER_AUTH_SECRET` or app-scoped key fallbacks. Reads try every
220
+ * configured legacy/previous candidate and report when the ciphertext should
221
+ * be refreshed under the preferred key.
195
222
  */
196
223
  export function getSharedSecretEncryptionKey(): Buffer {
197
224
  return deriveSecretEncryptionKey(
198
225
  preferredSharedEncryptionKeyMaterial(),
199
226
  "[agent-native/secrets] Refusing to start in production without encryption key material for workspace secrets. " +
200
- "Set SECRETS_ENCRYPTION_KEY, ensure a hosted workspace has A2A_SECRET, or set BETTER_AUTH_SECRET / an app-scoped " +
227
+ "Set WORKSPACE_SECRETS_ENCRYPTION_KEY (preferred), SECRETS_ENCRYPTION_KEY, ensure a hosted workspace has A2A_SECRET, " +
228
+ "or set BETTER_AUTH_SECRET / an app-scoped " +
201
229
  "*_SECRETS_ENCRYPTION_KEY compatibility fallback in the deploy environment.",
202
- "[agent-native/secrets] SECRETS_ENCRYPTION_KEY not set — using app-scoped or machine-local fallback for workspace secrets. " +
203
- "Set SECRETS_ENCRYPTION_KEY or rely on A2A_SECRET-derived material on hosted workspace deploys so sibling apps share vault rows.",
230
+ "[agent-native/secrets] Workspace encryption key not set — using app-scoped or machine-local fallback for workspace secrets. " +
231
+ "Set WORKSPACE_SECRETS_ENCRYPTION_KEY or rely on A2A_SECRET-derived material on hosted workspace deploys so sibling apps share vault rows.",
204
232
  );
205
233
  }
206
234
 
@@ -208,6 +236,7 @@ export function getSharedSecretEncryptionKey(): Buffer {
208
236
  export function hasSharedSecretEncryptionKeyMaterial(): boolean {
209
237
  if (typeof process === "undefined") return false;
210
238
  return Boolean(
239
+ workspaceSharedEncryptionKeyMaterial() ||
211
240
  process.env.SECRETS_ENCRYPTION_KEY ||
212
241
  a2aSharedEncryptionKeyMaterial() ||
213
242
  process.env.BETTER_AUTH_SECRET,
@@ -2,10 +2,12 @@
2
2
  * Storage layer for the framework secrets registry.
3
3
  *
4
4
  * Values are encrypted at rest with AES-256-GCM. The workspace-shared
5
- * encryption key is derived from `SECRETS_ENCRYPTION_KEY` (preferred) or the
6
- * workspace-wide `A2A_SECRET`. `BETTER_AUTH_SECRET` and app-scoped keys remain
7
- * read fallbacks for rows written before sibling apps converged on the shared
8
- * key. Successful fallback reads race-safely refresh the shared ciphertext.
5
+ * encryption key prefers `WORKSPACE_SECRETS_ENCRYPTION_KEY`, then the legacy
6
+ * combined `SECRETS_ENCRYPTION_KEY`, then the workspace-wide `A2A_SECRET`.
7
+ * A configured previous workspace key, `BETTER_AUTH_SECRET`, and app-scoped
8
+ * keys remain read fallbacks for rows written before sibling apps converged on
9
+ * the shared key. Successful fallback reads race-safely refresh the shared
10
+ * ciphertext.
9
11
  *
10
12
  * Secret values are NEVER logged and NEVER returned from any route handler.
11
13
  */
@@ -360,6 +360,8 @@ export const DELEGATED_AGENT_EXECUTION_CONTRACT = `
360
360
  <delegated-agent-contract>
361
361
  This request was delegated by another app. You are the specialist owner of the work.
362
362
  - Interpret the caller's natural-language objective using your own instructions, skills, data dictionary, credentials, and tools. Choose providers, schemas, queries, and joins here; never ask the caller to invent SQL or source-specific implementation details for you.
363
+ - Finish the objective autonomously in this delegated turn whenever a safe, reasonable default exists. Do not bounce the work back with UI-navigation requests, implementation questions, or intermediate status. Ask for input only when authorization, missing credentials, or a consequential user choice truly blocks progress.
364
+ - When evidence is incomplete, return the best grounded partial answer with explicit coverage gaps instead of refusing or asking the caller to choose your source, filter, dashboard, or workflow.
363
365
  - Minimize round trips. Filter, join, aggregate, paginate, stage, and reduce large datasets inside your own tools rather than returning raw records or transcripts.
364
366
  - Return a concise caller-ready result with the answer, source and coverage details, relevant counts or IDs, caveats/partial gaps, and exact artifact URLs. Do not return tool transcripts or large raw payloads.
365
367
  </delegated-agent-contract>`;
@@ -227,15 +227,20 @@ Core routes plugin mounts these under `/_agent-native/secrets/` automatically:
227
227
  prefer `<APP_NAME>_SECRETS_ENCRYPTION_KEY` (for example,
228
228
  `ANALYTICS_SECRETS_ENCRYPTION_KEY`), then `SECRETS_ENCRYPTION_KEY`, then
229
229
  `BETTER_AUTH_SECRET`.
230
- - Workspace-shared `app_secrets` prefer `SECRETS_ENCRYPTION_KEY`, then a
231
- purpose-derived key from `A2A_SECRET`. Better Auth and app-scoped keys remain
232
- legacy read candidates; a successful legacy decrypt is compare-and-swap
233
- migrated to the preferred shared key without changing the row timestamp.
234
- - Set the same stable `SECRETS_ENCRYPTION_KEY` in every app that reads a shared
235
- vault. If the vault relies on `A2A_SECRET`, rotating A2A material also rotates
236
- its encryption key. Before rotating A2A, add the stable vault key everywhere
237
- and read/migrate every existing row while the old A2A material is still
238
- available.
230
+ - Workspace-shared `app_secrets` prefer
231
+ `WORKSPACE_SECRETS_ENCRYPTION_KEY`, then the legacy shared
232
+ `SECRETS_ENCRYPTION_KEY`, then a purpose-derived key from `A2A_SECRET`.
233
+ Better Auth and app-scoped keys remain legacy read candidates; a successful
234
+ legacy decrypt is compare-and-swap migrated to the preferred shared key
235
+ without changing the row timestamp.
236
+ - Set the same stable `WORKSPACE_SECRETS_ENCRYPTION_KEY` in every app that
237
+ reads a shared vault. It is intentionally separate from app-local OAuth
238
+ encryption. If the vault still relies on `A2A_SECRET`, rotating A2A material
239
+ also rotates its encryption key. Add the stable vault key everywhere and
240
+ read/migrate existing rows before rotating A2A.
241
+ - To rotate the dedicated workspace key itself, deploy the new value together
242
+ with `WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS=<old value>`, let reads
243
+ migrate ciphertext, then remove the previous key after the migration window.
239
244
  - If no configured key material exists, development uses a machine-local
240
245
  fallback and logs a one-time warning. Production fails closed.
241
246
 
@@ -227,15 +227,20 @@ Core routes plugin mounts these under `/_agent-native/secrets/` automatically:
227
227
  prefer `<APP_NAME>_SECRETS_ENCRYPTION_KEY` (for example,
228
228
  `ANALYTICS_SECRETS_ENCRYPTION_KEY`), then `SECRETS_ENCRYPTION_KEY`, then
229
229
  `BETTER_AUTH_SECRET`.
230
- - Workspace-shared `app_secrets` prefer `SECRETS_ENCRYPTION_KEY`, then a
231
- purpose-derived key from `A2A_SECRET`. Better Auth and app-scoped keys remain
232
- legacy read candidates; a successful legacy decrypt is compare-and-swap
233
- migrated to the preferred shared key without changing the row timestamp.
234
- - Set the same stable `SECRETS_ENCRYPTION_KEY` in every app that reads a shared
235
- vault. If the vault relies on `A2A_SECRET`, rotating A2A material also rotates
236
- its encryption key. Before rotating A2A, add the stable vault key everywhere
237
- and read/migrate every existing row while the old A2A material is still
238
- available.
230
+ - Workspace-shared `app_secrets` prefer
231
+ `WORKSPACE_SECRETS_ENCRYPTION_KEY`, then the legacy shared
232
+ `SECRETS_ENCRYPTION_KEY`, then a purpose-derived key from `A2A_SECRET`.
233
+ Better Auth and app-scoped keys remain legacy read candidates; a successful
234
+ legacy decrypt is compare-and-swap migrated to the preferred shared key
235
+ without changing the row timestamp.
236
+ - Set the same stable `WORKSPACE_SECRETS_ENCRYPTION_KEY` in every app that
237
+ reads a shared vault. It is intentionally separate from app-local OAuth
238
+ encryption. If the vault still relies on `A2A_SECRET`, rotating A2A material
239
+ also rotates its encryption key. Add the stable vault key everywhere and
240
+ read/migrate existing rows before rotating A2A.
241
+ - To rotate the dedicated workspace key itself, deploy the new value together
242
+ with `WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS=<old value>`, let reads
243
+ migrate ciphertext, then remove the previous key after the migration window.
239
244
  - If no configured key material exists, development uses a machine-local
240
245
  fallback and logs a one-time warning. Production fails closed.
241
246
 
@@ -227,15 +227,20 @@ Core routes plugin mounts these under `/_agent-native/secrets/` automatically:
227
227
  prefer `<APP_NAME>_SECRETS_ENCRYPTION_KEY` (for example,
228
228
  `ANALYTICS_SECRETS_ENCRYPTION_KEY`), then `SECRETS_ENCRYPTION_KEY`, then
229
229
  `BETTER_AUTH_SECRET`.
230
- - Workspace-shared `app_secrets` prefer `SECRETS_ENCRYPTION_KEY`, then a
231
- purpose-derived key from `A2A_SECRET`. Better Auth and app-scoped keys remain
232
- legacy read candidates; a successful legacy decrypt is compare-and-swap
233
- migrated to the preferred shared key without changing the row timestamp.
234
- - Set the same stable `SECRETS_ENCRYPTION_KEY` in every app that reads a shared
235
- vault. If the vault relies on `A2A_SECRET`, rotating A2A material also rotates
236
- its encryption key. Before rotating A2A, add the stable vault key everywhere
237
- and read/migrate every existing row while the old A2A material is still
238
- available.
230
+ - Workspace-shared `app_secrets` prefer
231
+ `WORKSPACE_SECRETS_ENCRYPTION_KEY`, then the legacy shared
232
+ `SECRETS_ENCRYPTION_KEY`, then a purpose-derived key from `A2A_SECRET`.
233
+ Better Auth and app-scoped keys remain legacy read candidates; a successful
234
+ legacy decrypt is compare-and-swap migrated to the preferred shared key
235
+ without changing the row timestamp.
236
+ - Set the same stable `WORKSPACE_SECRETS_ENCRYPTION_KEY` in every app that
237
+ reads a shared vault. It is intentionally separate from app-local OAuth
238
+ encryption. If the vault still relies on `A2A_SECRET`, rotating A2A material
239
+ also rotates its encryption key. Add the stable vault key everywhere and
240
+ read/migrate existing rows before rotating A2A.
241
+ - To rotate the dedicated workspace key itself, deploy the new value together
242
+ with `WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS=<old value>`, let reads
243
+ migrate ciphertext, then remove the previous key after the migration window.
239
244
  - If no configured key material exists, development uses a machine-local
240
245
  fallback and logs a one-time warning. Production fails closed.
241
246