@agent-native/core 0.164.11 → 0.164.15

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 (106) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +84 -0
  3. package/corpus/templates/calendar/AGENTS.md +5 -0
  4. package/corpus/templates/calendar/actions/delete-event.ts +1 -1
  5. package/corpus/templates/calendar/actions/delete-events.ts +508 -0
  6. package/corpus/templates/calendar/actions/event-action-helpers.ts +1 -1
  7. package/corpus/templates/calendar/actions/list-events.ts +53 -0
  8. package/corpus/templates/calendar/changelog/2026-08-18-the-agent-can-now-remove-many-meetings-at-once-such-as-every.md +6 -0
  9. package/corpus/templates/calendar/server/lib/event-weekday.ts +112 -0
  10. package/corpus/templates/content/actions/_database-source-utils.ts +25 -2
  11. package/corpus/templates/content/actions/_database-utils.ts +2 -0
  12. package/corpus/templates/content/actions/_local-folder-source.ts +186 -2
  13. package/corpus/templates/content/actions/connect-local-folder-source.ts +23 -8
  14. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +3 -3
  15. package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +2 -2
  16. package/corpus/templates/content/actions/sync-local-folder-source.ts +130 -12
  17. package/corpus/templates/content/app/components/LocalFolderLiveSync.tsx +77 -0
  18. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +354 -8
  19. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +23 -1
  20. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +78 -1
  21. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +36 -0
  22. package/corpus/templates/content/app/components/editor/local-source-write-state.ts +52 -0
  23. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +244 -6
  24. package/corpus/templates/content/app/components/sidebar/local-source-hierarchy.ts +189 -0
  25. package/corpus/templates/content/app/hooks/use-create-page.ts +26 -4
  26. package/corpus/templates/content/app/hooks/use-documents.ts +83 -0
  27. package/corpus/templates/content/app/i18n-data.ts +79 -0
  28. package/corpus/templates/content/app/lib/desktop-content-files.ts +57 -1
  29. package/corpus/templates/content/app/lib/local-content-source-files.ts +210 -34
  30. package/corpus/templates/content/app/lib/local-folder-live-sync.ts +191 -0
  31. package/corpus/templates/content/app/root.tsx +2 -1
  32. package/corpus/templates/content/app/routes/_app.local-files.tsx +103 -38
  33. package/corpus/templates/content/docs/product/capabilities/content.source.file-folder.md +2 -2
  34. package/corpus/templates/content/docs/product/features/content.feature.bring-your-local-work.md +3 -3
  35. package/corpus/templates/content/docs/product/roadmap.md +2 -2
  36. package/corpus/templates/content/docs/solutions/2026-08-18-content-navigation-coherence-shape.md +482 -0
  37. package/corpus/templates/content/shared/api.ts +25 -0
  38. package/corpus/templates/factory/AGENTS.md +2 -1
  39. package/corpus/templates/factory/actions/get-factory-graph-version.ts +69 -0
  40. package/corpus/templates/factory/actions/list-factory-graph-versions.ts +82 -0
  41. package/corpus/templates/factory/actions/restore-factory-graph-version.ts +163 -0
  42. package/corpus/templates/factory/actions/save-factory-graph.ts +93 -60
  43. package/corpus/templates/factory/app/components/factory/FactoryHistoryView.tsx +709 -0
  44. package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +62 -2
  45. package/corpus/templates/factory/app/components/ui/alert-dialog.tsx +1 -0
  46. package/corpus/templates/factory/app/i18n/en-US.ts +43 -0
  47. package/corpus/templates/factory/app/routes/factory.tsx +115 -15
  48. package/corpus/templates/factory/server/plugins/agent-chat.ts +3 -1
  49. package/dist/app-config/auth.d.ts +4 -0
  50. package/dist/app-config/auth.js +7 -0
  51. package/dist/app-config/schema.d.ts +3 -0
  52. package/dist/app-config/schema.js +2 -0
  53. package/dist/cli/code-agent-executor.js +6 -1
  54. package/dist/cli/code-agent-mcp-config.d.ts +2 -2
  55. package/dist/cli/code-agent-mcp-config.js +34 -9
  56. package/dist/client/org/OrgSwitcher.js +8 -4
  57. package/dist/collab/awareness.d.ts +2 -2
  58. package/dist/collab/routes.d.ts +1 -1
  59. package/dist/collab/struct-routes.d.ts +1 -1
  60. package/dist/mcp-client/config.d.ts +11 -0
  61. package/dist/mcp-client/config.js +33 -26
  62. package/dist/notifications/routes.d.ts +3 -3
  63. package/dist/observability/routes.d.ts +3 -3
  64. package/dist/progress/routes.d.ts +1 -1
  65. package/dist/resources/handlers.d.ts +2 -2
  66. package/dist/secrets/routes.d.ts +9 -9
  67. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  68. package/dist/server/auth.js +15 -1
  69. package/dist/server/realtime-token.d.ts +1 -1
  70. package/docs/content/actions-access-control.mdx +136 -2
  71. package/docs/content/actions-agent-tools.mdx +101 -0
  72. package/docs/content/actions-defining.mdx +2 -2
  73. package/docs/content/audit-log.mdx +1 -1
  74. package/docs/content/authentication.mdx +1 -1
  75. package/docs/content/cloudflare.mdx +43 -0
  76. package/docs/content/creating-templates.mdx +1 -1
  77. package/docs/content/deploy-an-app.mdx +109 -0
  78. package/docs/content/deployment-environment-variables.mdx +101 -0
  79. package/docs/content/deployment.mdx +125 -792
  80. package/docs/content/docs-components.mdx +2 -2
  81. package/docs/content/environment-variables.mdx +41 -41
  82. package/docs/content/file-uploads.mdx +74 -40
  83. package/docs/content/harness-agents.mdx +2 -2
  84. package/docs/content/internationalization.mdx +61 -65
  85. package/docs/content/key-concepts.mdx +3 -3
  86. package/docs/content/multi-app-workspace.mdx +2 -2
  87. package/docs/content/netlify.mdx +47 -0
  88. package/docs/content/node-docker.mdx +138 -0
  89. package/docs/content/organizations-teams-permissions.mdx +5 -5
  90. package/docs/content/other-platforms.mdx +27 -0
  91. package/docs/content/ssr-caching.mdx +45 -0
  92. package/docs/content/template-assets-developers.mdx +2 -2
  93. package/docs/content/template-chat-developers.mdx +2 -2
  94. package/docs/content/template-content-developers.mdx +2 -2
  95. package/docs/content/{local-file-mode.mdx → template-content-local-files.mdx} +2 -1
  96. package/docs/content/template-content-sync.mdx +2 -2
  97. package/docs/content/toolkit-agent-ux.mdx +1 -1
  98. package/docs/content/toolkit-resources.mdx +2 -2
  99. package/docs/content/updating-ui-in-production.mdx +35 -0
  100. package/docs/content/vercel.mdx +38 -0
  101. package/docs/content/what-is-agent-native.mdx +1 -1
  102. package/docs/content/workspace-deployment.mdx +350 -0
  103. package/package.json +1 -1
  104. package/docs/content/agents.mdx +0 -9
  105. package/docs/content/database.mdx +0 -317
  106. package/docs/content/human-approval.mdx +0 -156
