@agent-native/core 0.228.2-nightly-20261113152307 → 0.228.2

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 (34) hide show
  1. package/dist/cli/skills-content/an-skill.d.ts +1 -1
  2. package/dist/cli/skills-content/an-skill.js +16 -0
  3. package/dist/collab/routes.d.ts +1 -1
  4. package/dist/collab/struct-routes.d.ts +1 -1
  5. package/dist/db/schema-snapshot.generated.js +2 -1
  6. package/dist/framework-package-versions.json +11 -11
  7. package/dist/identity/rekey.js +2 -0
  8. package/dist/mcp/embed-app.js +259 -3
  9. package/dist/mcp/oauth-route.js +7 -0
  10. package/dist/notifications/routes.d.ts +3 -3
  11. package/dist/observability/routes.d.ts +5 -5
  12. package/dist/secrets/api-keys.js +8 -7
  13. package/dist/secrets/index.d.ts +2 -1
  14. package/dist/secrets/index.js +2 -1
  15. package/dist/secrets/routes.d.ts +4 -4
  16. package/dist/secrets/routes.js +7 -8
  17. package/dist/secrets/schema.d.ts +51 -0
  18. package/dist/secrets/schema.js +3 -0
  19. package/dist/secrets/storage.d.ts +83 -1
  20. package/dist/secrets/storage.js +194 -6
  21. package/dist/secrets/usage.js +2 -4
  22. package/dist/secrets/vault-managed.d.ts +17 -0
  23. package/dist/secrets/vault-managed.js +19 -0
  24. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  25. package/dist/server/builder-signup-experiment.d.ts +4 -4
  26. package/dist/server/credential-provider.js +7 -7
  27. package/dist/server/first-touch-route.d.ts +1 -1
  28. package/dist/server/transcribe-voice.d.ts +1 -1
  29. package/dist/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +11 -0
  30. package/docs/content/security.mdx +1 -1
  31. package/docs/content/template-dispatch-developers.mdx +7 -7
  32. package/docs/content/template-dispatch-reference.mdx +16 -10
  33. package/package.json +4 -4
  34. package/src/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +11 -0
@@ -27,14 +27,14 @@ async function canReadDesignatedVaultFallback(vaultOrgId, credentialKey) {
27
27
  }
28
28
  try {
29
29
  const result = await getDbExec().execute({
30
+ // A grant names its key itself; legacy grants without `credential_key`
31
+ // match nothing until they are backfilled.
30
32
  sql: `SELECT 1
31
- FROM vault_grants AS grants
32
- INNER JOIN vault_secrets AS secrets ON secrets.id = grants.secret_id
33
- WHERE grants.org_id = ?
34
- AND grants.app_id = ?
35
- AND grants.status = 'active'
36
- AND secrets.org_id = grants.org_id
37
- AND secrets.credential_key = ?
33
+ FROM vault_grants
34
+ WHERE org_id = ?
35
+ AND app_id = ?
36
+ AND status = 'active'
37
+ AND credential_key = ?
38
38
  LIMIT 1`,
39
39
  args: [vaultOrgId, appId, credentialKey],
40
40
  });
@@ -17,8 +17,8 @@ export type FirstTouchSyncResult = {
17
17
  * browser's `an_ft` and `an_lt` again, so Safari keeps them their 30 days.
18
18
  */
19
19
  export declare function createFirstTouchHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
20
- error: string;
21
20
  reason?: undefined;
21
+ error: string;
22
22
  stored?: undefined;
23
23
  firstTouch?: undefined;
24
24
  } | {
@@ -17,8 +17,8 @@
17
17
  * typed JSON-in / JSON-out).
18
18
  */
19
19
  export declare function createTranscribeVoiceHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
20
- error: string;
21
20
  text?: undefined;
21
+ error: string;
22
22
  } | {
23
23
  error?: undefined;
24
24
  text: string;
@@ -120,6 +120,17 @@ resource CSP should include the exact origins of apps granted through Dispatch,
120
120
  not broad sources like `https:`. This lets Claude's transplant path fetch the
121
121
  signed target app HTML while keeping the connector's resource surface narrow.
122
122
 
123
+ Side panel and app tools: every `embedApp()` resource sets
124
+ `_meta.ui.preferredDisplayMode: "fullscreen"`, so Claude opens the app in its
125
+ side panel beside the chat (other hosts ignore the field). The shell declares
126
+ `availableDisplayModes` and `tools` in `ui/initialize` and answers the host's
127
+ `tools/list` / `tools/call` on the open view: `read_view` returns the page's
128
+ path, title, selection and visible text, `navigate_view` moves the open app to
129
+ another same-origin path in place, and any page tools the app registered through
130
+ WebMCP are passed through. Prefer these over a new `open_app` call when the user
131
+ asks to change what the open panel shows, since each new UI tool call opens a
132
+ new view.
133
+
123
134
  Host sizing rule: the MCP resource shell owns a bounded inline height and the
124
135
  embedded route should scroll internally. `embedApp({ height })` defaults to a
125
136
  `560px` shell, clamps to `320-900px`, and subtracts `44px` for the wrapper bar
@@ -322,7 +322,7 @@ pnpm action db-check-scoping --require-org # Also require org_id
322
322
  | OAuth tokens (Google, GitHub) | `oauth_tokens` store via `saveOAuthTokens()` |
323
323
  | Session tokens | Automatic (Better Auth handles this) |
324
324
 
325
- Per-user/per-org credentials and the vault are encrypted at rest with AES-256-GCM. The Dispatch vault keeps no plaintext copy: `vault_secrets` holds names and metadata, and each value lives only in `app_secrets`. 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).
325
+ Per-user/per-org credentials and the vault are encrypted at rest with AES-256-GCM. The Dispatch vault keeps no plaintext copy: each key's name, label, and encrypted value live together in its `app_secrets` row, and the vault's own tables hold only grants, requests, and the audit log. 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).
326
326
 
