@taskforcehq/taskforce 0.3.314 → 0.3.316

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 (36) hide show
  1. package/dist/components/features/AnnotatedAttachmentWorkspace.js +51 -11
  2. package/dist/core/PlanEntitlementService.js +6 -1
  3. package/dist/core/Taskforce.d.ts +2 -0
  4. package/dist/core/Taskforce.js +16 -2
  5. package/dist/hooks/useTaskforce.js +13 -6
  6. package/dist/mcp/canonicalAssetHelpers.js +33 -19
  7. package/dist/mcp/runtime.js +39 -9
  8. package/dist/mcp/taskAttachmentHelpers.js +29 -7
  9. package/dist/mcp/toolCatalog.d.ts +7 -0
  10. package/dist/server/index.js +1 -1
  11. package/dist/server/routes/documents.js +2 -1
  12. package/dist/server/routes.js +10 -6
  13. package/dist/storage/documentIntegrity.js +1 -6
  14. package/dist/storage/documentPurge.js +11 -12
  15. package/dist/sync/workspaceRepair.js +11 -9
  16. package/dist/ui/.well-known/mcp-registry-auth +1 -0
  17. package/dist/ui/assets/{AgentsModule-jZnjzh-I.js → AgentsModule-CNBWCIXk.js} +1 -1
  18. package/dist/ui/assets/AnnotatedAttachmentWorkspace-9qxB0e6A.js +3 -0
  19. package/dist/ui/assets/{ContextAttachmentManager-OnjnW5fC.js → ContextAttachmentManager-CRyuFYlg.js} +1 -1
  20. package/dist/ui/assets/{DocumentWorkspace-Btxo9quS.js → DocumentWorkspace-Dx7wb9NF.js} +1 -1
  21. package/dist/ui/assets/{EntityActivityTimeline-C8BjU7yO.js → EntityActivityTimeline-Cmal2OrJ.js} +1 -1
  22. package/dist/ui/assets/{InitiativesModule-DYBB7WD-.js → InitiativesModule-wzbYPQQL.js} +1 -1
  23. package/dist/ui/assets/{PlansPage-p0gvF4qR.js → PlansPage-BAnSfbTf.js} +1 -1
  24. package/dist/ui/assets/{TaskContextUpload-EGeOdrPm.js → TaskContextUpload-DENyMyUk.js} +1 -1
  25. package/dist/ui/assets/{TaskSettings-CJpF1CMF.js → TaskSettings-DASuVwpY.js} +1 -1
  26. package/dist/ui/assets/{WorkflowsModule-IQOmxPXX.js → WorkflowsModule-DJMEA_yt.js} +1 -1
  27. package/dist/ui/assets/documentReferences-BhNx80zO.js +1 -0
  28. package/dist/ui/assets/{index-DUt7ifSO.js → index-CWg2olz9.js} +5 -5
  29. package/dist/ui/index.html +1 -1
  30. package/dist/utils/pathContainment.d.ts +7 -0
  31. package/dist/utils/pathContainment.js +53 -0
  32. package/dist/utils/pathSafety.d.ts +6 -0
  33. package/dist/utils/pathSafety.js +73 -0
  34. package/package.json +3 -1
  35. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CGFrFF0m.js +0 -3
  36. package/dist/ui/assets/documentReferences-DXW5aT08.js +0 -1
@@ -118,6 +118,14 @@ function isEditableKeyboardTarget(target) {
118
118
  return true;
119
119
  return tagName === 'input' || tagName === 'textarea' || tagName === 'select';
120
120
  }
121
+ function isInteractiveKeyboardTarget(target) {
122
+ if (!(target instanceof HTMLElement))
123
+ return false;
124
+ if (isEditableKeyboardTarget(target))
125
+ return true;
126
+ const tagName = target.tagName.toLowerCase();
127
+ return tagName === 'button' || tagName === 'a' || target.getAttribute('role') === 'button';
128
+ }
121
129
  function normalizeOrder(annotations) {
122
130
  return annotations.map((annotation, index) => ({
123
131
  ...annotation,
@@ -438,6 +446,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
438
446
  const [imageLoadError, setImageLoadError] = useState(false);
439
447
  const [zoomLevel, setZoomLevel] = useState(1);
440
448
  const [panMode, setPanMode] = useState(false);
449
+ const [spacePanActive, setSpacePanActive] = useState(false);
441
450
  const [pastingImage, setPastingImage] = useState(false);
442
451
  const [naturalImageSize, setNaturalImageSize] = useState({ width: 0, height: 0 });
443
452
  const overlayRef = useRef(null);
@@ -542,6 +551,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
542
551
  || canvasMediaSize.height > (scroller.clientHeight + 1)));
543
552
  }, [canvasMediaSize.height, canvasMediaSize.width, zoomLevel]);
544
553
  const zoomLabel = `${Math.round(zoomLevel * 100)}%`;
