@agent-native/core 0.131.6 → 0.131.9

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 (113) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +31 -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/application-state/store.ts +18 -31
  10. package/corpus/core/src/cli/create.ts +115 -16
  11. package/corpus/core/src/client/AssistantChat.tsx +10 -2
  12. package/corpus/core/src/client/frame-protocol.ts +5 -1
  13. package/corpus/core/src/guards/no-env-credentials.ts +2 -0
  14. package/corpus/core/src/scripts/call-agent.ts +4 -2
  15. package/corpus/core/src/secrets/crypto.ts +44 -15
  16. package/corpus/core/src/secrets/storage.ts +6 -4
  17. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +2 -0
  18. package/corpus/core/src/templates/default/.agents/skills/secrets/SKILL.md +14 -9
  19. package/corpus/core/src/templates/headless/.agents/skills/secrets/SKILL.md +14 -9
  20. package/corpus/core/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +14 -9
  21. package/corpus/core/src/workspace-files/tool.ts +18 -11
  22. package/corpus/templates/analytics/AGENTS.md +1 -0
  23. package/corpus/templates/analytics/changelog/2026-07-30-delegated-analytics-questions-now-return-a-useful-current-me.md +6 -0
  24. package/corpus/templates/analytics/server/lib/real-data-actions.ts +10 -0
  25. package/corpus/templates/clips/actions/generate-workflow.ts +6 -4
  26. package/corpus/templates/clips/actions/reconcile-workflow-generation.ts +257 -0
  27. package/corpus/templates/clips/app/hooks/use-auto-title.ts +166 -4
  28. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -1
  29. package/corpus/templates/clips/changelog/2026-07-27-stopped-agent-runs-no-longer-leave-generated-workflow-cards-.md +6 -0
  30. package/corpus/templates/clips/changelog/2026-07-30-workflow-generation-now-stops-retrying-after-repeated-failur.md +6 -0
  31. package/corpus/templates/clips/shared/workflow.ts +5 -0
  32. package/corpus/templates/plan/actions/get-visual-plan.ts +10 -6
  33. package/corpus/templates/plan/actions/patch-visual-plan-source.ts +117 -1
  34. package/corpus/templates/plan/actions/update-visual-plan.ts +56 -14
  35. package/corpus/templates/plan/app/pages/PlansPage.tsx +1 -1
  36. package/corpus/templates/plan/changelog/2026-07-30-plan-agents-recover-with-targeted-writes.md +6 -0
  37. package/corpus/templates/plan/shared/plan-content.ts +35 -0
  38. package/corpus/templates/slides/actions/create-deck.ts +1 -1
  39. package/corpus/templates/slides/changelog/2026-07-30-new-decks-now-start-with-an-empty-slide-list-when-no-slides-.md +6 -0
  40. package/dist/a2a/invoke.d.ts.map +1 -1
  41. package/dist/a2a/invoke.js +4 -2
  42. package/dist/a2a/invoke.js.map +1 -1
  43. package/dist/agent/action-continuation-guidance.d.ts.map +1 -1
  44. package/dist/agent/action-continuation-guidance.js +2 -1
  45. package/dist/agent/action-continuation-guidance.js.map +1 -1
  46. package/dist/agent/engine/registry.d.ts.map +1 -1
  47. package/dist/agent/engine/registry.js +59 -12
  48. package/dist/agent/engine/registry.js.map +1 -1
  49. package/dist/application-state/store.d.ts.map +1 -1
  50. package/dist/application-state/store.js +20 -36
  51. package/dist/application-state/store.js.map +1 -1
  52. package/dist/cli/create.d.ts +8 -1
  53. package/dist/cli/create.d.ts.map +1 -1
  54. package/dist/cli/create.js +98 -16
  55. package/dist/cli/create.js.map +1 -1
  56. package/dist/client/AssistantChat.d.ts.map +1 -1
  57. package/dist/client/AssistantChat.js +10 -2
  58. package/dist/client/AssistantChat.js.map +1 -1
  59. package/dist/client/frame-protocol.d.ts +1 -0
  60. package/dist/client/frame-protocol.d.ts.map +1 -1
  61. package/dist/client/frame-protocol.js.map +1 -1
  62. package/dist/collab/routes.d.ts +1 -1
  63. package/dist/collab/struct-routes.d.ts +1 -1
  64. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  65. package/dist/guards/no-env-credentials.d.ts.map +1 -1
  66. package/dist/guards/no-env-credentials.js +2 -0
  67. package/dist/guards/no-env-credentials.js.map +1 -1
  68. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  69. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  70. package/dist/notifications/routes.d.ts +6 -6
  71. package/dist/observability/routes.d.ts +3 -3
  72. package/dist/scripts/call-agent.d.ts.map +1 -1
  73. package/dist/scripts/call-agent.js +4 -2
  74. package/dist/scripts/call-agent.js.map +1 -1
  75. package/dist/secrets/crypto.d.ts +14 -9
  76. package/dist/secrets/crypto.d.ts.map +1 -1
  77. package/dist/secrets/crypto.js +42 -16
  78. package/dist/secrets/crypto.js.map +1 -1
  79. package/dist/secrets/storage.d.ts +6 -4
  80. package/dist/secrets/storage.d.ts.map +1 -1
  81. package/dist/secrets/storage.js +6 -4
  82. package/dist/secrets/storage.js.map +1 -1
  83. package/dist/server/agent-chat/action-filters-a2a.d.ts +1 -1
  84. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  85. package/dist/server/agent-chat/action-filters-a2a.js +2 -0
  86. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  87. package/dist/server/realtime-token.d.ts +1 -1
  88. package/dist/server/transcribe-voice.d.ts +1 -1
  89. package/dist/templates/default/.agents/skills/secrets/SKILL.md +14 -9
  90. package/dist/templates/headless/.agents/skills/secrets/SKILL.md +14 -9
  91. package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +14 -9
  92. package/dist/workspace-files/tool.d.ts.map +1 -1
  93. package/dist/workspace-files/tool.js +18 -11
  94. package/dist/workspace-files/tool.js.map +1 -1
  95. package/docs/content/deployment.mdx +12 -10
  96. package/docs/content/security.mdx +2 -2
  97. package/package.json +1 -1
  98. package/src/a2a/invoke.ts +4 -2
  99. package/src/agent/action-continuation-guidance.ts +2 -1
  100. package/src/agent/engine/registry.ts +61 -9
  101. package/src/application-state/store.ts +18 -31
  102. package/src/cli/create.ts +115 -16
  103. package/src/client/AssistantChat.tsx +10 -2
  104. package/src/client/frame-protocol.ts +5 -1
  105. package/src/guards/no-env-credentials.ts +2 -0
  106. package/src/scripts/call-agent.ts +4 -2
  107. package/src/secrets/crypto.ts +44 -15
  108. package/src/secrets/storage.ts +6 -4
  109. package/src/server/agent-chat/action-filters-a2a.ts +2 -0
  110. package/src/templates/default/.agents/skills/secrets/SKILL.md +14 -9
  111. package/src/templates/headless/.agents/skills/secrets/SKILL.md +14 -9
  112. package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +14 -9
  113. package/src/workspace-files/tool.ts +18 -11