327
327
  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.
328
328
 
@@ -17,7 +17,7 @@ This page is for engineers extending Dispatch or customizing the template: how t
17
17
  filename="packages/dispatch/src/server/lib/vault-store.ts"
18
18
  language="ts"
19
19
  code={
20
- 'export async function approveRequest(\n requestId: string,\n secretValue: string,\n secretName?: string,\n ctx: VaultCtx = requireVaultCtx(),\n) {\n await assertCanManageVault();\n const db = getDb();\n const request = await getRequestForTenant(requestId, ctx);\n if (!request) throw new Error("Request not found");\n\n const timestamp = now();\n const reviewer = ctx.ownerEmail;\n const staleApplyingBefore = timestamp - 5 * 60 * 1000;\n\n // Fence the transition on the current status — scoped to caller\'s tenant —\n // so a concurrent approve can\'t both win: only the caller that flips\n // pending -> applying proceeds to create the secret/grant. A crashed worker\n // leaves an applying row behind, so a later reviewer may reclaim a lease\n // that has been idle for five minutes. See\n // claimAgentTeamRun in packages/core/src/server/agent-teams-run-queue.ts\n // for the same pattern.\n const claimed = await db\n .update(schema.vaultRequests)\n .set({\n status: "applying",\n updatedAt: timestamp,\n })\n .where(\n and(\n eq(schema.vaultRequests.id, requestId),\n tenantScope(schema.vaultRequests, ctx),\n or(\n eq(schema.vaultRequests.status, "pending"),\n and(\n eq(schema.vaultRequests.status, "applying"),\n sql`${schema.vaultRequests.updatedAt} < ${staleApplyingBefore}`,\n ),\n ),\n ),\n )\n .returning();\n\n if (claimed.length === 0) {\n const current = await getRequestForTenant(requestId, ctx);\n if (current?.status === "applying") {\n throw new Error("Vault request is already being applied");\n }\n return current;\n }\n\n const claimedRequest = claimed[0];\n\n // Secret + grant must land in the REQUEST\'s tenant, not the approver\'s\n // (the approver may be acting on behalf of another user in the same org).\n const requestCtx = ctxForRow(claimedRequest);\n\n try {\n // Check if secret already exists in the request\'s tenant for this key.\n const existingSecrets = await db\n .select()\n .from(schema.vaultSecrets)\n .where(\n and(\n eq(schema.vaultSecrets.credentialKey, claimedRequest.credentialKey),\n ctxScope(schema.vaultSecrets, requestCtx),\n ),\n );\n let secret = existingSecrets[0] ?? null;\n\n if (!secret) {\n secret = await createSecret(\n {\n credentialKey: claimedRequest.credentialKey,\n value: secretValue,\n name: secretName || claimedRequest.credentialKey,\n },\n requestCtx,\n );\n }\n\n if (secret) {\n // Create the grant in the request\'s tenant as well.\n await createGrant(secret.id, claimedRequest.appId, requestCtx);\n }\n } catch (error) {\n await db\n .update(schema.vaultRequests)\n .set({ status: "pending", updatedAt: now() })\n .where(\n and(\n eq(schema.vaultRequests.id, requestId),\n tenantScope(schema.vaultRequests, ctx),\n eq(schema.vaultRequests.status, "applying"),\n ),\n );\n throw error;\n }\n\n await db\n .update(schema.vaultRequests)\n .set({\n status: "approved",\n reviewedBy: reviewer,\n reviewedAt: timestamp,\n updatedAt: now(),\n })\n .where(\n and(\n eq(schema.vaultRequests.id, requestId),\n tenantScope(schema.vaultRequests, ctx),\n eq(schema.vaultRequests.status, "applying"),\n ),\n );\n\n await recordVaultAudit({\n action: "request.approved",\n appId: claimedRequest.appId,\n summary: `Approved ${claimedRequest.credentialKey} for ${claimedRequest.appId} (requested by ${claimedRequest.requestedBy})`,\n metadata: { requestId, reviewer },\n });\n\n return getRequestForTenant(requestId, ctx);\n}'
20
+ 'export async function approveRequest(\n requestId: string,\n secretValue: string,\n secretName?: string,\n ctx: VaultCtx = requireVaultCtx(),\n) {\n await assertCanManageVault();\n const db = getDb();\n const request = await getRequestForTenant(requestId, ctx);\n if (!request) throw new Error("Request not found");\n\n const timestamp = now();\n const reviewer = ctx.ownerEmail;\n const staleApplyingBefore = timestamp - 5 * 60 * 1000;\n\n // Fence the transition on the current status — scoped to caller\'s tenant —\n // so a concurrent approve can\'t both win: only the caller that flips\n // pending -> applying proceeds to create the secret/grant. A crashed worker\n // leaves an applying row behind, so a later reviewer may reclaim a lease\n // that has been idle for five minutes. See\n // claimAgentTeamRun in packages/core/src/server/agent-teams-run-queue.ts\n // for the same pattern.\n const claimed = await db\n .update(schema.vaultRequests)\n .set({\n status: "applying",\n updatedAt: timestamp,\n })\n .where(\n and(\n eq(schema.vaultRequests.id, requestId),\n tenantScope(schema.vaultRequests, ctx),\n or(\n eq(schema.vaultRequests.status, "pending"),\n and(\n eq(schema.vaultRequests.status, "applying"),\n sql`${schema.vaultRequests.updatedAt} < ${staleApplyingBefore}`,\n ),\n ),\n ),\n )\n .returning();\n\n const claimedRequest = claimed.at(0);\n if (!claimedRequest) {\n const current = await getRequestForTenant(requestId, ctx);\n if (current?.status === "applying") {\n throw new Error("Vault request is already being applied");\n }\n return current;\n }\n\n // Secret + grant must land in the REQUEST\'s tenant, not the approver\'s\n // (the approver may be acting on behalf of another user in the same org).\n const requestCtx = ctxForRow(claimedRequest);\n\n try {\n // A key that already exists in the scope is only granted: the approver\'s\n // value never overwrites a row, whether the Vault or an app wrote it.\n let secretId = (await listScopeSecrets(requestCtx)).find(\n (row) => row.credentialKey === claimedRequest.credentialKey,\n )?.id;\n\n if (!secretId) {\n secretId = (\n await createSecret(\n {\n credentialKey: claimedRequest.credentialKey,\n value: secretValue,\n name: secretName || claimedRequest.credentialKey,\n },\n requestCtx,\n )\n ).id;\n }\n\n await createGrant(secretId, claimedRequest.appId, requestCtx);\n } catch (error) {\n await db\n .update(schema.vaultRequests)\n .set({ status: "pending", updatedAt: now() })\n .where(\n and(\n eq(schema.vaultRequests.id, requestId),\n tenantScope(schema.vaultRequests, ctx),\n eq(schema.vaultRequests.status, "applying"),\n ),\n );\n throw error;\n }\n\n await db\n .update(schema.vaultRequests)\n .set({\n status: "approved",\n reviewedBy: reviewer,\n reviewedAt: timestamp,\n updatedAt: now(),\n })\n .where(\n and(\n eq(schema.vaultRequests.id, requestId),\n tenantScope(schema.vaultRequests, ctx),\n eq(schema.vaultRequests.status, "applying"),\n ),\n );\n\n await recordVaultAudit({\n action: "request.approved",\n credentialKey: claimedRequest.credentialKey,\n appId: claimedRequest.appId,\n summary: `Approved ${claimedRequest.credentialKey} for ${claimedRequest.appId} (requested by ${claimedRequest.requestedBy})`,\n metadata: { requestId, reviewer },\n });\n\n return getRequestForTenant(requestId, ctx);\n}'
21
21
  }