554
+ const isPanActive = canPan && (panMode || spacePanActive);
545
555
  const annotationOrderNumbers = useMemo(() => {
546
556
  const orderMap = new Map();
547
557
  draftAnnotations.forEach((annotation, index) => {
@@ -1232,6 +1242,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1232
1242
  setNaturalImageSize({ width: 0, height: 0 });
1233
1243
  setZoomLevel(1);
1234
1244
  setPanMode(false);
1245
+ setSpacePanActive(false);
1235
1246
  }, [activeTarget?.path]);
1236
1247
  useEffect(() => {
1237
1248
  if (!imageLoading)
@@ -1250,6 +1261,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1250
1261
  if (canPan)
1251
1262
  return;
1252
1263
  setPanMode(false);
1264
+ setSpacePanActive(false);
1253
1265
  }, [canPan]);
1254
1266
  useEffect(() => {
1255
1267
  setPayloadPreview(null);
@@ -1657,6 +1669,34 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1657
1669
  window.addEventListener('keydown', handleKeyDown);
1658
1670
  return () => window.removeEventListener('keydown', handleKeyDown);
1659
1671
  }, [deleteSelectedAnnotation, selectedAnnotationId]);
1672
+ useEffect(() => {
1673
+ const handleKeyDown = (event) => {
1674
+ if (event.code !== 'Space')
1675
+ return;
1676
+ if (!canPan)
1677
+ return;
1678
+ if (isInteractiveKeyboardTarget(event.target))
1679
+ return;
1680
+ event.preventDefault();
1681
+ setSpacePanActive(true);
1682
+ };
1683
+ const handleKeyUp = (event) => {
1684
+ if (event.code !== 'Space')
1685
+ return;
1686
+ setSpacePanActive(false);
1687
+ };
1688
+ const handleWindowBlur = () => {
1689
+ setSpacePanActive(false);
1690
+ };
1691
+ window.addEventListener('keydown', handleKeyDown);
1692
+ window.addEventListener('keyup', handleKeyUp);
1693
+ window.addEventListener('blur', handleWindowBlur);
1694
+ return () => {
1695
+ window.removeEventListener('keydown', handleKeyDown);
1696
+ window.removeEventListener('keyup', handleKeyUp);
1697
+ window.removeEventListener('blur', handleWindowBlur);
1698
+ };
1699
+ }, [canPan]);
1660
1700
  const readPoint = useCallback((event) => {
1661
1701
  const rect = overlayRef.current?.getBoundingClientRect();
1662
1702
  if (!rect || rect.width <= 0 || rect.height <= 0)
@@ -1667,7 +1707,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1667
1707
  };
1668
1708
  }, []);
1669
1709
  const handleCanvasPointerDown = useCallback((event) => {
1670
- if (panMode && canPan) {
1710
+ if (isPanActive) {
1671
1711
  const scroller = canvasScrollerRef.current;
1672
1712
  if (!scroller)
1673
1713
  return;
@@ -1709,7 +1749,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1709
1749
  setDraftAnnotations((current) => normalizeOrder([...current, annotation]));
1710
1750
  setSelectedAnnotationId(annotation.id);
1711
1751
  markDraftChanged(300);
1712
- }, [canPan, defaultMarkerColor, markDraftChanged, panMode, readPoint, selectedSessionId, toolMode]);
1752
+ }, [defaultMarkerColor, isPanActive, markDraftChanged, readPoint, selectedSessionId, toolMode]);
1713
1753
  const handleCanvasPointerUp = useCallback((event) => {
1714
1754
  if (panRef.current?.pointerId === event.pointerId) {
1715
1755
  event.currentTarget.releasePointerCapture?.(event.pointerId);
@@ -1726,7 +1766,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1726
1766
  setToolMode('select');
1727
1767
  }, [readPoint, toolMode]);
1728
1768
  const startDragAnnotation = useCallback((event, annotation) => {
1729
- if (toolMode !== 'select' || panMode)
1769
+ if (toolMode !== 'select' || isPanActive)
1730
1770
  return;
1731
1771
  const point = readPoint(event);
1732
1772
  if (!point)
@@ -1738,7 +1778,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1738
1778
  };
1739
1779
  setSelectedAnnotationId(annotation.id);
1740
1780
  event.stopPropagation();
1741
- }, [panMode, readPoint, toolMode]);
1781
+ }, [isPanActive, readPoint, toolMode]);
1742
1782
  const handleCanvasPointerMove = useCallback((event) => {
1743
1783
  if (panRef.current?.pointerId === event.pointerId) {
1744
1784
  const scroller = canvasScrollerRef.current;
@@ -1872,7 +1912,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1872
1912
  panRef.current = null;
1873
1913
  }, []);
1874
1914
  const startResizeBox = useCallback((event, annotation) => {
1875
- if (toolMode !== 'select' || panMode)
1915
+ if (toolMode !== 'select' || isPanActive)
1876
1916
  return;
1877
1917
  const point = readPoint(event);
1878
1918
  if (!point)
@@ -1884,9 +1924,9 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1884
1924
  };
1885
1925
  setSelectedAnnotationId(annotation.id);
1886
1926
  event.stopPropagation();
1887
- }, [panMode, readPoint, toolMode]);
1927
+ }, [isPanActive, readPoint, toolMode]);
1888
1928
  const startArrowHandleDrag = useCallback((event, annotation, endpoint) => {
1889
- if (toolMode !== 'select' || panMode)
1929
+ if (toolMode !== 'select' || isPanActive)
1890
1930
  return;
1891
1931
  const point = readPoint(event);
1892
1932
  if (!point)
@@ -1899,7 +1939,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1899
1939
  };
1900
1940
  setSelectedAnnotationId(annotation.id);
1901
1941
  event.stopPropagation();
1902
- }, [panMode, readPoint, toolMode]);
1942
+ }, [isPanActive, readPoint, toolMode]);
1903
1943
  const activeSessionTimestamp = selectedSession?.updatedAt ? formatTimestamp(selectedSession.updatedAt) : 'Not saved yet';