package/corpus/README.md CHANGED
@@ -30,4 +30,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
30
30
 
31
31
  - core files: 1644
32
32
  - toolkit files: 168
33
- - template files: 7219
33
+ - template files: 7226
@@ -1,5 +1,36 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.131.9
4
+
5
+ ### Patch Changes
6
+
7
+ - d80a9c9: Report workspace files as truncated only when more content exists beyond the requested read page, and normalize paging arguments to integer boundaries.
8
+ - 3c538e4: Preserve application-state database read failures and distinguish explicit stops or exhausted reconnect failures from recoverable chat handoffs.
9
+
10
+ ## 0.131.8
11
+
12
+ ### Patch Changes
13
+
14
+ - c7ec59d: `create .` now scaffolds into the current directory and takes the project name
15
+ from the folder's basename, matching `create-react-app` / `npm init`. Previously
16
+ `.` was rejected as an invalid name. The current directory must be empty apart
17
+ from benign VCS/editor files (`.git`, `.gitignore`, `LICENSE`, `README.md`, …)
18
+ so an existing project is never merged over.
19
+
20
+ The scaffold is built in a private staging directory and only the files that
21
+ don't already exist are copied in, so a mid-scaffold failure can never delete
22
+ the current directory (including `.git`) and pre-existing files like
23
+ `README.md` and `.gitignore` are preserved. When the current directory is
24
+ already a git repo, `create .` skips `git init`/commit so it never writes an
25
+ unexpected commit into the user's history.
26
+
27
+ ## 0.131.7
28
+
29
+ ### Patch Changes
30
+
31
+ - 4f3a651: Harden delegated agent transport and provider selection, and support stable workspace-vault key rotation without changing app-local OAuth encryption.
32
+ - 4f3a651: Keep interrupted Plan edits on small targeted writes instead of retrying oversized full-plan payloads.
33
+
3
34
  ## 0.131.6