@@ -1,6 +1,12 @@
1
1
  import { useActionQuery } from "@agent-native/core/client/hooks";
2
2
  import { useT } from "@agent-native/core/client/i18n";
3
- import { IconArrowRight, IconPlus, IconTrash } from "@tabler/icons-react";
3
+ import {
4
+ IconAlertCircle,
5
+ IconArrowRight,
6
+ IconLoader2,
7
+ IconPlus,
8
+ IconTrash,
9
+ } from "@tabler/icons-react";
4
10
  import { useMemo, useState } from "react";
5
11
  import { useSearchParams } from "react-router";
6
12
 
@@ -22,8 +28,13 @@ interface FactoryInspectorProps {
22
28
  factoryId?: string;
23
29
  dirty: boolean;
24
30
  saving: boolean;
31
+ saveError: string | null;
32
+ saveConflictNeedsResolution: boolean;
33
+ refreshing: boolean;
25
34
  onGraphChange: (graph: FactoryCanvasGraph) => void;
26
35
  onSave: () => void;
36
+ onRefresh: () => void;
37
+ onDiscardLocalChanges: () => void;
27
38
  onAddNode: () => void;
28
39
  onDeleteNode: (nodeId: string) => void;
29
40
  onConnect: (sourceId: string, targetId: string) => void;
@@ -54,8 +65,13 @@ export function FactoryInspector({
54
65
  factoryId,
55
66
  dirty,
56
67
  saving,
68
+ saveError,
69
+ saveConflictNeedsResolution,
70
+ refreshing,
57
71
  onGraphChange,
58
72
  onSave,
73
+ onRefresh,
74
+ onDiscardLocalChanges,
59
75
  onAddNode,
60
76
  onDeleteNode,
61
77
  onConnect,
@@ -383,12 +399,56 @@ export function FactoryInspector({
383
399
  </section>
384
400
  </div>
385
401
 
402
+ {saveError ? (
403
+ <div
404
+ className="flex items-start gap-2 border-t border-border/60 bg-destructive/5 p-4 text-sm text-destructive"
405
+ role="alert"
406
+ >
407
+ <IconAlertCircle className="mt-0.5 size-4 shrink-0" />
408
+ <div className="min-w-0 flex-1 space-y-2">
409
+ <p>{saveError}</p>
410
+ {saveConflictNeedsResolution ? (
411
+ <p className="text-xs leading-5 text-destructive/80">
412
+ {t("factoryInspector.saveConflictHint")}
413
+ </p>
414
+ ) : null}
415
+ <div className="flex flex-wrap gap-2">
416
+ {!saveConflictNeedsResolution ? (
417
+ <Button
418
+ type="button"
419
+ variant="link"
420
+ size="sm"
421
+ className="h-auto p-0 text-destructive"
422
+ disabled={refreshing}
423
+ onClick={onRefresh}
424
+ >
425
+ {refreshing ? (
426
+ <IconLoader2 className="size-3.5 animate-spin" />
427
+ ) : null}
428
+ {t("triage.refresh")}
429
+ </Button>
430
+ ) : (
431
+ <Button
432
+ type="button"
433
+ variant="link"
434
+ size="sm"
435
+ className="h-auto p-0 text-destructive"
436
+ onClick={onDiscardLocalChanges}
437
+ >
438
+ {t("factoryInspector.discardLocalChanges")}
439
+ </Button>
440
+ )}
441
+ </div>
442
+ </div>
443
+ </div>
444
+ ) : null}
445
+
386
446
  {(dirty || saving) && (
387
447
  <div className="bg-muted/15 p-4">
388
448
  <Button
389
449
  type="button"
390
450
  className="w-full"
391
- disabled={saving}
451
+ disabled={saving || refreshing}
392
452
  onClick={onSave}
393
453
  >
394
454
  {saving
@@ -0,0 +1 @@
1
+ export * from "@agent-native/toolkit/ui/alert-dialog";
@@ -204,6 +204,46 @@ const messages = {
204
204
  auditUxImpact: "UX impact",
205
205
  auditOwnerArea: "Owner area",
206
206
  auditGuardsLabel: "Guards",
207
+ historyTab: "History",
208
+ historyTitle: "Saved versions",
209
+ historyLoading: "Loading saved versions",
210
+ historyLoadError: "Could not load Factory history.",
211
+ historyEmpty: "No saved versions yet",
212
+ historyEmptyHint:
213
+ "Save the visual graph once and Factory will keep each version here for review and rollback.",
214
+ historyVersion: "Version {{version}}",
215
+ historyCurrent: "Current",
216
+ historySource: "Source",
217
+ historySourceManual: "Visual editor",
218
+ historySourceAi: "Agent proposal",
219
+ historySourceSeed: "Factory seed",
220
+ historySourceRestore: "Rollback",
221
+ historySourceSaved: "Saved graph",
222
+ historyNodes: "Nodes",
223
+ historyConnections: "Connections",
224
+ historyChangeSummary: "Change summary",
225
+ historyNoSummary: "No change summary was recorded.",
226
+ historyCurrentHint: "This is the version currently used by the Factory.",
227
+ historyRestore: "Restore this version",
228
+ historyRestoreTitle: "Restore version {{version}}?",
229
+ historyRestoreDescription:
230
+ "Factory will create a new current version from this snapshot. Existing versions stay available in history.",
231
+ historyRestoreUnsaved:
232
+ "You also have unsaved visual changes. Restoring will discard them.",
233
+ historyCancel: "Cancel",
234
+ historyRestoreConfirm: "Restore version",
235
+ historyRestored: "Version restored. A new current version was created.",
236
+ historyRestoreFailed: "Could not restore this Factory version.",
237
+ historyRestoreConflict:
238
+ "Factory changed while restoring. Review the refreshed history before trying again.",
239
+ historyRefreshFailed:
240
+ "The version was restored, but the Factory view could not refresh.",
241
+ historyLoadOlder: "Load older versions",
242
+ historySnapshotLoadError: "Could not load this version preview.",
243
+ historyRestoreUnverified:
244
+ "Restore completed, but the result could not be verified. Refresh history before trying again.",
245
+ saveConflictFallback:
246
+ "Factory changed while saving. Refresh Factory and try again.",
207
247
  automationsTitle: "Automations",
208
248
  automationsDescription: "Schedule and run Factory prompts.",
209
249
  automationsLoading: "Loading automations...",
@@ -312,6 +352,9 @@ const messages = {
312
352
  addComment: "Add comment",
313
353
  savingGraph: "Saving graph...",
314
354
  saveGraph: "Save factory changes",
355
+ saveConflictHint:
356
+ "Your unsaved edits are preserved. Discard them to load the newer Factory version.",
357
+ discardLocalChanges: "Discard local edits and load current version",
315
358
  savedGraph: "Factory is saved",
316
359
  saveNote: "Saves a new version. Existing runs keep their original context.",
317
360
  factoryName: "Factory name",
@@ -31,6 +31,7 @@ import {
31
31
  type FactoryCanvasGraph,
32
32
  type FactoryCanvasNode,
33
33
  } from "@/components/factory/FactoryCanvas";
34
+ import { FactoryHistoryView } from "@/components/factory/FactoryHistoryView";
34
35
  import { FactoryInspector } from "@/components/factory/FactoryInspector";
35
36
  import { TriageStatusPill } from "@/components/triage/triage-status-pill";
36
37
  import { Button } from "@/components/ui/button";
@@ -131,6 +132,7 @@ type WorkspaceTab =
131
132
  | "automations"
132
133
  | "agents"
133
134
  | "audit"
135
+ | "history"
134
136
  | "settings";
135
137
 
136
138
  type FactoryAutomationRun = {
@@ -184,9 +186,14 @@ export default function FactoryRoute() {
184
186
  const [creating, setCreating] = useState(false);
185
187
  const [draftGraph, setDraftGraph] = useState<FactoryCanvasGraph | null>(null);
186
188
  const [dirty, setDirty] = useState(false);
189
+ const [saveError, setSaveError] = useState<string | null>(null);
190
+ const [saveConflictRemoteGraph, setSaveConflictRemoteGraph] =
191
+ useState<FactoryCanvasGraph | null>(null);
192
+ const [refreshingFactory, setRefreshingFactory] = useState(false);
187
193
  const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
188
194
  const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null);
189
195
  const [auditRefreshToken, setAuditRefreshToken] = useState(0);
196
+ const draftRevisionRef = useRef(0);
190
197
 
191
198
  function setActiveTab(tab: WorkspaceTab) {
192
199
  setSearchParams(
@@ -194,6 +201,10 @@ export default function FactoryRoute() {
194
201
  const next = new URLSearchParams(current);
195
202
  if (tab === "overview") next.delete("tab");
196
203
  else next.set("tab", tab);
204
+ if (tab === "history") {
205
+ next.delete("node");
206
+ next.delete("edge");
207
+ }
197
208
  return next;
198
209
  },
199
210
  { replace: true },
@@ -255,17 +266,17 @@ export default function FactoryRoute() {
255
266
  const graphData =
256
267
  rawGraphData?.factory.id === factoryId ? rawGraphData : undefined;
257
268
  const graph = draftGraph ?? graphData?.graph ?? null;
258
- const graphVersion = graphData?.factory.graphVersion ?? graph?.version ?? 1;
269
+ const graphVersion = graph?.version ?? graphData?.factory.graphVersion ?? 1;
259
270
  const saveGraphMutation = useActionMutation("save-factory-graph");
260
271
  const factoryList = (factoryListQuery.data ?? []) as FactorySummary[];
261
272
 
262
273
  useEffect(() => {
263
- if (!graphData || creating) return;
274
+ if (!graphData || creating || dirty) return;
264
275
  setDraftGraph(graphData.graph);
265
276
  setDirty(false);
266
277
  setSelectedNodeId(null);
267
278
  setSelectedEdgeId(null);
268
- }, [creating, graphData]);
279
+ }, [creating, dirty, graphData]);
269
280
 
270
281
  useEffect(() => {
271
282
  setSelectedNodeId(searchParams.get("node"));
@@ -313,6 +324,9 @@ export default function FactoryRoute() {
313
324
  }
314
325
 
315
326
  function updateGraph(next: FactoryCanvasGraph) {
327
+ draftRevisionRef.current += 1;
328
+ setSaveError(null);
329
+ setSaveConflictRemoteGraph(null);
316
330
  setDraftGraph(next);
317
331
  setDirty(
318
332
  !graphData?.graph ||
@@ -396,20 +410,87 @@ export default function FactoryRoute() {
396
410
 
397
411
  async function saveGraph() {
398
412
  if (!graph || !selectedFactoryId) return;
399
- await saveGraphMutation.mutateAsync({
400
- factoryId: selectedFactoryId,
401
- name: graph.name,
402
- description: graph.description,
403
- prompt: creating ? "" : (graphData?.factory.prompt ?? ""),
404
- source: "manual",
405
- changeSummary: creating
406
- ? "Created from the Factory visual editor."
407
- : "Updated in the Factory visual editor.",
408
- graph,
409
- });
413
+ const submittedDraftRevision = draftRevisionRef.current;
414
+ setSaveError(null);
415
+ try {
416
+ await saveGraphMutation.mutateAsync({
417
+ factoryId: selectedFactoryId,
418
+ name: graph.name,
419
+ description: graph.description,
420
+ prompt: creating ? "" : (graphData?.factory.prompt ?? ""),
421
+ source: "manual",
422
+ changeSummary: creating
423
+ ? "Created from the Factory visual editor."
424
+ : "Updated in the Factory visual editor.",
425
+ expectedGraphVersion: creating ? 0 : graph.version,
426
+ graph,
427
+ });
428
+ setCreating(false);
429
+ setDirty(draftRevisionRef.current !== submittedDraftRevision);
430
+ setSaveConflictRemoteGraph(null);
431
+ await Promise.all([graphQuery.refetch(), factoryListQuery.refetch()]);
432
+ } catch (error) {
433
+ setSaveError(
434
+ error instanceof Error
435
+ ? error.message
436
+ : t("factoryRoute.saveConflictFallback"),
437
+ );
438
+ }
439
+ }
440
+
441
+ async function refreshFactoryAfterSaveConflict() {
442
+ setRefreshingFactory(true);
443
+ try {
444
+ const results = await Promise.all([
445
+ graphQuery.refetch(),
446
+ factoryListQuery.refetch(),
447
+ ]);
448
+ if (results.some((result) => result.isError)) {
449
+ throw new Error("Factory refresh failed.");
450
+ }
451
+ const remoteGraph = (results[0].data as FactoryGraphResponse | undefined)
452
+ ?.graph;
453
+ if (dirty) {
454
+ if (!remoteGraph) throw new Error("Factory graph refresh failed.");
455
+ setSaveConflictRemoteGraph(remoteGraph);
456
+ setSaveError(t("factoryRoute.saveConflictFallback"));
457
+ return;
458
+ }
459
+ setSaveError(null);
460
+ } catch {
461
+ setSaveError(t("factoryRoute.saveConflictFallback"));
462
+ } finally {
463
+ setRefreshingFactory(false);
464
+ }
465
+ }
466
+
467
+ function discardLocalFactoryChanges() {
468
+ if (!saveConflictRemoteGraph) return;
469
+ setDraftGraph(saveConflictRemoteGraph);
470
+ setSaveConflictRemoteGraph(null);
471
+ setSaveError(null);
472
+ setDirty(false);
473
+ setCreating(false);
474
+ setSelectedNodeId(null);
475
+ setSelectedEdgeId(null);
476
+ }
477
+
478
+ async function handleFactoryRestored(result?: { graph: FactoryCanvasGraph }) {
410
479
  setCreating(false);
480
+ if (result?.graph) setDraftGraph(result.graph);
481
+ else setDraftGraph(null);
411
482
  setDirty(false);
412
- await Promise.all([graphQuery.refetch(), factoryListQuery.refetch()]);
483
+ setSaveError(null);
484
+ setSaveConflictRemoteGraph(null);
485
+ setSelectedNodeId(null);
486
+ setSelectedEdgeId(null);
487
+ const results = await Promise.all([
488
+ graphQuery.refetch(),
489
+ factoryListQuery.refetch(),
490
+ ]);
491
+ if (results.some((result) => result.isError)) {
492
+ throw new Error("Factory view refresh failed.");
493
+ }
413
494
  }
414
495
 
415
496
  if (!selectedFactoryId && activeTab === "agents") {
@@ -659,6 +740,12 @@ export default function FactoryRoute() {
659
740
  >
660
741
  Activity
661
742
  </TabButton>
743
+ <TabButton
744
+ active={activeTab === "history"}
745
+ onClick={() => setActiveTab("history")}
746
+ >
747
+ {t("factoryRoute.historyTab")}
748
+ </TabButton>
662
749
  <TabButton
663
750
  active={activeTab === "settings"}
664
751
  onClick={() => setActiveTab("settings")}
@@ -727,8 +814,13 @@ export default function FactoryRoute() {
727
814
  factoryId={factoryId}
728
815
  dirty={dirty}
729
816
  saving={saveGraphMutation.isPending}
817
+ saveError={saveError}
818
+ saveConflictNeedsResolution={Boolean(saveConflictRemoteGraph)}
819
+ refreshing={refreshingFactory}
730
820
  onGraphChange={updateGraph}
731
821
  onSave={() => void saveGraph()}
822
+ onRefresh={refreshFactoryAfterSaveConflict}
823
+ onDiscardLocalChanges={discardLocalFactoryChanges}
732
824
  onAddNode={addNode}
733
825
  onDeleteNode={deleteNode}
734
826
  onConnect={connectNodes}
@@ -747,6 +839,13 @@ export default function FactoryRoute() {
747
839
  factoryId={factoryId}
748
840
  refreshToken={auditRefreshToken}
749
841
  />
842
+ ) : activeTab === "history" ? (
843
+ <FactoryHistoryView
844
+ key={factoryId}
845
+ factoryId={factoryId}
846
+ hasUnsavedChanges={dirty}
847
+ onRestored={handleFactoryRestored}
848
+ />
750
849
  ) : (
751
850
  <SettingsView t={t} />
752
851
  )}
@@ -783,6 +882,7 @@ function parseWorkspaceTab(value: string | null): WorkspaceTab {
783
882
  value === "automations" ||
784
883
  value === "agents" ||
785
884
  value === "audit" ||
885
+ value === "history" ||
786
886
  value === "settings"
787
887
  ? value
788
888
  : "overview";
@@ -57,7 +57,9 @@ current factory graph, and executes only the explicit automation prompts that
57
57
  are stored in the organization.
58
58
  Use the Factory actions as the source of truth. When a user asks to
59
59
  create or change a factory, first inspect the current graph, then propose a complete
60
- versioned graph through save-factory-graph with source=ai and a concise changeSummary.
60
+ versioned graph through save-factory-graph with source=ai, the inspected graphVersion
61
+ as expectedGraphVersion, and a concise changeSummary. Never save a graph from a
62
+ stale read: refresh and re-propose when the action reports a version conflict.
61
63
  Never hide a graph change in prose: the visual map and the saved graph must agree.
62
64
  The graph is currently a reviewable blueprint, not the runtime router: automation
63
65
  markdown resources are the runtime prompts, while enabled triage rules are evaluated
@@ -0,0 +1,4 @@
1
+ import { z } from "zod";
2
+ export declare const authConfig: z.ZodObject<{
3
+ disableDesktopSsoFallbackInDevelopment: z.ZodDefault<z.ZodBoolean>;
4
+ }, z.core.$strip>;
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+ export const authConfig = z.object({
3
+ disableDesktopSsoFallbackInDevelopment: z.boolean().default(false).meta({
4
+ env: "AGENT_NATIVE_DISABLE_DESKTOP_SSO_FALLBACK",
5
+ doc: "Disable the loopback Desktop SSO fallback in development so isolated acceptance runs can use their configured local identity. Ignored in production.",
6
+ }),
7
+ });
@@ -32,6 +32,9 @@ export declare const appConfigSchema: z.ZodObject<{
32
32
  packageName: z.ZodOptional<z.ZodString>;
33
33
  template: z.ZodOptional<z.ZodString>;
34
34
  }, z.core.$strip>>;
35
+ auth: z.ZodPrefault<z.ZodObject<{
36
+ disableDesktopSsoFallbackInDevelopment: z.ZodDefault<z.ZodBoolean>;
37
+ }, z.core.$strip>>;
35
38
  integrations: z.ZodPrefault<z.ZodObject<{
36
39
  allowUnverifiedWebhooks: z.ZodDefault<z.ZodBoolean>;
37
40
  }, z.core.$strip>>;
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { a2aConfig } from "./a2a.js";
3
3
  import { agentConfig } from "./agent.js";
4
4
  import { appConfig } from "./app.js";
5
+ import { authConfig } from "./auth.js";
5
6
  import { integrationsConfig } from "./integrations.js";
6
7
  import { privateBlobConfig } from "./private-blob.js";
7
8
  import { workspaceConfig } from "./workspace.js";
@@ -25,6 +26,7 @@ export const appConfigSchema = z.object({
25
26
  a2a: a2aConfig.prefault({}),
26
27
  agent: agentConfig.prefault({}),
27
28
  app: appConfig.prefault({}),
29
+ auth: authConfig.prefault({}),
28
30
  integrations: integrationsConfig.prefault({}),
29
31
  privateBlob: privateBlobConfig.prefault({}),
30
32
  workspace: workspaceConfig.prefault({}),
@@ -1226,8 +1226,13 @@ async function executeCodexCliRun(options) {
1226
1226
  const streamToolOutputToStdout = options.streamToolOutputToStdout ??
1227
1227
  process.env.AGENT_NATIVE_CODE_AGENT_STRUCTURED_STDOUT !== "1";
1228
1228
  const additionalSkillsRoot = process.env.AGENT_NATIVE_CODE_AGENT_SKILLS_ROOT?.trim();
1229
+ // Desktop has already resolved and capability-scoped its MCP catalog before
1230
+ // spawning the packaged runner. Reading persisted settings again here would
1231
+ // both widen that boundary and require the app's native database binding in
1232
+ // the standalone runner process.
1233
+ const mcpConfig = process.env.MCP_SERVERS === undefined ? await buildMergedConfig() : null;
1229
1234
  const args = [
1230
- ...codexMcpConfigArgs(),
1235
+ ...codexMcpConfigArgs(mcpConfig),
1231
1236
  "--ask-for-approval",
1232
1237
  "never",
1233
1238
  "--sandbox",
@@ -1,4 +1,4 @@
1
- import type { McpConfig } from "../mcp-client/config.js";
1
+ import { type McpConfig } from "../mcp-client/config.js";
2
2
  /**
3
3
  * Keep a desktop coding run on the app registry's MCP servers. The normal
4
4
  * server-side manager merges persisted MCP settings for web requests, but a
@@ -18,4 +18,4 @@ export declare function mergeCodeAgentMcpConfig(config: McpConfig | null, enviro
18
18
  * coding session receives the workspace apps, not the user's global MCP
19
19
  * catalog.
20
20
  */
21
- export declare function codexMcpConfigArgs(environment?: NodeJS.ProcessEnv): string[];
21
+ export declare function codexMcpConfigArgs(config?: McpConfig | null, environment?: NodeJS.ProcessEnv): string[];
@@ -1,3 +1,4 @@
1
+ import { resolveDesktopChildComputerMcpServer, } from "../mcp-client/config.js";
1
2
  const DESKTOP_MCP_ALLOWLIST_ENV = "AGENT_NATIVE_CODE_AGENT_MCP_SERVER_ALLOWLIST";
2
3
  function parseServerAllowlist(raw) {
3
4
  if (raw === undefined)
@@ -17,6 +18,9 @@ export function restrictCodeAgentMcpConfig(config, environment = process.env) {
17
18
  const allowlist = parseServerAllowlist(environment[DESKTOP_MCP_ALLOWLIST_ENV]);
18
19
  if (!allowlist || !config)
19
20
  return config;
21
+ const desktopServer = findDesktopChildComputerMcpServer(config, environment);
22
+ if (desktopServer)
23
+ allowlist.add(desktopServer.id);
20
24
  const servers = Object.fromEntries(Object.entries(config.servers).filter(([id]) => allowlist.has(id)));
21
25
  return { ...config, servers };
22
26
  }
@@ -59,6 +63,18 @@ function parseMcpServers(environment) {
59
63
  return [id, value];
60
64
  }));
61
65
  }
66
+ function desktopChildComputerServerConfig(environment) {
67
+ return resolveDesktopChildComputerMcpServer({}, environment)?.config ?? null;
68
+ }
69
+ function findDesktopChildComputerMcpServer(config, environment) {
70
+ const expected = desktopChildComputerServerConfig(environment);
71
+ if (!expected || expected.type !== "http")
72
+ return null;
73
+ const match = Object.entries(config.servers).find(([, server]) => server.type === "http" &&
74
+ server.url === expected.url &&
75
+ server.headers?.Authorization === expected.headers?.Authorization);
76
+ return match ? { id: match[0], config: match[1] } : null;
77
+ }
62
78
  /**
63
79
  * Add host-provided app/plugin servers to the normal merged config. The
64
80
  * filesystem and environment layers have different precedence rules, so the
@@ -66,14 +82,23 @@ function parseMcpServers(environment) {
66
82
  */
67
83
  export function mergeCodeAgentMcpConfig(config, environment = process.env) {
68
84
  const environmentServers = parseMcpServers(environment);
69
- if (Object.keys(environmentServers).length === 0)
85
+ const servers = {
86
+ ...(config?.servers ?? {}),
87
+ ...environmentServers,
88
+ };
89
+ const existingDesktopServer = config
90
+ ? findDesktopChildComputerMcpServer(config, environment)
91
+ : null;
92
+ const desktopServer = existingDesktopServer
93
+ ? null
94
+ : resolveDesktopChildComputerMcpServer(servers, environment);
95
+ if (desktopServer)
96
+ servers[desktopServer.id] = desktopServer.config;
97
+ if (Object.keys(servers).length === 0)
70
98
  return config;
71
99
  return {
72
100
  ...(config ?? { servers: {} }),
73
- servers: {
74
- ...(config?.servers ?? {}),
75
- ...environmentServers,
76
- },
101
+ servers,
77
102
  source: config?.source
78
103
  ? `${config.source}+desktop-environment`
79
104
  : "env:MCP_SERVERS",
@@ -85,8 +110,8 @@ export function mergeCodeAgentMcpConfig(config, environment = process.env) {
85
110
  * coding session receives the workspace apps, not the user's global MCP
86
111
  * catalog.
87
112
  */
88
- export function codexMcpConfigArgs(environment = process.env) {
89
- const servers = parseMcpServers(environment);
113
+ export function codexMcpConfigArgs(config = null, environment = process.env) {
114
+ const servers = restrictCodeAgentMcpConfig(mergeCodeAgentMcpConfig(config, environment), environment)?.servers ?? {};
90
115
  if (Object.keys(servers).length === 0)
91
116
  return [];
92
117
  const args = [];
@@ -94,9 +119,9 @@ export function codexMcpConfigArgs(environment = process.env) {
94
119
  if (server.type !== "http" || !server.url)
95
120
  continue;
96
121
  const key = codexConfigKey(serverId);
97
- args.push("-c", `mcp_servers.${tomlString(key)}.url=${tomlString(server.url)}`);
122
+ args.push("-c", `mcp_servers.${key}.url=${tomlString(server.url)}`);
98
123
  if (server.headers && Object.keys(server.headers).length > 0) {
99
- args.push("-c", `mcp_servers.${tomlString(key)}.http_headers=${tomlInlineTable(server.headers)}`);
124
+ args.push("-c", `mcp_servers.${key}.http_headers=${tomlInlineTable(server.headers)}`);
100
125
  }
101
126
  }
102
127
  return args;
@@ -7,6 +7,7 @@ import { setBrowserDemoModeEnabled } from "../../demo/browser-state.js";
7
7
  import { shouldOfferWorkspace } from "../../org/workspace-url.js";
8
8
  import { agentNativePath } from "../api-path.js";
9
9
  import { useT } from "../i18n.js";
10
+ import { buildSignInReturnHref } from "../require-session.js";
10
11
  import { useDemoModeStatus } from "../use-demo-mode-status.js";
11
12
  import { notifySessionInvalidated, useSession } from "../use-session.js";
12
13
  import { useOrg, useSwitchOrg, useCreateOrg, useInviteMember, useAcceptInvitation, useJoinByDomain, } from "./hooks.js";
@@ -142,16 +143,19 @@ export function OrgSwitcher({ className, hideWhenSingle, reserveSpace, compact,
142
143
  return;
143
144
  setSigningOut(true);
144
145
  try {
145
- await fetch(agentNativePath("/_agent-native/auth/logout"), {
146
+ const response = await fetch(agentNativePath("/_agent-native/auth/logout"), {
146
147
  method: "POST",
147
148
  credentials: "include",
148
149
  });
150
+ if (!response.ok) {
151
+ console.warn("Logout request returned an error before sign-in", response.status);
152
+ }
149
153
  }
150
- catch {
151
- /* fall through to reload — server may already have cleared the cookie */
154
+ catch (error) {
155
+ console.warn("Unable to complete logout request before sign-in", error);
152
156
  }
153
157
  notifySessionInvalidated();
154
- window.location.reload();
158
+ window.location.replace(buildSignInReturnHref());
155
159
  };
156
160
  if (!org) {
157
161
  return reserveSpace && isLoading ? (_jsx(OrgSwitcherLoadingPlaceholder, { className: className })) : null;
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
62
62
  error: string;
63
63
  states?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  states: {
66
67
  clientId: number;
67
68
  state: string;
68
69
  }[];
69
- error?: undefined;
70
70
  }>>;
71
71
  /**
72
72
  * GET /_agent-native/collab/:docId/users
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
77
77
  error: string;
78
78
  users?: undefined;
79
79
  } | {
80
+ error?: undefined;
80
81
  users: {
81
82
  clientId: number;
82
83
  lastSeen: number;
83
84
  }[];
84
- error?: undefined;
85
85
  }>>;
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- error: string;
30
29
  ok?: undefined;
30
+ error: string;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- ok?: undefined;
17
16
  error: string;
17
+ ok?: undefined;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -60,12 +60,23 @@ export interface McpConfig {
60
60
  /** Where the config was loaded from (workspace root path, app path, or "env") */
61
61
  source?: string;
62
62
  }
63
+ export interface DesktopChildComputerMcpServer {
64
+ id: string;
65
+ config: McpHttpServerConfig;
66
+ }
63
67
  /**
64
68
  * Load MCP configuration.
65
69
  *
66
70
  * @param startDir - Directory to start the upward search from (defaults to cwd)
67
71
  */
68
72
  export declare function loadMcpConfig(startDir?: string): McpConfig | null;
73
+ /**
74
+ * The desktop process supplies this configuration only to a child it spawned
75
+ * for a specific Agent run. Raw MCP config cannot opt into this trust path:
76
+ * the explicit child gate, loopback URL, and strong per-run bearer are all
77
+ * required. Existing user servers always win on key collisions.
78
+ */
79
+ export declare function resolveDesktopChildComputerMcpServer(existingServers: Record<string, McpServerConfig>, environment?: NodeJS.ProcessEnv): DesktopChildComputerMcpServer | null;
69
80
  /**
70
81
  * Auto-detect the claude-in-chrome MCP server if it's installed but no
71
82
  * config file exists. Gated by `AGENT_NATIVE_DISABLE_MCP_AUTODETECT`.