@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/src/cli/create.ts CHANGED
@@ -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();
@@ -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
 
@@ -86,13 +86,16 @@ export function createWorkspaceFilesTool(): Record<string, ActionEntry> {
86
86
  'MIME type for new files. Default: "text/plain". Use "application/json" for JSON, "text/markdown" for Markdown.',
87
87
  },
88
88
  offset: {
89
- type: "number",
89
+ type: "integer",
90
+ minimum: 0,
90
91
  description:
91
- "Character offset to start reading from (for paging large files). Default: 0.",
92
+ "Non-negative character offset to start reading from (for paging large files). Default: 0.",
92
93
  },
93
94
  maxChars: {
94
- type: "number",
95
- description: `Maximum characters to return when reading. Default: ${DEFAULT_READ_CHARS}. Max: ${MAX_READ_CHARS}.`,
95
+ type: "integer",
96
+ minimum: 1,
97
+ maximum: MAX_READ_CHARS,
98
+ description: `Positive maximum characters to return when reading. Default: ${DEFAULT_READ_CHARS}. Max: ${MAX_READ_CHARS}.`,
96
99
  },
97
100
  pattern: {
98
101
  type: "string",
@@ -168,16 +171,19 @@ export function createWorkspaceFilesTool(): Record<string, ActionEntry> {
168
171
  if (!path) return "Error: path is required for read.";
169
172
  const rawOffset = Number(args.offset);
170
173
  const offset =
171
- Number.isFinite(rawOffset) && rawOffset > 0 ? rawOffset : 0;
174
+ Number.isFinite(rawOffset) && rawOffset > 0
175
+ ? Math.floor(rawOffset)
176
+ : 0;
172
177
  const rawMax = Number(args.maxChars);
173
178
  const maxChars =
174
179
  Number.isFinite(rawMax) && rawMax > 0
175
- ? Math.min(rawMax, MAX_READ_CHARS)
180
+ ? Math.min(Math.max(1, Math.floor(rawMax)), MAX_READ_CHARS)
176
181
  : DEFAULT_READ_CHARS;
177
182
 
183
+ // The sentinel character distinguishes an exact page from a truncated one.
178
184
  const file = await readWorkspaceFile(scope, path, {
179
185
  offset,
180
- maxChars,
186
+ maxChars: maxChars + 1,
181
187
  });
182
188
  if (!file) {
183
189
  return JSON.stringify({
@@ -186,19 +192,20 @@ export function createWorkspaceFilesTool(): Record<string, ActionEntry> {
186
192
  });
187
193
  }
188
194
 
189
- const truncated = file.content.length >= maxChars;
195
+ const truncated = file.content.length > maxChars;
196
+ const content = file.content.slice(0, maxChars);
190
197
  return JSON.stringify({
191
198
  ok: true,
192
199
  path: file.path,
193
200
  contentType: file.contentType,
194
201
  sizeBytes: file.sizeBytes,
195
202
  updatedAt: file.updatedAt,
196
- content: file.content,
203
+ content,
197
204
  ...(truncated
198
205
  ? {
199
206
  truncated: true,
200
- nextOffset: offset + file.content.length,
201
- hint: `File has more content. Call again with offset: ${offset + file.content.length}`,
207
+ nextOffset: offset + content.length,
208
+ hint: `File has more content. Call again with offset: ${offset + content.length}`,
202
209
  }
203
210
  : {}),
204
211
  });