4
35
 
5
36
  ### Patch Changes
@@ -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)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.131.6",
3
+ "version": "0.131.9",
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": {
@@ -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
 
@@ -2,7 +2,6 @@ import {
2
2
  getDbExec,
3
3
  getDialect,
4
4
  isLocalDatabase,
5
- isConnectionError,
6
5
  isPostgres,
7
6
  intType,
8
7
  type DbExec,
@@ -102,21 +101,14 @@ export async function appStateGet(
102
101
  sessionId: string,
103
102
  key: string,
104
103
  ): Promise<Record<string, unknown> | null> {
105
- try {
106
- await ensureTable();
107
- const client = getDbExec();
108
- const { rows } = await client.execute({
109
- sql: `SELECT value FROM application_state WHERE session_id = ? AND key = ?`,
110
- args: [sessionId, key],
111
- });
112
- if (rows.length === 0) return null;
113
- return JSON.parse(rows[0].value as string);
114
- } catch (err) {
115
- // Transient WS / connection drops (Neon serverless) — caller polls every
116
- // 2s and will see the value on the next tick. Swallow rather than 500.
117
- if (isConnectionError(err)) return null;
118
- throw err;
119
- }
104
+ await ensureTable();
105
+ const client = getDbExec();
106
+ const { rows } = await client.execute({
107
+ sql: `SELECT value FROM application_state WHERE session_id = ? AND key = ?`,
108
+ args: [sessionId, key],
109
+ });
110
+ if (rows.length === 0) return null;
111
+ return JSON.parse(rows[0].value as string);
120
112
  }
121
113
 
122
114
  /**
@@ -133,22 +125,17 @@ export async function appStateGetMany(
133
125
  for (const key of uniqueKeys) values[key] = null;
134
126
  if (uniqueKeys.length === 0) return values;
135
127
 
136
- try {
137
- await ensureTable();
138
- const client = getDbExec();
139
- const placeholders = uniqueKeys.map(() => "?").join(", ");
140
- const { rows } = await client.execute({
141
- sql: `SELECT key, value FROM application_state WHERE session_id = ? AND key IN (${placeholders})`,
142
- args: [sessionId, ...uniqueKeys],
143
- });
144
- for (const row of rows) {
145
- values[row.key as string] = JSON.parse(row.value as string);
146
- }
147
- return values;
148
- } catch (err) {
149
- if (isConnectionError(err)) return values;
150
- throw err;
128
+ await ensureTable();
129
+ const client = getDbExec();
130
+ const placeholders = uniqueKeys.map(() => "?").join(", ");
131
+ const { rows } = await client.execute({
132
+ sql: `SELECT key, value FROM application_state WHERE session_id = ? AND key IN (${placeholders})`,
133
+ args: [sessionId, ...uniqueKeys],
134
+ });
135
+ for (const row of rows) {
136
+ values[row.key as string] = JSON.parse(row.value as string);
151
137
  }
138
+ return values;
152
139
  }
153
140
 
154
141
  export async function appStatePut(
@@ -50,6 +50,18 @@ const FIRST_PARTY_TARBALL_SYMLINK_EXCLUDES = [
50
50
  "*/.claude/skills",
51
51
  ];