1904
1944
  return (_jsxs("div", { className: `${styles.shell} ${showImageTray ? styles.shellWithImageTray : ''} ${showImageTray && imageTrayOpen ? styles.shellImageTrayOpen : ''}`.trim(), children: [showImageTray ? (_jsxs("aside", { className: `${styles.imageTrayPanel} ${imageTrayOpen ? styles.imageTrayPanelOpen : ''}`.trim(), "aria-label": "Image tray", "aria-hidden": !imageTrayOpen, children: [_jsxs("div", { className: styles.imageTrayHeader, children: [_jsxs("span", { className: styles.imageTrayTitle, children: [_jsx(FolderOpen, { size: 14 }), "Images"] }), _jsx("button", { type: "button", className: "tf-control-icon", onClick: onCloseImageTray, title: "Collapse image tray", "aria-label": "Collapse image tray", children: _jsx(ChevronLeft, { size: 16 }) })] }), _jsxs("div", { className: styles.imageTraySearch, children: [_jsx(Search, { size: 12, className: styles.imageTraySearchIcon }), _jsx("input", { type: "text", placeholder: "Search images...", value: imageTraySearchQuery, onChange: (event) => setImageTraySearchQuery(event.target.value), className: styles.imageTraySearchInput })] }), _jsx("div", { className: `tf-scrollbar ${styles.imageTrayList}`, children: imageTrayLoading ? (_jsx("div", { className: styles.imageTrayState, children: "Loading images..." })) : imageTrayError ? (_jsx("div", { className: styles.imageTrayStateError, children: imageTrayError })) : filteredImageTrayItems.length === 0 ? (_jsx("div", { className: styles.imageTrayState, children: imageTraySearchQuery.trim() ? 'No images match your search.' : 'No images found.' })) : filteredImageTrayItems.map((item) => {
1905
1945
  const isSelected = activeTarget?.assetId === item.assetId;
@@ -1957,7 +1997,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1957
1997
  if (!selectedSession)
1958
1998
  return;
1959
1999
  restoreLastSavedDraft();
1960
- }, disabled: !selectedSession || !hasUnsavedChanges, "aria-label": "Reset unsaved changes", title: "Reset unsaved changes", children: _jsx(RotateCcw, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomOut, disabled: !activeTarget || imageLoading || zoomLevel <= 0.25, "aria-label": "Zoom out", children: _jsx(ZoomOut, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: resetZoom, disabled: !activeTarget || imageLoading || zoomLevel === 1, "aria-label": `Reset zoom to 100 percent (currently ${zoomLabel})`, title: `Reset zoom to 100% (currently ${zoomLabel})`, children: _jsx("span", { children: zoomLabel }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomIn, disabled: !activeTarget || imageLoading || zoomLevel >= 4, "aria-label": "Zoom in", children: _jsx(ZoomIn, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.toolBtn} ${styles.toolbarButton} ${panMode ? styles.toolBtnActive : ''}`, onClick: () => setPanMode((current) => !current), disabled: !activeTarget || imageLoading || !canPan, "aria-label": "Pan canvas", title: "Pan canvas", children: _jsx(Hand, { size: 18 }) })] }), _jsx("span", { className: styles.toolbarSeparator, "aria-hidden": "true" }), _jsxs("div", { className: styles.toolbarActions, children: [_jsx("div", { className: styles.toolbarSelectionActions, children: selectedAnnotation ? (_jsxs(_Fragment, { children: [_jsxs("div", { ref: colorPickerRef, className: styles.toolbarColorPicker, children: [_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton} ${styles.colorPickerButton}`, onClick: () => setColorPickerOpen((current) => !current), "aria-label": "Marker color", "aria-expanded": colorPickerOpen, title: "Marker color", children: _jsx("span", { className: styles.colorPickerSwatch, style: { backgroundColor: selectedMarkerColor }, "aria-hidden": "true" }) }), colorPickerOpen ? (_jsx("div", { className: styles.colorPickerPopover, role: "menu", "aria-label": "Marker color options", children: MARKER_COLOR_SWATCHES.map((color) => (_jsx("button", { type: "button", className: `${styles.colorOption} ${selectedMarkerColor === color ? styles.colorOptionActive : ''}`, style: { backgroundColor: color }, onClick: () => applyMarkerColor(color), "aria-label": `Use marker color ${color}`, "aria-pressed": selectedMarkerColor === color }, color))) })) : null] }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: deleteSelectedAnnotation, "aria-label": "Delete marker", title: "Delete selected marker", children: _jsx(Trash2, { size: 18 }) }), _jsx("div", { className: styles.toolbarGeometryFields, "aria-label": "Marker geometry percent controls", children: getGeometryFields(selectedAnnotation).map((field) => (_jsxs("label", { className: styles.toolbarGeometryField, children: [_jsx("span", { className: `tf-text-meta ${styles.toolbarGeometryLabel}`, children: field.label }), _jsx("input", { className: `tf-field-shell ${styles.toolbarGeometryInput}`, type: "number", min: 0, max: 100, step: 0.1, value: field.value, onChange: (event) => {
2000
+ }, disabled: !selectedSession || !hasUnsavedChanges, "aria-label": "Reset unsaved changes", title: "Reset unsaved changes", children: _jsx(RotateCcw, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomOut, disabled: !activeTarget || imageLoading || zoomLevel <= 0.25, "aria-label": "Zoom out", children: _jsx(ZoomOut, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: resetZoom, disabled: !activeTarget || imageLoading || zoomLevel === 1, "aria-label": `Reset zoom to 100 percent (currently ${zoomLabel})`, title: `Reset zoom to 100% (currently ${zoomLabel})`, children: _jsx("span", { children: zoomLabel }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomIn, disabled: !activeTarget || imageLoading || zoomLevel >= 4, "aria-label": "Zoom in", children: _jsx(ZoomIn, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.toolBtn} ${styles.toolbarButton} ${isPanActive ? styles.toolBtnActive : ''}`, onClick: () => setPanMode((current) => !current), disabled: !activeTarget || imageLoading || !canPan, "aria-label": "Pan canvas", title: "Pan canvas", children: _jsx(Hand, { size: 18 }) })] }), _jsx("span", { className: styles.toolbarSeparator, "aria-hidden": "true" }), _jsxs("div", { className: styles.toolbarActions, children: [_jsx("div", { className: styles.toolbarSelectionActions, children: selectedAnnotation ? (_jsxs(_Fragment, { children: [_jsxs("div", { ref: colorPickerRef, className: styles.toolbarColorPicker, children: [_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton} ${styles.colorPickerButton}`, onClick: () => setColorPickerOpen((current) => !current), "aria-label": "Marker color", "aria-expanded": colorPickerOpen, title: "Marker color", children: _jsx("span", { className: styles.colorPickerSwatch, style: { backgroundColor: selectedMarkerColor }, "aria-hidden": "true" }) }), colorPickerOpen ? (_jsx("div", { className: styles.colorPickerPopover, role: "menu", "aria-label": "Marker color options", children: MARKER_COLOR_SWATCHES.map((color) => (_jsx("button", { type: "button", className: `${styles.colorOption} ${selectedMarkerColor === color ? styles.colorOptionActive : ''}`, style: { backgroundColor: color }, onClick: () => applyMarkerColor(color), "aria-label": `Use marker color ${color}`, "aria-pressed": selectedMarkerColor === color }, color))) })) : null] }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: deleteSelectedAnnotation, "aria-label": "Delete marker", title: "Delete selected marker", children: _jsx(Trash2, { size: 18 }) }), _jsx("div", { className: styles.toolbarGeometryFields, "aria-label": "Marker geometry percent controls", children: getGeometryFields(selectedAnnotation).map((field) => (_jsxs("label", { className: styles.toolbarGeometryField, children: [_jsx("span", { className: `tf-text-meta ${styles.toolbarGeometryLabel}`, children: field.label }), _jsx("input", { className: `tf-field-shell ${styles.toolbarGeometryInput}`, type: "number", min: 0, max: 100, step: 0.1, value: field.value, onChange: (event) => {
1961
2001
  const nextValue = parsePercentField(event.target.value);
1962
2002
  if (nextValue === null)
1963
2003
  return;
@@ -1980,7 +2020,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
1980
2020
  setNaturalImageSize({ width: 0, height: 0 });
1981
2021
  setImageLoading(false);
1982
2022
  setImageLoadError(true);
1983
- } }), !imageLoading && !imageLoadError && selectedSession ? (_jsxs("div", { ref: overlayRef, className: `${styles.overlay} ${toolMode === 'select' ? styles.overlaySelect : ''} ${panMode && canPan ? styles.overlayPan : ''}`, onPointerDown: handleCanvasPointerDown, onPointerMove: handleCanvasPointerMove, onPointerUp: (event) => {
2023
+ } }), !imageLoading && !imageLoadError && selectedSession ? (_jsxs("div", { ref: overlayRef, className: `${styles.overlay} ${toolMode === 'select' ? styles.overlaySelect : ''} ${isPanActive ? styles.overlayPan : ''}`, onPointerDown: handleCanvasPointerDown, onPointerMove: handleCanvasPointerMove, onPointerUp: (event) => {
1984
2024
  handleCanvasPointerUp(event);
1985
2025
  handleCanvasPointerEnd();
1986
2026
  }, "data-testid": "annotated-attachment-overlay", onPointerLeave: handleCanvasPointerLeave, onPointerCancel: handleCanvasPointerCancel, children: [_jsx("svg", { className: styles.overlaySvg, viewBox: arrowOverlayViewBox, preserveAspectRatio: "none", "aria-hidden": "true", children: draftAnnotations.filter((annotation) => annotation.kind === 'arrow').map((annotation) => {
@@ -2022,7 +2062,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
2022
2062
  ? 'Save failed. Retry now.'
2023
2063
  : saveState === 'saving' || saveState === 'pending'
2024
2064
  ? 'Saving…'
2025
- : 'Saved' }), error ? _jsx("span", { children: error }) : _jsx("span", { children: !selectedSession ? 'Create a session to begin placing markers on this image.' : panMode && canPan ? 'Drag on the image to pan.' : toolMode === 'select' ? 'Select a marker to edit it.' : `Click on the image to place a ${toolMode}.` }), activeTarget && !selectedSession && showCreateSessionPrompt ? (_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => void createSession(), disabled: saving || loading, children: saving ? 'Creating…' : 'Create one now' })) : null] })] }), _jsx("section", { className: `tf-surface-panel ${styles.panel} ${styles.detailPanel}`, children: selectedSession ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.panelHeader, children: [_jsxs("div", { className: styles.panelHeaderText, children: [_jsx("div", { className: `tf-heading-card ${styles.panelTitle}`, children: "Markers" }), _jsxs("div", { className: "tf-text-secondary", children: [draftAnnotations.length, " in this session"] })] }), _jsxs("div", { className: styles.sessionActions, children: [_jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => moveSelectedAnnotation('up'), disabled: !selectedAnnotation || selectedAnnotationIndex <= 0, "aria-label": "Move marker up", children: _jsx(ArrowUp, { size: 16 }) }), _jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => moveSelectedAnnotation('down'), disabled: !selectedAnnotation || selectedAnnotationIndex === -1 || selectedAnnotationIndex >= draftAnnotations.length - 1, "aria-label": "Move marker down", children: _jsx(ArrowDown, { size: 16 }) })] })] }), _jsx("div", { className: `tf-scrollbar ${styles.annotationList}`, children: draftAnnotations.length === 0 ? (_jsx("div", { className: styles.detailEmpty, children: _jsx("p", { className: "tf-empty-copy", children: "Add a marker on the image to begin." }) })) : draftAnnotations.map((annotation, index) => {
2065
+ : 'Saved' }), error ? _jsx("span", { children: error }) : _jsx("span", { children: !selectedSession ? 'Create a session to begin placing markers on this image.' : isPanActive ? 'Drag on the image to pan.' : toolMode === 'select' ? 'Select a marker to edit it.' : `Click on the image to place a ${toolMode}.` }), activeTarget && !selectedSession && showCreateSessionPrompt ? (_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => void createSession(), disabled: saving || loading, children: saving ? 'Creating…' : 'Create one now' })) : null] })] }), _jsx("section", { className: `tf-surface-panel ${styles.panel} ${styles.detailPanel}`, children: selectedSession ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.panelHeader, children: [_jsxs("div", { className: styles.panelHeaderText, children: [_jsx("div", { className: `tf-heading-card ${styles.panelTitle}`, children: "Markers" }), _jsxs("div", { className: "tf-text-secondary", children: [draftAnnotations.length, " in this session"] })] }), _jsxs("div", { className: styles.sessionActions, children: [_jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => moveSelectedAnnotation('up'), disabled: !selectedAnnotation || selectedAnnotationIndex <= 0, "aria-label": "Move marker up", children: _jsx(ArrowUp, { size: 16 }) }), _jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => moveSelectedAnnotation('down'), disabled: !selectedAnnotation || selectedAnnotationIndex === -1 || selectedAnnotationIndex >= draftAnnotations.length - 1, "aria-label": "Move marker down", children: _jsx(ArrowDown, { size: 16 }) })] })] }), _jsx("div", { className: `tf-scrollbar ${styles.annotationList}`, children: draftAnnotations.length === 0 ? (_jsx("div", { className: styles.detailEmpty, children: _jsx("p", { className: "tf-empty-copy", children: "Add a marker on the image to begin." }) })) : draftAnnotations.map((annotation, index) => {
2026
2066
  const AnnotationKindIcon = ANNOTATION_KIND_ICON[annotation.kind];
2027
2067
  const annotationKindLabel = ANNOTATION_KIND_LABELS[annotation.kind];
2028
2068
  const annotationSequenceLabel = `${annotationKindLabel} ${index + 1}`;
@@ -258,12 +258,17 @@ export class PlanEntitlementService {
258
258
  };
259
259
  }
260
260
  if (normalizedFeatureKey === AI_PROFILES_FEATURE_KEY) {
261
+ const limit = this.readPositiveIntegerConfig(config[AI_PROFILES_LIMIT_CONFIG_KEY], 1);
261
262
  if (access === 'limited') {
262
- const limit = this.readPositiveIntegerConfig(config[AI_PROFILES_LIMIT_CONFIG_KEY], 1);
263
263
  return {
264
264
  [AI_PROFILES_LIMIT_CONFIG_KEY]: limit ?? 1
265
265
  };
266
266
  }
267
+ if (limit !== null) {
268
+ return {
269
+ [AI_PROFILES_LIMIT_CONFIG_KEY]: limit
270
+ };
271
+ }
267
272
  return {};
268
273
  }
269
274
  return config && typeof config === 'object' ? { ...config } : {};
@@ -911,6 +911,8 @@ export declare class TaskforceCore implements TaskforceSyncCapable {
911
911
  private tenantId;
912
912
  private db;
913
913
  private globalSettingsDb;
914
+ private readonly globalSettingsSchemaEnsuredStores;
915
+ private readonly structuredSettingsSchemaEnsuredStores;
914
916
  private authTokenService;
915
917
  private identityService;
916
918
  private mcpTokenService;
@@ -40,6 +40,7 @@ import { formatInitiativeReference, formatWorkstreamReference, } from '../utils/
40
40
  import { getDocumentReferenceLabel } from '../utils/documentReferences.js';
41
41
  import { getImageReferenceLabel } from '../utils/imageReferences.js';
42
42
  import { getCanonicalDocumentName } from '../utils/documentNames.js';
43
+ import { isPathInsideRoot, resolveStorageKeyPathInsideRoot } from '../utils/pathSafety.js';
43
44
  import { DEFAULT_TASKFORCE_THEME } from '../utils/theme.js';
44
45
  import { ensureAssigneeTaskSchema, ensureAnnotatedAttachmentSessionsSchema, ensureAuthIdentitySchema, ensureBillingSchema, ensureDocumentReviewSessionsSchema, ensureDocumentReferenceSchema, ensureEntitlementsSchema, ensureScheduleTaskSchema, ensureTaskAssetsSchema, ensureTaskReferenceSchema, ensureTenantSchema, ensureWorkspaceAssetsSchema, ensureWorkspaceSchema } from '../migrations/taskSchemaMigrations.js';
45
46
  export class TaskforceRuleError extends Error {
@@ -142,6 +143,8 @@ export class TaskforceCore {
142
143
  tenantId;
143
144
  db;
144
145
  globalSettingsDb = null;
146
+ globalSettingsSchemaEnsuredStores = new WeakSet();
147
+ structuredSettingsSchemaEnsuredStores = new WeakSet();
145
148
  authTokenService;
146
149
  identityService;
147
150
  mcpTokenService;
@@ -2961,6 +2964,8 @@ export class TaskforceCore {
2961
2964
  return this.globalSettingsDb || this.db;
2962
2965
  }
2963
2966
  ensureGlobalSettingsTable(store) {
2967
+ if (this.globalSettingsSchemaEnsuredStores.has(store))
2968
+ return;
2964
2969
  store.exec(`
2965
2970
  CREATE TABLE IF NOT EXISTS global_settings (
2966
2971
  tenant_id TEXT PRIMARY KEY,
@@ -2968,8 +2973,11 @@ export class TaskforceCore {
2968
2973
  updated_at TEXT NOT NULL
2969
2974
  );
2970
2975
  `);
2976
+ this.globalSettingsSchemaEnsuredStores.add(store);
2971
2977
  }
2972
2978
  ensureStructuredSettingsTables(store) {
2979
+ if (this.structuredSettingsSchemaEnsuredStores.has(store))
2980
+ return;
2973
2981
  store.exec(`
2974
2982
  CREATE TABLE IF NOT EXISTS system_settings (
2975
2983
  tenant_id TEXT NOT NULL DEFAULT 'default',
@@ -3002,6 +3010,7 @@ export class TaskforceCore {
3002
3010
  CREATE INDEX IF NOT EXISTS idx_user_preferences_user_key
3003
3011
  ON user_preferences(user_id, prefs_key);
3004
3012
  `);
3013
+ this.structuredSettingsSchemaEnsuredStores.add(store);
3005
3014
  }
3006
3015
  readSettingsPayloadRow(store, tableName, whereValues) {
3007
3016
  try {
@@ -3260,8 +3269,13 @@ export class TaskforceCore {
3260
3269
  };
3261
3270
  }
3262
3271
  if (storageProvider !== 'r2') {
3263
- const candidatePath = path.join(this.basePath, storageKey);
3264
- if (!fs.existsSync(candidatePath) || !fs.statSync(candidatePath).isFile()) {
3272
+ const candidatePath = resolveStorageKeyPathInsideRoot(this.basePath, storageKey);
3273
+ if (!candidatePath || !fs.existsSync(candidatePath) || !fs.statSync(candidatePath).isFile()) {
3274
+ return null;
3275
+ }
3276
+ const storageRoot = fs.existsSync(this.basePath) ? fs.realpathSync(this.basePath) : path.resolve(this.basePath);
3277
+ const realCandidatePath = fs.realpathSync(candidatePath);
3278
+ if (!isPathInsideRoot(realCandidatePath, storageRoot)) {
3265
3279
  return null;
3266
3280
  }
3267
3281
  }
@@ -2708,14 +2708,21 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2708
2708
  onTaskCountChange(tasks.length);
2709
2709
  }
2710
2710
  }, [tasks.length, onTaskCountChange]);
2711
+ const handledInitialTaskIdRef = useRef(null);
2711
2712
  // Deep linking handler
2712
2713
  useEffect(() => {
2713
- if (initialTaskId && tasks.length > 0) {
2714
- const task = tasks.find(t => t.id.endsWith(initialTaskId) || t.id === initialTaskId);
2715
- if (task) {
2716
- handleEdit(task);
2717
- setActiveTab('add');
2718
- }
2714
+ if (!initialTaskId) {
2715
+ handledInitialTaskIdRef.current = null;
2716
+ return;
2717
+ }
2718
+ if (handledInitialTaskIdRef.current === initialTaskId || tasks.length === 0) {
2719
+ return;
2720
+ }
2721
+ const task = tasks.find(t => t.id.endsWith(initialTaskId) || t.id === initialTaskId);
2722
+ if (task) {
2723
+ handledInitialTaskIdRef.current = initialTaskId;
2724
+ handleEdit(task);
2725
+ setActiveTab('add');
2719
2726
  }
2720
2727
  }, [initialTaskId, tasks]);
2721
2728
  // Update active category logic
@@ -1,11 +1,12 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
+ import { normalizeRelativeStorageKey, resolveExistingStorageKeyFilePathInsideRoot, resolveStorageKeyWritePathInsideRoot } from '../utils/pathSafety.js';
4
5
  export function createCanonicalAssetHelpers(deps) {
5
6
  const normalizeAttachmentStorageKey = (pathValue, fsPathValue) => {
6
7
  const fsPathRaw = String(fsPathValue || '').trim().replace(/\\/g, '/');
7
8
  if (fsPathRaw) {
8
- return fsPathRaw.replace(/^\.taskforce\//, '').trim() || null;
9
+ return normalizeRelativeStorageKey(fsPathRaw.replace(/^\.taskforce\//, '').trim());
9
10
  }
10
11
  const pathRaw = String(pathValue || '').trim();
11
12
  if (!pathRaw)
@@ -13,10 +14,18 @@ export function createCanonicalAssetHelpers(deps) {
13
14
  if (/^https?:\/\//i.test(pathRaw))
14
15
  return pathRaw;
15
16
  if (pathRaw.startsWith('/api/taskforce/context/')) {
16
- return decodeURIComponent(pathRaw.slice('/api/taskforce/context/'.length)).trim() || null;
17
+ return normalizeRelativeStorageKey(decodeURIComponent(pathRaw.slice('/api/taskforce/context/'.length)).trim());
17
18
  }
18
19
  return null;
19
20
  };
21
+ const isSafeCanonicalAssetStorage = (asset) => {
22
+ if (asset.storageProvider === 'external')
23
+ return true;
24
+ return Boolean(normalizeRelativeStorageKey(asset.storageKey));
25
+ };
26
+ const resolveCanonicalLocalFilePath = (storageKey) => {
27
+ return resolveExistingStorageKeyFilePathInsideRoot(deps.basePath, storageKey);
28
+ };
20
29
  const buildSyntheticAttachmentId = (taskId, attachment) => {
21
30
  const stableSource = [
22
31
  taskId,
@@ -32,14 +41,14 @@ export function createCanonicalAssetHelpers(deps) {
32
41
  const assetId = String(attachment.assetId || '').trim();
33
42
  if (assetId) {
34
43
  const byId = deps.workspaceAssetStore.get(assetId, deps.activeWorkspaceId);
35
- if (byId && !byId.deletedAt)
44
+ if (byId && !byId.deletedAt && isSafeCanonicalAssetStorage(byId))
36
45
  return byId;
37
46
  }
38
47
  const storageKey = normalizeAttachmentStorageKey(attachment.path, attachment.fsPath);
39
48
  if (!storageKey || /^https?:\/\//i.test(storageKey))
40
49
  return null;
41
50
  const byStorageKey = deps.workspaceAssetStore.getByStorageKey(storageKey, deps.activeWorkspaceId);
42
- if (byStorageKey && !byStorageKey.deletedAt)
51
+ if (byStorageKey && !byStorageKey.deletedAt && isSafeCanonicalAssetStorage(byStorageKey))
43
52
  return byStorageKey;
44
53
  return null;
45
54
  };
@@ -80,6 +89,9 @@ export function createCanonicalAssetHelpers(deps) {
80
89
  throw new Error('Attachment scan pending');
81
90
  }
82
91
  if (asset.storageProvider === 'r2') {
92
+ if (!normalizeRelativeStorageKey(asset.storageKey)) {
93
+ throw new Error(`Attachment ${attachment.attachmentId} not found on ${targetLabel} ${taskId}`);
94
+ }
83
95
  const attachmentContext = describeAttachmentStorageContext(attachment, asset.storageKey);
84
96
  if (!deps.objectStorageClient) {
85
97
  throw new Error(`Attachment read failed from R2 because object storage is not configured in this MCP runtime (${attachmentContext})`);
@@ -97,11 +109,8 @@ export function createCanonicalAssetHelpers(deps) {
97
109
  }
98
110
  return object.body;
99
111
  }
100
- const resolvedPath = path.resolve(deps.basePath, asset.storageKey);
101
- if (!resolvedPath.startsWith(path.resolve(deps.basePath))) {
102
- throw new Error(`Attachment ${attachment.attachmentId} not found on ${targetLabel} ${taskId}`);
103
- }
104
- if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) {
112
+ const resolvedPath = resolveCanonicalLocalFilePath(asset.storageKey);
113
+ if (!resolvedPath) {
105
114
  throw new Error(`Attachment ${attachment.attachmentId} not found on ${targetLabel} ${taskId}`);
106
115
  }
107
116
  return fs.readFileSync(resolvedPath);
@@ -117,6 +126,9 @@ export function createCanonicalAssetHelpers(deps) {
117
126
  throw new Error('Document scan pending');
118
127
  }
119
128
  if (asset.storageProvider === 'r2') {
129
+ if (!normalizeRelativeStorageKey(asset.storageKey)) {
130
+ throw new Error(`Document ${asset.assetId} not found in workspace ${deps.activeWorkspaceId}.`);
131
+ }
120
132
  if (!deps.objectStorageClient) {
121
133
  throw new Error(`Document read failed from R2 because object storage is not configured in this MCP runtime (assetId=${asset.assetId}, storageKey=${asset.storageKey || 'unknown'})`);
122
134
  }
@@ -133,11 +145,8 @@ export function createCanonicalAssetHelpers(deps) {
133
145
  }
134
146
  return object.body;
135
147
  }
136
- const resolvedPath = path.resolve(deps.basePath, asset.storageKey);
137
- if (!resolvedPath.startsWith(path.resolve(deps.basePath))) {
138
- throw new Error(`Document ${asset.assetId} not found in workspace ${deps.activeWorkspaceId}.`);
139
- }
140
- if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) {
148
+ const resolvedPath = resolveCanonicalLocalFilePath(asset.storageKey);
149
+ if (!resolvedPath) {
141
150
  throw new Error(`Document ${asset.assetId} not found in workspace ${deps.activeWorkspaceId}.`);
142
151
  }
143
152
  return fs.readFileSync(resolvedPath);
@@ -176,14 +185,17 @@ export function createCanonicalAssetHelpers(deps) {
176
185
  const logicalFilename = resolveAttachmentLogicalFilename(attachment, asset);
177
186
  if (logicalFilename !== requestedFilename)
178
187
  continue;
179
- const relativePath = deps.toPosixPath(path.relative(deps.projectRoot, path.resolve(deps.basePath, asset.storageKey)));
188
+ const absolutePath = resolveStorageKeyWritePathInsideRoot(deps.basePath, asset.storageKey);
189
+ if (!absolutePath)
190
+ continue;
191
+ const relativePath = deps.toPosixPath(path.relative(deps.projectRoot, absolutePath));
180
192
  return {
181
193
  index,
182
194
  attachment,
183
195
  asset,
184
196
  descriptor,
185
197
  relativePath,
186
- absolutePath: path.resolve(deps.basePath, asset.storageKey),
198
+ absolutePath,
187
199
  logicalFilename,
188
200
  };
189
201
  }
@@ -197,10 +209,12 @@ export function createCanonicalAssetHelpers(deps) {
197
209
  const targetId = String(targetIdRaw || '').trim();
198
210
  if (!relativePath || !targetId)
199
211
  return null;
200
- const absolutePath = path.resolve(deps.projectRoot, relativePath);
201
- if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile())
212
+ const storageKey = normalizeRelativeStorageKey(relativePath.replace(/^\.taskforce\//, '').trim() || relativePath);
213
+ if (!storageKey)
214
+ return null;
215
+ const absolutePath = resolveExistingStorageKeyFilePathInsideRoot(deps.basePath, storageKey);
216
+ if (!absolutePath)
202
217
  return null;
203
- const storageKey = relativePath.replace(/^\.taskforce\//, '').trim() || relativePath;
204
218
  const buffer = fs.readFileSync(absolutePath);
205
219
  const stats = fs.statSync(absolutePath);
206
220
  const mimeType = deps.inferMimeTypeFromPath(relativePath);
@@ -11,6 +11,7 @@ import { buildResolveAiProfileInputFromBootstrap } from './aiProfileBootstrap.js
11
11
  import { createCanonicalAssetHelpers } from './canonicalAssetHelpers.js';
12
12
  import { createDocumentHelpers } from './documentHelpers.js';
13
13
  import { createTaskAttachmentHelpers } from './taskAttachmentHelpers.js';
14
+ import { isPathWithinRoot } from '../utils/pathContainment.js';
14
15
  import { validateStructuredDocForAttach } from './structuredDocValidation.js';
15
16
  import { validateStructuredCommentForAdd } from './structuredCommentValidation.js';
16
17
  import { resolveDatabaseConfigFromEnv } from '../storage/providerConfig.js';
@@ -23,6 +24,7 @@ import { getDocumentReferenceLabel, parseDocumentReference } from '../utils/docu
23
24
  import { getImageReferenceLabel, parseImageReference } from '../utils/imageReferences.js';
24
25
  import { inferCanonicalAssetKind, isValidCanonicalDocumentAsset } from '../utils/canonicalAssetKind.js';
25
26
  import { getTaskReferenceLabel, shouldUseProvisionalTaskReferences } from '../utils/taskReferences.js';
27
+ import { resolveStorageKeyPathInsideRoot } from '../utils/pathSafety.js';
26
28
  import { parseAiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
27
29
  import { parseAiProfileSeatScope } from '../shared/aiProfileSeatScope.js';
28
30
  import { formatInitiativeReference, formatWorkstreamReference, resolveInitiativeReference, parseWorkstreamReference, } from '../utils/planningReferences.js';
@@ -1426,12 +1428,8 @@ export function createTaskforceMcpServer(options = {}) {
1426
1428
  function isHttpUrl(value) {
1427
1429
  return /^https?:\/\//i.test(value);
1428
1430
  }
1429
- function isWithinRoot(filePath, root) {
1430
- const relative = path.relative(root, filePath);
1431
- return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
1432
- }
1433
1431
  function isApprovedAttachmentSourcePath(filePath, projectRootReal) {
1434
- return getApprovedAttachmentSourceRoots(projectRootReal).some((root) => isWithinRoot(filePath, root));
1432
+ return getApprovedAttachmentSourceRoots(projectRootReal).some((root) => isPathWithinRoot(filePath, root));
1435
1433
  }
1436
1434
  function toPosixPath(filePath) {
1437
1435
  return filePath.split(path.sep).join('/');
@@ -2187,6 +2185,17 @@ export function createTaskforceMcpServer(options = {}) {
2187
2185
  ? " NOTE: Use this tool instead of editing files directly to ensure data integrity."
2188
2186
  : "";
2189
2187
  const describeTool = (text) => prefix + text + agentNote;
2188
+ const titleForTool = (name) => String(name || '')
2189
+ .split('_')
2190
+ .filter(Boolean)
2191
+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
2192
+ .join(' ');
2193
+ const annotationsForTool = (name) => {
2194
+ const title = titleForTool(name);
2195
+ return isMcpWriteToolName(name)
2196
+ ? { title, destructiveHint: true }
2197
+ : { title, readOnlyHint: true };
2198
+ };
2190
2199
  const tools = registeredTools
2191
2200
  .map((tool) => tool.buildDescriptor({
2192
2201
  describeTool,
@@ -2195,6 +2204,13 @@ export function createTaskforceMcpServer(options = {}) {
2195
2204
  typeEnum,
2196
2205
  approachEnum,
2197
2206
  complexityDesc,
2207
+ }))
2208
+ .map((tool) => ({
2209
+ ...tool,
2210
+ annotations: {
2211
+ ...annotationsForTool(tool.name),
2212
+ ...(tool.annotations || {}),
2213
+ },
2198
2214
  }))
2199
2215
  .map((tool) => {
2200
2216
  if (tool.name === 'resolve_profile' || !isMcpWriteToolName(tool.name))
@@ -2976,9 +2992,24 @@ export function createTaskforceMcpServer(options = {}) {
2976
2992
  case "replace_task_checklist": {
2977
2993
  const { taskId, items } = args;
2978
2994
  const { id: resolvedTaskId } = resolveTaskByIdentifierOrThrow(taskId);
2995
+ const now = new Date().toISOString();
2996
+ const checklistItems = Array.isArray(items)
2997
+ ? items.map((item, index) => ({
2998
+ id: item?.id,
2999
+ taskId: resolvedTaskId,
3000
+ title: String(item?.title ?? item?.text ?? '').trim() || `Checklist item ${index + 1}`,
3001
+ isCompleted: Boolean(item?.isCompleted ?? item?.done),
3002
+ order: Number.isFinite(Number(item?.order)) ? Number(item.order) : index,
3003
+ createdAt: String(item?.createdAt || now),
3004
+ updatedAt: item?.updatedAt ? String(item.updatedAt) : now,
3005
+ }))
3006
+ : [];
2979
3007
  const updatedTask = core.updateTask(resolvedTaskId, {
2980
- checklistItems: Array.isArray(items) ? items : [],
3008
+ checklistItems,
2981
3009
  }, ACTIVE_WORKSPACE_ID, { actorRef: getActiveMcpActorRef() });
3010
+ if (!updatedTask) {
3011
+ throw new Error(`Task ${resolvedTaskId} not found`);
3012
+ }
2982
3013
  const replaced = core.listChecklistItems(resolvedTaskId, ACTIVE_WORKSPACE_ID);
2983
3014
  recordMcpWriteAudit('replace_task_checklist', {
2984
3015
  taskId: resolvedTaskId,
@@ -2994,7 +3025,6 @@ export function createTaskforceMcpServer(options = {}) {
2994
3025
  text: JSON.stringify({
2995
3026
  taskId: resolvedTaskId,
2996
3027
  items: replaced,
2997
- ...(updatedTask ? { task: updatedTask } : {}),
2998
3028
  }, null, 2),
2999
3029
  }],
3000
3030
  };
@@ -4880,8 +4910,8 @@ export function createTaskforceMcpServer(options = {}) {
4880
4910
  await objectStorageClient.deleteObject(replacedStorageKey).catch(() => { });
4881
4911
  }
4882
4912
  else if (overwriteTarget.asset.storageProvider === 'local') {
4883
- const replacedAbsolutePath = path.resolve(basePath, replacedStorageKey);
4884
- if (replacedAbsolutePath.startsWith(path.resolve(basePath)) && fs.existsSync(replacedAbsolutePath)) {
4913
+ const replacedAbsolutePath = resolveStorageKeyPathInsideRoot(basePath, replacedStorageKey);
4914
+ if (replacedAbsolutePath && fs.existsSync(replacedAbsolutePath)) {
4885
4915
  fs.rmSync(replacedAbsolutePath, { force: true });
4886
4916
  }
4887
4917
  }
@@ -1,18 +1,40 @@
1
1
  export function createTaskAttachmentHelpers(deps) {
2
2
  const isCanonicalDocumentAssetRecord = (asset) => deps.isValidCanonicalDocumentAsset(asset);
3
+ const inferSpecificMimeType = (...candidates) => {
4
+ for (const candidate of candidates) {
5
+ const value = String(candidate || '').trim();
6
+ if (!value)
7
+ continue;
8
+ const inferred = deps.inferMimeTypeFromPath(value);
9
+ if (inferred && inferred !== 'application/octet-stream')
10
+ return inferred;
11
+ }
12
+ return '';
13
+ };
3
14
  const describeTaskAttachment = (taskId, attachmentInput) => {
4
15
  const attachment = deps.canonicalAssetHelpers.toTaskAttachmentRecord(attachmentInput);
5
16
  const asset = deps.canonicalAssetHelpers.getAttachmentAssetRecord(attachment);
6
17
  const storageKey = deps.canonicalAssetHelpers.normalizeAttachmentStorageKey(attachment.path, attachment.fsPath);
7
- const assetId = String(attachment.assetId || asset?.assetId || '').trim() || undefined;
8
- const source = (asset?.storageProvider === 'external' || /^https?:\/\//i.test(String(attachment.path || '').trim()))
18
+ const assetId = String(asset?.assetId || '').trim() || undefined;
19
+ const source = (asset?.storageProvider === 'external'
20
+ || /^https?:\/\//i.test(String(attachment.path || '').trim())
21
+ || (!asset && !storageKey))
9
22
  ? 'external'
10
23
  : 'canonical';
11
- const mimeType = String(asset?.mimeType
12
- || (storageKey && !/^https?:\/\//i.test(storageKey) ? deps.inferMimeTypeFromPath(storageKey) : '')
13
- || deps.inferMimeTypeFromPath(String(attachment.originalFilename || attachment.displayName || attachment.caption || attachment.path || ''))).trim() || 'application/octet-stream';
14
- const kind = asset?.kind || deps.inferAssetKind(storageKey || String(attachment.path || ''), mimeType);
15
- const isCanonicalDocument = Boolean(asset && isCanonicalDocumentAssetRecord(asset)) || (!asset && kind === 'document');
24
+ const storedMimeType = String(asset?.mimeType || '').trim();
25
+ const inferredMimeType = inferSpecificMimeType(asset?.originalFilename, asset?.storageKey, storageKey && !/^https?:\/\//i.test(storageKey) ? storageKey : '', attachment.originalFilename, attachment.displayName, attachment.path);
26
+ const mimeType = (storedMimeType && storedMimeType !== 'application/octet-stream'
27
+ ? storedMimeType
28
+ : inferredMimeType) || storedMimeType || 'application/octet-stream';
29
+ const inferredKind = deps.inferAssetKind(asset?.originalFilename || asset?.storageKey || storageKey || String(attachment.originalFilename || attachment.path || ''), mimeType);
30
+ const kind = asset?.kind && asset.kind !== 'file' ? asset.kind : inferredKind;
31
+ const isCanonicalDocument = kind === 'document' && (!asset
32
+ || isCanonicalDocumentAssetRecord({
33
+ kind,
34
+ storageKey: String(asset.storageKey || storageKey || '').trim(),
35
+ originalFilename: String(asset.originalFilename || attachment.originalFilename || '').trim(),
36
+ mimeType,
37
+ }));
16
38
  const referenceNumber = typeof attachment.referenceNumber === 'number'
17
39
  ? attachment.referenceNumber
18
40
  : (typeof asset?.referenceNumber === 'number' ? asset.referenceNumber : null);
@@ -8,6 +8,13 @@ export type McpToolDescriptor = {
8
8
  name: McpToolName;
9
9
  description: string;
10
10
  inputSchema: McpToolInputSchema;
11
+ annotations?: {
12
+ title?: string;
13
+ readOnlyHint?: boolean;
14
+ destructiveHint?: boolean;
15
+ idempotentHint?: boolean;
16
+ openWorldHint?: boolean;
17
+ };
11
18
  };
12
19
  export type McpToolDescriptorContext = {
13
20
  describeTool: (text: string) => string;
@@ -784,7 +784,7 @@ export function createStandaloneServer(options) {
784
784
  return;
785
785
  }
786
786
  }
787
- if (isAuthApiPath) {
787
+ if (shouldApplyAuthRateLimit) {
788
788
  const latestAuthRateSettings = resolveAuthRateLimitSettings();
789
789
  if (latestAuthRateSettings.enabled !== authRateLimitSettings.enabled
790
790
  || latestAuthRateSettings.maxRequests !== authRateLimitSettings.maxRequests