22
22
  annotations={[
23
23
  {
@@ -26,22 +26,22 @@ This page is for engineers extending Dispatch or customizing the template: how t
26
26
  note: "The claim only succeeds if the row is still pending, or stuck applying for over five minutes. Two simultaneous approvals can't both win: only one caller's update actually matches a row and returns it.",
27
27
  },
28
28
  {
29
- lines: "44-50",
29
+ lines: "44-51",
30
30
  label: "Losing the race isn't an error",
31
31
  note: "If the claim matched nothing, the request is either already mid-flight elsewhere (throw) or was already resolved by the time this call ran (just return its current state).",
32
32
  },
33
33
  {
34
- lines: "59-69",
35
- label: "Reuse an existing secret instead of duplicating one",
36
- note: "A second request for the same credential key finds the secret already created by an earlier approval and grants access to it, rather than creating a duplicate.",
34
+ lines: "58-77",
35
+ label: "Grant an existing key instead of overwriting it",
36
+ note: "A key that already exists in the scope is only granted, so a second request for the same credential key reuses it and the approver's value never replaces a row, whether the Vault or an app wrote it.",
37
37
  },
38
38
  {
39
- lines: "86-98",
39
+ lines: "78-90",
40
40
  label: "Roll back the lock on failure",
41
41
  note: "If creating the secret or grant throws, the request goes back to pending instead of staying stuck in applying, which is what makes the five-minute stale-lease reclaim above necessary in the first place.",
42
42
  },
43
43
  {
44
- lines: "100-114",
44
+ lines: "92-106",
45
45
  label: "Finalize only from the lock this call holds",
46
46
  note: "The final flip to approved is still scoped to status = applying, so it can only apply the transition this specific call claimed.",
47
47
  },
@@ -136,21 +136,26 @@ Dispatch ships close to a hundred actions. Most of the UI-facing ones are alread
136
136
  id="doc-block-dispatch11"
137
137
  title="Secrets vault schema"
138
138
  summary={
139
- "A secret is stored once. A grant gives a named app access. A request plus review gates sensitive access before a grant exists. The audit log records who used which secret, when. Defined in @agent-native/dispatch (packages/dispatch/src/db/schema.ts)."
139
+ "A key is stored once, encrypted, as a row of the credential store (app_secrets, owned by @agent-native/core). A grant gives a named app access to it by key name. A request plus review gates sensitive access before a grant exists. The audit log records who changed which key, when. The grant, request, and audit tables are defined in @agent-native/dispatch (packages/dispatch/src/db/schema.ts)."
140
140
  }
141
141
  entities={[
142
142
  {
143
143
  id: "secrets",
144
- name: "vault_secrets",
145
- note: "Stored credential values",
144
+ name: "app_secrets",
145
+ note: "Stored credentials, encrypted",
146
146
  fields: [
147
147
  { name: "id", type: "text", pk: true },
148
- { name: "owner_email", type: "text" },
149
- { name: "org_id", type: "text", nullable: true },
150
- { name: "name", type: "text" },
151
- { name: "credential_key", type: "text" },
152
- { name: "value", type: "text", note: "secret value" },
148
+ {
149
+ name: "scope",
150
+ type: "text",
151
+ note: "org, or workspace for a solo vault",
152
+ },
153
+ { name: "scope_id", type: "text" },
154
+ { name: "key", type: "text", note: "credential key" },
155
+ { name: "encrypted_value", type: "text", note: "ciphertext only" },
156
+ { name: "label", type: "text", nullable: true },
153
157
  { name: "provider", type: "text", nullable: true },
158
+ { name: "created_by", type: "text", nullable: true },
154
159
  ],
155
160
  },
156
161
  {
@@ -159,7 +164,8 @@ Dispatch ships close to a hundred actions. Most of the UI-facing ones are alread
159
164
  note: "Per-app access grant",
160
165
  fields: [
161
166
  { name: "id", type: "text", pk: true },
162
- { name: "secret_id", type: "text", fk: "vault_secrets.id" },
167
+ { name: "secret_id", type: "text", fk: "app_secrets.id" },
168
+ { name: "credential_key", type: "text", nullable: true },
163
169
  { name: "app_id", type: "text" },
164
170
  { name: "granted_by", type: "text" },
165
171
  { name: "status", type: "text" },
@@ -187,7 +193,7 @@ Dispatch ships close to a hundred actions. Most of the UI-facing ones are alread
187
193
  {
188
194
  name: "secret_id",
189
195
  type: "text",
190
- fk: "vault_secrets.id",
196
+ fk: "app_secrets.id",
191
197
  nullable: true,
192
198
  },
193
199
  { name: "app_id", type: "text", nullable: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.228.2-nightly-20261113152307",
3
+ "version": "0.228.2",
4
4
  "description": "The agentic application framework for building autonomous agents with intuitive UIs",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -771,8 +771,8 @@
771
771
  "yaml": "^2.9.0",
772
772
  "yjs": "^13.6.33",
773
773
  "zod": "^4.3.6",
774
- "@agent-native/agentkit": "^0.228.2-nightly-20261113152307",
775
- "@agent-native/recap-cli": "^0.5.102"
774
+ "@agent-native/agentkit": "^0.228.2",
775
+ "@agent-native/recap-cli": "^0.5.103"
776
776
  },
777
777
  "devDependencies": {
778
778
  "@ai-sdk/anthropic": "^3.0.71",
@@ -845,7 +845,7 @@
845
845
  "xlsx": "^0.18.5"
846
846
  },
847
847
  "peerDependencies": {
848
- "@agent-native/core-corpus": "0.228.2-nightly-20261113152307",
848
+ "@agent-native/core-corpus": ">=0.196.0",
849
849
  "@ai-sdk/anthropic": ">=3",
850
850
  "@ai-sdk/cohere": ">=3",
851
851
  "@ai-sdk/google": ">=3",
@@ -120,6 +120,17 @@ resource CSP should include the exact origins of apps granted through Dispatch,
120
120
  not broad sources like `https:`. This lets Claude's transplant path fetch the
121
121
  signed target app HTML while keeping the connector's resource surface narrow.
122
122
 
123
+ Side panel and app tools: every `embedApp()` resource sets
124
+ `_meta.ui.preferredDisplayMode: "fullscreen"`, so Claude opens the app in its
125
+ side panel beside the chat (other hosts ignore the field). The shell declares
126
+ `availableDisplayModes` and `tools` in `ui/initialize` and answers the host's
127
+ `tools/list` / `tools/call` on the open view: `read_view` returns the page's
128
+ path, title, selection and visible text, `navigate_view` moves the open app to
129
+ another same-origin path in place, and any page tools the app registered through
130
+ WebMCP are passed through. Prefer these over a new `open_app` call when the user
131
+ asks to change what the open panel shows, since each new UI tool call opens a
132
+ new view.
133
+
123
134
  Host sizing rule: the MCP resource shell owns a bounded inline height and the
124
135
  embedded route should scroll internally. `embedApp({ height })` defaults to a
125
136
  `560px` shell, clamps to `320-900px`, and subtracts `44px` for the wrapper bar