52
52
  const localPackageTarballs = new Map<string, string>();
53
+ /** VCS/editor files that don't count as "not empty" for an in-place scaffold. */
54
+ const IN_PLACE_ALLOWLIST = new Set([
55
+ ".git",
56
+ ".gitignore",
57
+ ".gitattributes",
58
+ ".DS_Store",
59
+ ".idea",
60
+ ".vscode",
61
+ "LICENSE",
62
+ "README.md",
63
+ "Thumbs.db",
64
+ ]);
53
65
 
54
66
  /**
55
67
  * Tagged error for input that fails CLI-level validation (repo names, app
@@ -110,6 +122,11 @@ export interface CreateAppOptions {
110
122
  * unconditional workspace scaffold.
111
123
  */
112
124
  forceWorkspace?: boolean;
125
+ /**
126
+ * Internal: scaffold into the current directory instead of a new subfolder.
127
+ * Set when the name argument is `.`/`./` (see `createApp`).
128
+ */
129
+ inPlace?: boolean;
113
130
  }
114
131
 
115
132
  /**
@@ -127,6 +144,13 @@ export async function createApp(
127
144
  ): Promise<void> {
128
145
  const clack = await import("@clack/prompts");
129
146
 
147
+ // `create .` (or `./`) means "scaffold into the current folder" — derive the
148
+ // project name from the folder's basename, like create-react-app / npm init.
149
+ if (name === "." || name === "./") {
150
+ name = path.basename(process.cwd());
151
+ opts = { ...opts, inPlace: true };
152
+ }
153
+
130
154
  // Reject an invalid provided name before any interactive prompt so bad input
131
155
  // fails fast instead of blocking on the start-shape picker below.
132
156
  if (name !== undefined) {
@@ -335,6 +359,44 @@ function assertValidProjectName(
335
359
  process.exit(1);
336
360
  }
337
361
  }
362
+ /**
363
+ * Resolve where a scaffold writes and guard the target. A named project writes
364
+ * to a new sibling subfolder that must not already exist; `create .` writes
365
+ * into the current directory, which must be empty apart from benign VCS/editor
366
+ * files (a pre-existing repo is allowed).
367
+ *
368
+ * For an in-place scaffold we do NOT return the current directory: we build
369
+ * into a private staging directory and `finalizeScaffold` copies the result
370
+ * in afterward. Staging keeps the whole scaffold atomic — a mid-scaffold
371
+ * failure's cleanup can only ever delete the staging dir, never the user's
372
+ * current directory (including its `.git`).
373
+ */
374
+ function resolveScaffoldTarget(
375
+ name: string,
376
+ inPlace: boolean | undefined,
377
+ clack: typeof import("@clack/prompts"),
378
+ ): string {
379
+ if (inPlace) {
380
+ const conflicting = fs
381
+ .readdirSync(process.cwd())
382
+ .filter((entry) => !IN_PLACE_ALLOWLIST.has(entry));
383
+ if (conflicting.length > 0) {
384
+ const shown = conflicting.slice(0, 3).join(", ");
385
+ const more = conflicting.length > 3 ? ", …" : "";
386
+ clack.cancel(
387
+ `Current directory is not empty (${shown}${more}). Scaffold into an empty folder, or run \`create <name>\` to make a new one.`,
388
+ );
389
+ process.exit(1);
390
+ }
391
+ return fs.mkdtempSync(path.join(os.tmpdir(), "agent-native-create-"));
392
+ }
393
+ const targetDir = path.resolve(process.cwd(), name);
394
+ if (fs.existsSync(targetDir)) {
395
+ clack.cancel(`Directory "${name}" already exists.`);
396
+ process.exit(1);
397
+ }
398
+ return targetDir;
399
+ }
338
400
 
339
401
  /* ─────────────────────────────────────────────────────────────────────────
340
402
  * Workspace creation (new default)
@@ -378,11 +440,7 @@ async function createWorkspaceInteractive(
378
440
  });
379
441
  const templates = ["dispatch", ...optionalPicks];
380
442
 
381
- const targetDir = path.resolve(process.cwd(), name);
382
- if (fs.existsSync(targetDir)) {
383
- clack.cancel(`Directory "${name}" already exists.`);
384
- process.exit(1);
385
- }
443
+ const targetDir = resolveScaffoldTarget(name, opts?.inPlace, clack);
386
444
 
387
445
  const s = clack.spinner();
388
446
  for (const template of templates) {
@@ -482,13 +540,15 @@ async function createWorkspaceInteractive(
482
540
  } catch (err: any) {
483
541
  s.stop("Failed to scaffold workspace.");
484
542
  // Remove the partially-scaffolded workspace so a retry of `agent-native
485
- // create <name>` doesn't trip the "Directory already exists" guard.
543
+ // create <name>` doesn't trip the "Directory already exists" guard. For an
544
+ // in-place scaffold `targetDir` is the private staging dir, so this never
545
+ // touches the user's current directory.
486
546
  cleanupOnFailure(targetDir);
487
547
  clack.cancel(err?.message ?? String(err));
488
548
  process.exit(1);
489
549
  }
490
550
 
491
- tryGitInit(targetDir);
551
+ finalizeScaffold(targetDir, opts?.inPlace);
492
552
 
493
553
  // Show the user the tree we just built so the workspace/app distinction is
494
554
  // visible, not just described. First-time users routinely expect their
@@ -845,11 +905,7 @@ async function createStandaloneApp(
845
905
 
846
906
  name = await promptNameIfMissing(name, clack, "app", "my-app");
847
907
 
848
- const targetDir = path.resolve(process.cwd(), name);
849
- if (fs.existsSync(targetDir)) {
850
- clack.cancel(`Directory "${name}" already exists.`);
851
- process.exit(1);
852
- }
908
+ const targetDir = resolveScaffoldTarget(name, opts?.inPlace, clack);
853
909
 
854
910
  // Standalone is single-select — pick one template.
855
911
  let template =
@@ -889,12 +945,14 @@ async function createStandaloneApp(
889
945
  s.stop("App created!");
890
946
  } catch (err: any) {
891
947
  s.stop("Failed to create app.");
948
+ // `targetDir` is the private staging dir for an in-place scaffold, so this
949
+ // only ever removes the staging copy, never the user's current directory.
892
950
  cleanupOnFailure(targetDir);
893
951
  clack.cancel(err?.message ?? String(err));
894
952
  process.exit(1);
895
953
  }
896
954
 
897
- tryGitInit(targetDir);
955
+ finalizeScaffold(targetDir, opts?.inPlace);
898
956
 
899
957
  if (template === "headless") {
900
958
  clack.outro(
@@ -954,6 +1012,35 @@ function cleanupOnFailure(targetDir: string): void {
954
1012
  }
955
1013
  }
956
1014
 
1015
+ /**
1016
+ * Land a finished scaffold in its final home and initialize git. A named
1017
+ * scaffold is already in place, so this only inits git. An in-place scaffold
1018
+ * (`create .`) was built in `scaffoldDir` (a staging dir); copy only the files
1019
+ * that don't already exist into the current directory so pre-existing files
1020
+ * (`.git`, `README.md`, `.gitignore`, editor configs) are preserved, then drop
1021
+ * the staging dir. Git init/commit is skipped when the current directory is
1022
+ * already a repo so we never write an unexpected commit into the user's
1023
+ * history.
1024
+ */
1025
+ function finalizeScaffold(scaffoldDir: string, inPlace?: boolean): void {
1026
+ if (!inPlace) {
1027
+ tryGitInitUnlessRepo(scaffoldDir);
1028
+ return;
1029
+ }
1030
+ const dest = process.cwd();
1031
+ try {
1032
+ copyDir(scaffoldDir, dest, undefined, { skipExisting: true });
1033
+ } finally {
1034
+ cleanupOnFailure(scaffoldDir);
1035
+ }
1036
+ tryGitInitUnlessRepo(dest);
1037
+ }
1038
+
1039
+ function tryGitInitUnlessRepo(dir: string): void {
1040
+ if (fs.existsSync(path.join(dir, ".git"))) return;
1041
+ tryGitInit(dir);
1042
+ }
1043
+
957
1044
  /* ─────────────────────────────────────────────────────────────────────────
958
1045
  * Shared scaffolding helpers
959
1046
  * ───────────────────────────────────────────────────────────────────────── */
@@ -3399,13 +3486,25 @@ function replacePlaceholders(
3399
3486
  }
3400
3487
  }
3401
3488
 
3402
- function copyDir(src: string, dest: string, root?: string): void {
3489
+ function copyDir(
3490
+ src: string,
3491
+ dest: string,
3492
+ root?: string,
3493
+ opts?: { skipExisting?: boolean },
3494
+ ): void {
3403
3495
  const resolvedRoot = root ?? path.resolve(src);
3496
+ const skipExisting = opts?.skipExisting ?? false;
3404
3497
  fs.mkdirSync(dest, { recursive: true });
3405
3498
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
3406
3499
  const srcPath = path.join(src, entry.name);
3407
3500
  if (shouldSkipScaffoldEntry(entry.name, srcPath)) continue;
3408
3501
  const destPath = path.join(dest, entry.name);
3502
+ // Preserve anything already at the destination (in-place scaffold merges
3503
+ // into a directory the user may already own). Directories still recurse so
3504
+ // new files land inside a pre-existing folder.
3505
+ if (skipExisting && !entry.isDirectory() && fs.existsSync(destPath)) {
3506
+ continue;
3507
+ }
3409
3508
  if (entry.isSymbolicLink()) {
3410
3509
  const target = fs.readlinkSync(srcPath);
3411
3510
  const resolvedTarget = path.resolve(path.dirname(srcPath), target);
@@ -3415,7 +3514,7 @@ function copyDir(src: string, dest: string, root?: string): void {
3415
3514
  try {
3416
3515
  const stat = fs.statSync(srcPath);
3417
3516
  if (stat.isDirectory()) {
3418
- copyDir(srcPath, destPath, resolvedRoot);
3517
+ copyDir(srcPath, destPath, resolvedRoot, opts);
3419
3518
  } else {
3420
3519
  fs.copyFileSync(srcPath, destPath);
3421
3520
  }
@@ -3424,7 +3523,7 @@ function copyDir(src: string, dest: string, root?: string): void {
3424
3523
  }
3425
3524
  }
3426
3525
  } else if (entry.isDirectory()) {
3427
- copyDir(srcPath, destPath, resolvedRoot);
3526
+ copyDir(srcPath, destPath, resolvedRoot, opts);
3428
3527
  } else {
3429
3528
  fs.copyFileSync(srcPath, destPath);
3430
3529
  }
@@ -3421,7 +3421,11 @@ const AssistantChatInner = forwardRef<
3421
3421
  }
3422
3422
  window.dispatchEvent(
3423
3423
  new CustomEvent("agentNative.chatRunning", {
3424
- detail: { isRunning: false, tabId: tabId || threadId },
3424
+ detail: {
3425
+ isRunning: false,
3426
+ tabId: tabId || threadId,
3427
+ reason: "failed",
3428
+ },
3425
3429
  }),
3426
3430
  );
3427
3431
  return;
@@ -4510,7 +4514,11 @@ const AssistantChatInner = forwardRef<
4510
4514
  if (typeof window !== "undefined") {
4511
4515
  window.dispatchEvent(
4512
4516
  new CustomEvent("agentNative.chatRunning", {
4513
- detail: { isRunning: false, tabId: tabId || threadId },
4517
+ detail: {
4518
+ isRunning: false,
4519
+ tabId: tabId || threadId,
4520
+ reason: "stopped",
4521
+ },
4514
4522
  }),
4515
4523
  );
4516
4524
  }
@@ -115,7 +115,11 @@ export interface FrameOriginMessage {
115
115
 
116
116
  export interface ChatRunningMessage {
117
117
  type: "agentNative.chatRunning";
118
- detail: { isRunning: boolean; tabId?: string };
118
+ detail: {
119
+ isRunning: boolean;
120
+ tabId?: string;
121
+ reason?: "stopped" | "failed";
122
+ };
119
123
  }
120
124
 
121
125
  export interface UserInfoMessage {
@@ -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();