@neuralis/brain-core 0.3.1 → 0.4.0
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.
- package/README.md +88 -1417
- package/app/README.md +4 -4
- package/app/cards/FilesystemCardShell.tsx +42 -4
- package/app/cards/FilesystemChangeCard.tsx +34 -11
- package/app/cards/README.md +6 -5
- package/app/cards/diffUtils.ts +99 -1
- package/app/filesystem/FilesystemHost.tsx +24 -2
- package/app/filesystem/README.md +1 -1
- package/app/filesystem/api/README.md +3 -3
- package/app/filesystem/api/index.ts +38 -14
- package/app/filesystem/api/projectPackages.ts +13 -102
- package/app/filesystem/domain/types.ts +30 -10
- package/app/filesystem/hooks/README.md +4 -4
- package/app/filesystem/hooks/useDiscoverableSources.ts +19 -30
- package/app/filesystem/hooks/useFilesystemSSE.ts +42 -2
- package/app/filesystem/host/README.md +6 -4
- package/app/filesystem/host/buildFilesystemHostValue.ts +28 -2
- package/app/filesystem/host/installFilesystemHostComponents.tsx +1 -1
- package/app/filesystem/lib/sourceKindForm.ts +210 -27
- package/app/filesystem/lib/treeVisualState.tsx +98 -4
- package/app/filesystem/store/README.md +1 -0
- package/app/filesystem/store/filesystemStore.ts +99 -8
- package/app/filesystem/store/gitSlice.ts +5 -0
- package/app/filesystem/store/helpers.ts +17 -0
- package/app/filesystem/store/listSlice.ts +8 -0
- package/app/filesystem/ui/CodeEditor.tsx +18 -3
- package/app/filesystem/ui/DiscoverDock.tsx +5 -1
- package/app/filesystem/ui/FileEditor.tsx +15 -10
- package/app/filesystem/ui/FileTree.tsx +75 -24
- package/app/filesystem/ui/FilesystemWidget.tsx +10 -2
- package/app/filesystem/ui/FolderPicker.tsx +251 -0
- package/app/filesystem/ui/GitControlPanel.tsx +7 -2
- package/app/filesystem/ui/PackagesPanel.tsx +52 -374
- package/app/filesystem/ui/PendingChangesPanel.tsx +1 -1
- package/app/filesystem/ui/PolicyEditor.tsx +109 -24
- package/app/filesystem/ui/README.md +30 -23
- package/app/filesystem/ui/RawMediaPreview.tsx +58 -0
- package/app/filesystem/ui/SourceBranchHeader.tsx +4 -4
- package/app/filesystem/ui/SourcesPanel.tsx +502 -342
- package/app/filesystem/ui/Toolbar.tsx +1 -2
- package/app/filesystem/ui/VectorHealthPanel.tsx +2 -2
- package/app/shared/filesystem.png +0 -0
- package/dist/app/host.js +58 -46
- package/dist/scripts/sourceRootPairV1.d.ts +47 -0
- package/dist/scripts/sourceRootPairV1.d.ts.map +1 -0
- package/dist/scripts/sourceRootPairV1.js +147 -0
- package/dist/scripts/sourceRootPairV1.js.map +1 -0
- package/dist/scripts/sourceToFolderV1.d.ts +56 -0
- package/dist/scripts/sourceToFolderV1.d.ts.map +1 -0
- package/dist/scripts/sourceToFolderV1.js +152 -0
- package/dist/scripts/sourceToFolderV1.js.map +1 -0
- package/dist/scripts/sourceToHostV1.d.ts +56 -0
- package/dist/scripts/sourceToHostV1.d.ts.map +1 -0
- package/dist/scripts/sourceToHostV1.js +132 -0
- package/dist/scripts/sourceToHostV1.js.map +1 -0
- package/dist/src/channels/filesystem.d.ts +1 -1
- package/dist/src/channels/filesystem.js +1 -1
- package/dist/src/connectors/AppSourceConfigStore.d.ts +17 -3
- package/dist/src/connectors/AppSourceConfigStore.d.ts.map +1 -1
- package/dist/src/connectors/AppSourceConfigStore.js +46 -6
- package/dist/src/connectors/AppSourceConfigStore.js.map +1 -1
- package/dist/src/connectors/HostConnector.d.ts +201 -0
- package/dist/src/connectors/HostConnector.d.ts.map +1 -0
- package/dist/src/connectors/HostConnector.js +903 -0
- package/dist/src/connectors/HostConnector.js.map +1 -0
- package/dist/src/connectors/inProcessTransport.d.ts +131 -0
- package/dist/src/connectors/inProcessTransport.d.ts.map +1 -0
- package/dist/src/connectors/inProcessTransport.js +447 -0
- package/dist/src/connectors/inProcessTransport.js.map +1 -0
- package/dist/src/domain/access/callerPermissions.d.ts +29 -0
- package/dist/src/domain/access/callerPermissions.d.ts.map +1 -1
- package/dist/src/domain/access/callerPermissions.js +85 -5
- package/dist/src/domain/access/callerPermissions.js.map +1 -1
- package/dist/src/domain/access/nativeLocalFence.d.ts +65 -0
- package/dist/src/domain/access/nativeLocalFence.d.ts.map +1 -0
- package/dist/src/domain/access/nativeLocalFence.js +171 -0
- package/dist/src/domain/access/nativeLocalFence.js.map +1 -0
- package/dist/src/domain/access/uriPermission.js +3 -3
- package/dist/src/domain/access/uriPermission.js.map +1 -1
- package/dist/src/domain/byteRange.d.ts +15 -0
- package/dist/src/domain/byteRange.d.ts.map +1 -0
- package/dist/src/domain/byteRange.js +30 -0
- package/dist/src/domain/byteRange.js.map +1 -0
- package/dist/src/domain/defaultPermissions.d.ts.map +1 -1
- package/dist/src/domain/defaultPermissions.js +29 -0
- package/dist/src/domain/defaultPermissions.js.map +1 -1
- package/dist/src/domain/mime.d.ts +11 -19
- package/dist/src/domain/mime.d.ts.map +1 -1
- package/dist/src/domain/mime.js +13 -102
- package/dist/src/domain/mime.js.map +1 -1
- package/dist/src/domain/paths.d.ts.map +1 -1
- package/dist/src/domain/paths.js +5 -3
- package/dist/src/domain/paths.js.map +1 -1
- package/dist/src/domain/scopeAccessCtx.d.ts +2 -2
- package/dist/src/domain/scopeAccessCtx.js +2 -2
- package/dist/src/domain/sourceConfig.d.ts +52 -19
- package/dist/src/domain/sourceConfig.d.ts.map +1 -1
- package/dist/src/domain/sourceConfig.js +103 -19
- package/dist/src/domain/sourceConfig.js.map +1 -1
- package/dist/src/domain/sourcePackages.d.ts +8 -8
- package/dist/src/domain/sourcePackages.d.ts.map +1 -1
- package/dist/src/domain/sourceRootPair.d.ts +88 -0
- package/dist/src/domain/sourceRootPair.d.ts.map +1 -0
- package/dist/src/domain/sourceRootPair.js +108 -0
- package/dist/src/domain/sourceRootPair.js.map +1 -0
- package/dist/src/domain/syncTypes.d.ts +8 -1
- package/dist/src/domain/syncTypes.d.ts.map +1 -1
- package/dist/src/domain/syncTypes.js +1 -0
- package/dist/src/domain/syncTypes.js.map +1 -1
- package/dist/src/domain/types.d.ts +32 -2
- package/dist/src/domain/types.d.ts.map +1 -1
- package/dist/src/domain/uri.d.ts.map +1 -1
- package/dist/src/domain/uri.js +3 -2
- package/dist/src/domain/uri.js.map +1 -1
- package/dist/src/embeddings/clients/GeminiEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/GeminiEmbedder.js +2 -5
- package/dist/src/embeddings/clients/GeminiEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/OpenAIEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/OpenAIEmbedder.js +2 -3
- package/dist/src/embeddings/clients/OpenAIEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/QwenEmbedder.d.ts +2 -0
- package/dist/src/embeddings/clients/QwenEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/QwenEmbedder.js +10 -2
- package/dist/src/embeddings/clients/QwenEmbedder.js.map +1 -1
- package/dist/src/embeddings/clients/VoyageEmbedder.d.ts +2 -0
- package/dist/src/embeddings/clients/VoyageEmbedder.d.ts.map +1 -1
- package/dist/src/embeddings/clients/VoyageEmbedder.js +8 -1
- package/dist/src/embeddings/clients/VoyageEmbedder.js.map +1 -1
- package/dist/src/embeddings/factory.d.ts +5 -3
- package/dist/src/embeddings/factory.d.ts.map +1 -1
- package/dist/src/embeddings/factory.js +7 -3
- package/dist/src/embeddings/factory.js.map +1 -1
- package/dist/src/embeddings/models.d.ts +1 -1
- package/dist/src/embeddings/models.d.ts.map +1 -1
- package/dist/src/embeddings/retry.js +1 -1
- package/dist/src/embeddings/retry.js.map +1 -1
- package/dist/src/embeddings/types.d.ts +6 -0
- package/dist/src/embeddings/types.d.ts.map +1 -1
- package/dist/src/embeddings/types.js +11 -0
- package/dist/src/embeddings/types.js.map +1 -1
- package/dist/src/events/pending.d.ts +1 -1
- package/dist/src/events/pending.js +1 -1
- package/dist/src/git/GitRunner.d.ts +110 -30
- package/dist/src/git/GitRunner.d.ts.map +1 -1
- package/dist/src/git/GitRunner.js +511 -199
- package/dist/src/git/GitRunner.js.map +1 -1
- package/dist/src/git/GitWatchRegistry.d.ts +41 -35
- package/dist/src/git/GitWatchRegistry.d.ts.map +1 -1
- package/dist/src/git/GitWatchRegistry.js +61 -79
- package/dist/src/git/GitWatchRegistry.js.map +1 -1
- package/dist/src/git/gitPlace.d.ts +61 -0
- package/dist/src/git/gitPlace.d.ts.map +1 -0
- package/dist/src/git/gitPlace.js +74 -0
- package/dist/src/git/gitPlace.js.map +1 -0
- package/dist/src/git/sourceGitControl.d.ts +33 -21
- package/dist/src/git/sourceGitControl.d.ts.map +1 -1
- package/dist/src/git/sourceGitControl.js +211 -95
- package/dist/src/git/sourceGitControl.js.map +1 -1
- package/dist/src/git/sourceGitStatus.js +1 -1
- package/dist/src/git/sourceGitStatus.js.map +1 -1
- package/dist/src/git/untrackedStats.js +1 -1
- package/dist/src/git/untrackedStats.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lifecycle.d.ts +9 -12
- package/dist/src/lifecycle.d.ts.map +1 -1
- package/dist/src/lifecycle.js +2 -1
- package/dist/src/lifecycle.js.map +1 -1
- package/dist/src/routes/approve.d.ts +1 -1
- package/dist/src/routes/approve.js +1 -1
- package/dist/src/routes/events.d.ts +1 -1
- package/dist/src/routes/events.js +1 -1
- package/dist/src/routes/git.d.ts +7 -7
- package/dist/src/routes/git.d.ts.map +1 -1
- package/dist/src/routes/git.js +201 -72
- package/dist/src/routes/git.js.map +1 -1
- package/dist/src/routes/health.d.ts +6 -6
- package/dist/src/routes/health.js +12 -12
- package/dist/src/routes/helpers/sourceNotConfigured.d.ts +1 -1
- package/dist/src/routes/helpers/sourceNotConfigured.js +1 -1
- package/dist/src/routes/helpers/uriGuard.d.ts +1 -1
- package/dist/src/routes/helpers/uriGuard.d.ts.map +1 -1
- package/dist/src/routes/helpers/uriGuard.js +3 -2
- package/dist/src/routes/helpers/uriGuard.js.map +1 -1
- package/dist/src/routes/helpers.d.ts +11 -4
- package/dist/src/routes/helpers.d.ts.map +1 -1
- package/dist/src/routes/helpers.js +13 -6
- package/dist/src/routes/helpers.js.map +1 -1
- package/dist/src/routes/list.d.ts +1 -1
- package/dist/src/routes/list.d.ts.map +1 -1
- package/dist/src/routes/list.js +3 -4
- package/dist/src/routes/list.js.map +1 -1
- package/dist/src/routes/pending.d.ts +4 -4
- package/dist/src/routes/pending.js +9 -9
- package/dist/src/routes/policies.d.ts +10 -8
- package/dist/src/routes/policies.d.ts.map +1 -1
- package/dist/src/routes/policies.js +31 -25
- package/dist/src/routes/policies.js.map +1 -1
- package/dist/src/routes/raw.d.ts +1 -1
- package/dist/src/routes/raw.d.ts.map +1 -1
- package/dist/src/routes/raw.js +66 -12
- package/dist/src/routes/raw.js.map +1 -1
- package/dist/src/routes/read.d.ts +1 -1
- package/dist/src/routes/read.js +1 -1
- package/dist/src/routes/search.d.ts +1 -1
- package/dist/src/routes/search.js +1 -1
- package/dist/src/routes/sources.d.ts +63 -6
- package/dist/src/routes/sources.d.ts.map +1 -1
- package/dist/src/routes/sources.js +494 -382
- package/dist/src/routes/sources.js.map +1 -1
- package/dist/src/routes/upload.d.ts +1 -1
- package/dist/src/routes/upload.js +1 -1
- package/dist/src/routes/write.d.ts +2 -2
- package/dist/src/routes/write.js +2 -2
- package/dist/src/services/BridgeService.d.ts +21 -35
- package/dist/src/services/BridgeService.d.ts.map +1 -1
- package/dist/src/services/BridgeService.js +42 -88
- package/dist/src/services/BridgeService.js.map +1 -1
- package/dist/src/services/FilesystemService.d.ts +4 -3
- package/dist/src/services/FilesystemService.d.ts.map +1 -1
- package/dist/src/services/FilesystemService.js +112 -34
- package/dist/src/services/FilesystemService.js.map +1 -1
- package/dist/src/services/FilesystemUiService.d.ts +4 -1
- package/dist/src/services/FilesystemUiService.d.ts.map +1 -1
- package/dist/src/services/FilesystemUiService.js +66 -25
- package/dist/src/services/FilesystemUiService.js.map +1 -1
- package/dist/src/services/SyncScheduler.d.ts +12 -4
- package/dist/src/services/SyncScheduler.d.ts.map +1 -1
- package/dist/src/services/SyncScheduler.js +37 -8
- package/dist/src/services/SyncScheduler.js.map +1 -1
- package/dist/src/services/baselineRepair.d.ts +13 -3
- package/dist/src/services/baselineRepair.d.ts.map +1 -1
- package/dist/src/services/baselineRepair.js +13 -3
- package/dist/src/services/baselineRepair.js.map +1 -1
- package/dist/src/services/declaredSources.d.ts +132 -43
- package/dist/src/services/declaredSources.d.ts.map +1 -1
- package/dist/src/services/declaredSources.js +118 -45
- package/dist/src/services/declaredSources.js.map +1 -1
- package/dist/src/store/dataFormats.d.ts +17 -2
- package/dist/src/store/dataFormats.d.ts.map +1 -1
- package/dist/src/store/dataFormats.js +18 -3
- package/dist/src/store/dataFormats.js.map +1 -1
- package/dist/src/sync/DeltaSyncRunner.d.ts.map +1 -1
- package/dist/src/sync/DeltaSyncRunner.js +144 -4
- package/dist/src/sync/DeltaSyncRunner.js.map +1 -1
- package/dist/src/sync/SyncJobQueue.d.ts +7 -2
- package/dist/src/sync/SyncJobQueue.d.ts.map +1 -1
- package/dist/src/sync/SyncJobQueue.js +16 -10
- package/dist/src/sync/SyncJobQueue.js.map +1 -1
- package/dist/src/sync/SyncWatchService.d.ts +21 -4
- package/dist/src/sync/SyncWatchService.d.ts.map +1 -1
- package/dist/src/sync/SyncWatchService.js +115 -10
- package/dist/src/sync/SyncWatchService.js.map +1 -1
- package/dist/src/sync/categoryInference.d.ts +1 -1
- package/dist/src/sync/categoryInference.js +1 -1
- package/dist/src/tools/execCtx.d.ts +2 -2
- package/dist/src/tools/execCtx.js +2 -2
- package/dist/src/tools/fs_read.d.ts.map +1 -1
- package/dist/src/tools/fs_read.js +5 -0
- package/dist/src/tools/fs_read.js.map +1 -1
- package/dist/src/usecases/admitSourceRoot.d.ts +65 -0
- package/dist/src/usecases/admitSourceRoot.d.ts.map +1 -0
- package/dist/src/usecases/admitSourceRoot.js +85 -0
- package/dist/src/usecases/admitSourceRoot.js.map +1 -0
- package/dist/src/usecases/browseSourceFolders.d.ts +85 -0
- package/dist/src/usecases/browseSourceFolders.d.ts.map +1 -0
- package/dist/src/usecases/browseSourceFolders.js +210 -0
- package/dist/src/usecases/browseSourceFolders.js.map +1 -0
- package/dist/src/usecases/createDeclaredSources.d.ts +13 -2
- package/dist/src/usecases/createDeclaredSources.d.ts.map +1 -1
- package/dist/src/usecases/createDeclaredSources.js +39 -0
- package/dist/src/usecases/createDeclaredSources.js.map +1 -1
- package/dist/src/usecases/embeddingBacklog.js +1 -1
- package/dist/src/usecases/embeddingBacklog.js.map +1 -1
- package/dist/src/usecases/list.d.ts.map +1 -1
- package/dist/src/usecases/list.js +83 -26
- package/dist/src/usecases/list.js.map +1 -1
- package/dist/src/usecases/listAttachableDevices.d.ts +13 -0
- package/dist/src/usecases/listAttachableDevices.d.ts.map +1 -0
- package/dist/src/usecases/listAttachableDevices.js +10 -0
- package/dist/src/usecases/listAttachableDevices.js.map +1 -0
- package/dist/src/usecases/listSourcePackages.d.ts.map +1 -1
- package/dist/src/usecases/listSourcePackages.js +2 -5
- package/dist/src/usecases/listSourcePackages.js.map +1 -1
- package/dist/src/usecases/purgeExcludedPoints.d.ts.map +1 -1
- package/dist/src/usecases/purgeExcludedPoints.js +3 -2
- package/dist/src/usecases/purgeExcludedPoints.js.map +1 -1
- package/dist/src/usecases/readFiles.d.ts.map +1 -1
- package/dist/src/usecases/readFiles.js +99 -1
- package/dist/src/usecases/readFiles.js.map +1 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.d.ts +1 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.d.ts.map +1 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.js +2 -1
- package/dist/src/usecases/reconcileSourcePackageRoots.js.map +1 -1
- package/dist/src/usecases/revokePrincipal.d.ts +7 -5
- package/dist/src/usecases/revokePrincipal.d.ts.map +1 -1
- package/dist/src/usecases/revokePrincipal.js +29 -7
- package/dist/src/usecases/revokePrincipal.js.map +1 -1
- package/dist/src/usecases/scheduleSourceSync.d.ts +7 -0
- package/dist/src/usecases/scheduleSourceSync.d.ts.map +1 -1
- package/dist/src/usecases/scheduleSourceSync.js +6 -5
- package/dist/src/usecases/scheduleSourceSync.js.map +1 -1
- package/dist/src/usecases/syncPointWrites.d.ts +2 -0
- package/dist/src/usecases/syncPointWrites.d.ts.map +1 -1
- package/dist/src/usecases/syncPointWrites.js +5 -1
- package/dist/src/usecases/syncPointWrites.js.map +1 -1
- package/dist/src/usecases/triggerSourceSync.d.ts +46 -0
- package/dist/src/usecases/triggerSourceSync.d.ts.map +1 -0
- package/dist/src/usecases/triggerSourceSync.js +75 -0
- package/dist/src/usecases/triggerSourceSync.js.map +1 -0
- package/dist/src/usecases/uploadBinary.d.ts +2 -1
- package/dist/src/usecases/uploadBinary.d.ts.map +1 -1
- package/dist/src/usecases/uploadBinary.js +2 -2
- package/dist/src/usecases/uploadBinary.js.map +1 -1
- package/dist/src/usecases/vector/endpointGuard.d.ts +1 -1
- package/dist/src/usecases/vector/endpointGuard.js +1 -1
- package/dist/src/usecases/vectorMaintenance.js +1 -1
- package/dist/src/usecases/vectorMaintenance.js.map +1 -1
- package/dist/src/usecases/write/content.js +1 -1
- package/dist/src/vector/qdrantClient.d.ts +1 -1
- package/dist/src/vector/qdrantClient.js +1 -1
- package/dist/src/wiring.d.ts +3 -0
- package/dist/src/wiring.d.ts.map +1 -1
- package/dist/src/wiring.js +72 -6
- package/dist/src/wiring.js.map +1 -1
- package/dist/tools/fs_list.json +1 -1
- package/dist/tools/fs_read.json +1 -1
- package/dist/tools/fs_search.json +1 -1
- package/dist/tools/fs_write.json +1 -1
- package/instructions/brain-model.md +1 -1
- package/package.json +212 -114
- package/rules/brain-rules.md +2 -2
- package/skills/brain-start/SKILL.md +1 -1
- package/skills/connect-business-data/SKILL.md +3 -3
- package/skills/create-package/SKILL.md +26 -26
- package/skills/create-package/references/config-and-credentials.md +16 -13
- package/skills/create-package/references/connectors-and-uri-policies.md +39 -25
- package/skills/create-package/references/manifest-and-discovery.md +12 -12
- package/skills/create-package/references/runtime-and-build.md +19 -19
- package/skills/create-package/references/skills-and-context.md +36 -20
- package/skills/create-package/references/surfaces-and-design.md +16 -16
- package/skills/create-package/references/team-and-workflows.md +2 -2
- package/skills/create-package/references/test-and-ship.md +21 -21
- package/skills/create-package/references/tools-and-schemas.md +5 -5
- package/skills/create-package/scripts/{check-package.sh → check-package.mjs} +79 -65
- package/skills/create-package/scripts/{verify-loaded.sh → verify-loaded.mjs} +156 -162
- package/skills/git/SKILL.md +53 -51
- package/skills/manage-files/SKILL.md +22 -25
- package/skills/manage-sources/SKILL.md +128 -87
- package/skills/manage-uri-policy/SKILL.md +18 -15
- package/skills/organize/SKILL.md +1 -1
- package/skills/sync/SKILL.md +3 -3
- package/tools/fs_list.json +1 -1
- package/tools/fs_read.json +1 -1
- package/tools/fs_search.json +1 -1
- package/tools/fs_write.json +1 -1
- package/workflows/sources-digest.json +1 -1
- package/dist/src/connectors/LocalDiskConnector.d.ts +0 -171
- package/dist/src/connectors/LocalDiskConnector.d.ts.map +0 -1
- package/dist/src/connectors/LocalDiskConnector.js +0 -1233
- package/dist/src/connectors/LocalDiskConnector.js.map +0 -1
- package/docs/drive-overview.md +0 -59
- package/skills/manage-files/scripts/copy.sh +0 -7
- package/skills/manage-files/scripts/create.sh +0 -7
- package/skills/manage-files/scripts/mkdir.sh +0 -7
- package/skills/manage-files/scripts/modify.sh +0 -7
- package/skills/manage-files/scripts/upload.sh +0 -16
- package/skills/manage-files/scripts/write.sh +0 -29
- package/skills/manage-sources/scripts/reembed.sh +0 -23
package/dist/src/routes/git.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* "Git Control" sidebar panel, branch-switcher, side-by-side diff, and the
|
|
4
4
|
* in-panel commit graph.
|
|
5
5
|
*
|
|
6
|
-
* READ routes (GET, `
|
|
6
|
+
* READ routes (GET, `files.read`-gated):
|
|
7
7
|
* GET /git/status?source=<slug> — the WHOLE-repo change-set (staged /
|
|
8
8
|
* unstaged / untracked) + current branch +
|
|
9
9
|
* upstream/ahead/behind when an upstream
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
* including the hunk-extracted ones. Without `repo` everything is
|
|
76
76
|
* byte-identical to the single-repo behavior.
|
|
77
77
|
*
|
|
78
|
-
* WRITE routes (POST, additionally `
|
|
78
|
+
* WRITE routes (POST, additionally `files.write`-gated — G2.3):
|
|
79
79
|
* POST /git/stage {source, paths?[], hunk?} — whole-file `add` OR hunk
|
|
80
80
|
* `apply --cached`.
|
|
81
81
|
* POST /git/unstage {source, paths?[], hunk?} — whole-file `reset -- <p>`
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
* (plain, non-force — git refuses
|
|
91
91
|
* if it would overwrite local changes).
|
|
92
92
|
*
|
|
93
|
-
* REMOTE routes (POST, `
|
|
93
|
+
* REMOTE routes (POST, `files.write` + uri-policy `exec` — G4 keyring-kestrel Slice 2):
|
|
94
94
|
* POST /git/fetch {source} — `git fetch origin` (updates remote-tracking refs).
|
|
95
95
|
* POST /git/pull {source} — `fetch` + `merge --ff-only FETCH_HEAD` (S4: token
|
|
96
96
|
* never rides the working-tree-writing merge).
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
* composed ONCE in the resolver, threaded into `guardWritePaths` (incl.
|
|
115
115
|
* hunk-extracted paths) so no gate site can drift (MAJOR-B).
|
|
116
116
|
* - WRITE is the HUMAN face: the caller's authenticated session IS the consent
|
|
117
|
-
* (uri-policy `write` + the `
|
|
117
|
+
* (uri-policy `write` + the `files.write` feature). The agent-approval
|
|
118
118
|
* `PendingInteraction` ladder is a SEPARATE later increment (G3) — NOT here.
|
|
119
119
|
* A synthetic/agent sentinel identity is rejected upstream (catch-all boundary).
|
|
120
120
|
* - Q6 SECURITY FLOOR: before ANY destructive op (discard, branch -D) the SERVER
|
|
@@ -133,22 +133,23 @@
|
|
|
133
133
|
*/
|
|
134
134
|
import { realpath, stat } from 'node:fs/promises';
|
|
135
135
|
import path from 'node:path';
|
|
136
|
-
import { resolveUsername } from '@neuralis/package-system/contracts';
|
|
136
|
+
import { resolveUsername, sourcePlane } from '@neuralis/package-system/contracts';
|
|
137
137
|
import { hasFeature } from '@neuralis/package-system/access';
|
|
138
138
|
import { resolvePermissions } from '../domain/permissionResolver.js';
|
|
139
|
-
import {
|
|
139
|
+
import { HostConnector } from '../connectors/HostConnector.js';
|
|
140
140
|
import { getSourceGitStatus, getSourceGitLogStats, getSourceBranches, getSourceGitDiff, getSourceGitLog, getSourceGitShow, getSourceGitCommitFiles, getSourceGitOutgoing, getSourceGitSummary, discoverSourceRepoRoots, resolveGitCanWrite, filterVisibleRepoNames, invalidateSourceGitCache, } from '../git/sourceGitControl.js';
|
|
141
141
|
import { countUntrackedAdditions } from '../git/untrackedStats.js';
|
|
142
|
+
import { childPlace } from '../git/gitPlace.js';
|
|
142
143
|
import { getBrainConfigNumber } from '../config/env.js';
|
|
143
|
-
import { GitRunner, GitArgumentError, GitOutsideSourceError, GitUnavailableError, } from '../git/GitRunner.js';
|
|
144
|
+
import { GitRunner, GitArgumentError, GitDeviceUnavailableError, GitOutsideSourceError, GitUnavailableError, } from '../git/GitRunner.js';
|
|
144
145
|
import { resolveGitToken } from '../git/tokenResolve.js';
|
|
145
146
|
import { extractCtx, ensureAccess, getServices, toScopeAccessCtx, publishInAppGitBadge, } from './helpers.js';
|
|
146
147
|
import { assertUriCapability, uriPolicyDeniedResponse, UriPolicyDeniedError, } from './helpers/uriGuard.js';
|
|
147
148
|
export const pattern = 'git/*';
|
|
148
|
-
export const feature = '
|
|
149
|
+
export const feature = 'files.read';
|
|
149
150
|
// The mutating op-class escalates in-handler (`denyIfNoWrite`, and the per-row
|
|
150
151
|
// `canWrite` projection); the module gate only guarantees the read tier.
|
|
151
|
-
export const escalations = ['
|
|
152
|
+
export const escalations = ['files.write'];
|
|
152
153
|
/** Max change-set entries returned across all three buckets (response size cap). */
|
|
153
154
|
const MAX_STATUS_ENTRIES = 1000;
|
|
154
155
|
/**
|
|
@@ -197,7 +198,17 @@ const MAX_SUBJECT_LEN = 500;
|
|
|
197
198
|
* card shows the message, not a full diff). */
|
|
198
199
|
const MAX_BODY_LEN = 4000;
|
|
199
200
|
export const GET = async (req, raw) => {
|
|
200
|
-
|
|
201
|
+
try {
|
|
202
|
+
return await dispatchRead(req, raw);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
// A device that refused a read is named — never a clean repo's empty answer.
|
|
206
|
+
if (error instanceof GitDeviceUnavailableError)
|
|
207
|
+
return gitDeviceUnavailable(error.reason);
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
async function dispatchRead(req, state) {
|
|
201
212
|
const [sub] = req.path;
|
|
202
213
|
if (sub === 'status')
|
|
203
214
|
return handleGitStatus(req, state);
|
|
@@ -218,11 +229,11 @@ export const GET = async (req, raw) => {
|
|
|
218
229
|
if (sub === 'repos')
|
|
219
230
|
return handleGitRepos(req, state);
|
|
220
231
|
return { status: 404, body: { error: `Unknown git route: ${sub ?? ''}` } };
|
|
221
|
-
}
|
|
232
|
+
}
|
|
222
233
|
/**
|
|
223
234
|
* WRITE routes (G2.3) — the HUMAN face of the git engine. Dispatched on
|
|
224
|
-
* `req.path[0]`. Each handler additionally gates `
|
|
225
|
-
* `feature` only guarantees `
|
|
235
|
+
* `req.path[0]`. Each handler additionally gates `files.write` (the file-level
|
|
236
|
+
* `feature` only guarantees `files.read`) and the uri-policy `write` capability,
|
|
226
237
|
* auto-snapshots before a destructive op (Q6), and publishes the IN-APP badge
|
|
227
238
|
* with the actor's real identity (N3) on success.
|
|
228
239
|
*/
|
|
@@ -278,10 +289,11 @@ async function resolveGitSource(req, state, opts = {}) {
|
|
|
278
289
|
if (!config) {
|
|
279
290
|
return { ok: false, response: { status: 404, body: { error: `Source not found: ${source}` } } };
|
|
280
291
|
}
|
|
281
|
-
|
|
292
|
+
const plane = config.connection ? sourcePlane(config.connection.kind, config.connection.config) : 'connector';
|
|
293
|
+
if (plane !== 'local' && plane !== 'device') {
|
|
282
294
|
return {
|
|
283
295
|
ok: false,
|
|
284
|
-
response: { status: 400, body: { error: `Source "${config.source}" is not a git-capable
|
|
296
|
+
response: { status: 400, body: { error: `Source "${config.source}" is not a git-capable folder` } },
|
|
285
297
|
};
|
|
286
298
|
}
|
|
287
299
|
// uri-policy gate (defense-in-depth beyond the scope check). `capability` is
|
|
@@ -313,13 +325,11 @@ async function resolveGitSource(req, state, opts = {}) {
|
|
|
313
325
|
return { ok: false, response: uriPolicyDeniedResponse(error) };
|
|
314
326
|
throw error;
|
|
315
327
|
}
|
|
316
|
-
const
|
|
317
|
-
if (!
|
|
318
|
-
return
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
};
|
|
322
|
-
}
|
|
328
|
+
const found = await resolveGitPlace(services, state, ctx, config.source);
|
|
329
|
+
if (!found.ok)
|
|
330
|
+
return found;
|
|
331
|
+
const sourcePlace = found.place;
|
|
332
|
+
const root = sourcePlace.root;
|
|
323
333
|
const base = {
|
|
324
334
|
ok: true,
|
|
325
335
|
source: config.source,
|
|
@@ -330,15 +340,75 @@ async function resolveGitSource(req, state, opts = {}) {
|
|
|
330
340
|
createdBy: config.createdBy,
|
|
331
341
|
};
|
|
332
342
|
if (repo === undefined)
|
|
333
|
-
return { ...base, root };
|
|
343
|
+
return { ...base, root, place: sourcePlace };
|
|
334
344
|
// E1 braces — membership + fresh re-check on the nested root. ONE constant
|
|
335
345
|
// 404 for non-member / non-repo / symlink-escape / raced-away children —
|
|
336
346
|
// the response never reveals whether the child exists.
|
|
337
|
-
const
|
|
338
|
-
if (!
|
|
347
|
+
const nested = await resolveNestedRepoPlace(sourcePlace, repo);
|
|
348
|
+
if (!nested) {
|
|
339
349
|
return { ok: false, response: { status: 404, body: { error: `Repository not found: ${repo}` } } };
|
|
340
350
|
}
|
|
341
|
-
return { ...base, root:
|
|
351
|
+
return { ...base, root: nested.root, place: nested };
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* The place git runs for a source: the in-process folder's root, or a device
|
|
355
|
+
* folder behind the ONE device gate's `exec` arm (D7′ + the owner's per-host
|
|
356
|
+
* folders and "others" level, D-L1). Every git call on a device is a program run
|
|
357
|
+
* there, so the gate answers BEFORE anything spawns: the device's owner holding
|
|
358
|
+
* `device.own` always; anyone else only with `exec.device` on a device whose
|
|
359
|
+
* live verdict is `sandboxed` — and even then the broker re-judges at the spawn
|
|
360
|
+
* (`requireConfinement` + `actor:'other'` against the owner's per-folder
|
|
361
|
+
* "others" setting). The refusal is the gate's FIXED code and sentence.
|
|
362
|
+
*/
|
|
363
|
+
async function resolveGitPlace(services, state, ctx, source) {
|
|
364
|
+
const connector = services.infra?.connectors?.resolveBySource?.(source) ?? null;
|
|
365
|
+
if (!(connector instanceof HostConnector)) {
|
|
366
|
+
return { ok: false, response: { status: 400, body: { error: `Source "${source}" has no resolvable checkout root` } } };
|
|
367
|
+
}
|
|
368
|
+
const device = connector.device;
|
|
369
|
+
if (device === undefined)
|
|
370
|
+
return { ok: true, place: { root: connector.root } };
|
|
371
|
+
const devices = state.ctx.packages.getPackageApi('@neuralis/agent-core')?.devices;
|
|
372
|
+
const verdict = devices
|
|
373
|
+
? await devices.mayRun({ userId: ctx.userId, grantedFeatures: ctx.grantedFeatures ?? [] }, device)
|
|
374
|
+
: { ok: false, code: 'devices_unavailable', reason: 'The device plane is not available on this server.' };
|
|
375
|
+
if (!verdict.ok) {
|
|
376
|
+
return { ok: false, response: { status: 403, body: { ok: false, code: verdict.code, error: verdict.reason } } };
|
|
377
|
+
}
|
|
378
|
+
// A device whose plane is down answers its live reason (the listing's own
|
|
379
|
+
// probe, cached per device) before git is asked — never an empty repo.
|
|
380
|
+
const probe = await connector.probeConnection();
|
|
381
|
+
if (!probe.connected)
|
|
382
|
+
return { ok: false, response: gitDeviceUnavailable(probe.reason ?? 'store_unreachable') };
|
|
383
|
+
return { ok: true, place: { root: connector.root, device: { transport: connector.transport, owner: verdict.owner } } };
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* The E1 braces on a DEVICE folder: membership in the discovered set (which
|
|
387
|
+
* admitted only real, non-symlinked child directories with a real `.git`, see
|
|
388
|
+
* `scanDeviceDepth1RepoChildren`) plus a FRESH `.git` stat on the device. A
|
|
389
|
+
* device answers inside its owner's ceiling and never follows a link out of it;
|
|
390
|
+
* the child's own `.git` redirect is clamped again by the engine's floor 1b at
|
|
391
|
+
* the first spawn.
|
|
392
|
+
*/
|
|
393
|
+
async function resolveNestedRepoPlace(sourcePlace, repo) {
|
|
394
|
+
const seat = sourcePlace.device;
|
|
395
|
+
if (!seat) {
|
|
396
|
+
const nestedRoot = await resolveNestedRepoRoot(sourcePlace.root, repo);
|
|
397
|
+
return nestedRoot ? { root: nestedRoot } : null;
|
|
398
|
+
}
|
|
399
|
+
const discovered = await discoverSourceRepoRoots({ root: sourcePlace.root, device: { transport: seat.transport } });
|
|
400
|
+
if (!discovered.some((entry) => entry.rel === repo))
|
|
401
|
+
return null;
|
|
402
|
+
const nested = childPlace(sourcePlace, repo);
|
|
403
|
+
try {
|
|
404
|
+
const p = seat.transport.flavour === 'win32' ? path.win32 : path.posix;
|
|
405
|
+
if ((await seat.transport.stat(p.join(nested.root, '.git'))) === null)
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
return { root: nested.root, device: { transport: seat.transport, owner: seat.owner } };
|
|
342
412
|
}
|
|
343
413
|
/**
|
|
344
414
|
* E1 `repo` syntactic whitelist — ONE path segment (no `/` or `\`), 1–128
|
|
@@ -434,24 +504,16 @@ async function hasNestedDotGit(nestedRoot) {
|
|
|
434
504
|
return false;
|
|
435
505
|
}
|
|
436
506
|
}
|
|
437
|
-
function resolveLocalRootForSource(services, source) {
|
|
438
|
-
const connector = services.infra?.connectors?.resolveBySource?.(source) ?? null;
|
|
439
|
-
if (connector instanceof LocalDiskConnector)
|
|
440
|
-
return connector.root;
|
|
441
|
-
return null;
|
|
442
|
-
}
|
|
443
507
|
async function handleGitStatus(req, state) {
|
|
444
508
|
const resolved = await resolveGitSource(req, state);
|
|
445
509
|
if (!resolved.ok)
|
|
446
510
|
return resolved.response;
|
|
447
|
-
const status = await getSourceGitStatus(resolved.
|
|
448
|
-
//
|
|
449
|
-
//
|
|
511
|
+
const status = await getSourceGitStatus(resolved.place);
|
|
512
|
+
// A non-repo / slow / broken checkout is a typed refusal like /git/outgoing's —
|
|
513
|
+
// an empty `ok:true` change-set reads as "No changes". The sentence is fixed:
|
|
514
|
+
// git's stderr carries host paths (invariant 9).
|
|
450
515
|
if (!status) {
|
|
451
|
-
return {
|
|
452
|
-
status: 200,
|
|
453
|
-
body: { ok: true, status: { branch: null, staged: [], unstaged: [], untracked: [] } },
|
|
454
|
-
};
|
|
516
|
+
return { status: 200, body: { ok: false, code: 'unavailable', error: 'Git status is unavailable for this folder.' } };
|
|
455
517
|
}
|
|
456
518
|
// Q1 closure: drop per-path read-denied file NAMES from every bucket BEFORE
|
|
457
519
|
// capping (a root-allowed caller must not enumerate a `<sub>/**`-denied path;
|
|
@@ -468,12 +530,15 @@ async function handleGitStatus(req, state) {
|
|
|
468
530
|
// this reads FILES, so counting one the caller may not see would be work done
|
|
469
531
|
// on their behalf on content they are not allowed to know exists.
|
|
470
532
|
// Tracked rows already carry their numbers from `getSourceGitStatus`.
|
|
471
|
-
|
|
533
|
+
// A device folder's untracked files carry no number: counting reads every
|
|
534
|
+
// file over the link (one `stat` + one range read each, ~15 ms per RPC), which
|
|
535
|
+
// the panel's refresh cannot afford — absent means "no number", never zero.
|
|
536
|
+
const untrackedBudget = resolved.place.device ? 0 : getBrainConfigNumber('brainGitUntrackedCountCap', undefined, 200);
|
|
472
537
|
if (untrackedBudget > 0 && capped.untracked.length > 0) {
|
|
473
538
|
const targets = capped.untracked.slice(0, untrackedBudget).map((c) => c.path);
|
|
474
539
|
const counts = await countUntrackedAdditions(resolved.root, targets, {
|
|
475
540
|
maxBytes: UNTRACKED_MAX_BYTES,
|
|
476
|
-
timeoutMs: getBrainConfigNumber('brainGitPanelTimeoutMs', undefined,
|
|
541
|
+
timeoutMs: getBrainConfigNumber('brainGitPanelTimeoutMs', undefined, 15000),
|
|
477
542
|
});
|
|
478
543
|
capped.untracked = capped.untracked.map((c) => {
|
|
479
544
|
const n = counts.get(c.path);
|
|
@@ -525,7 +590,7 @@ async function handleGitBranch(req, state) {
|
|
|
525
590
|
const resolved = await resolveGitSource(req, state);
|
|
526
591
|
if (!resolved.ok)
|
|
527
592
|
return resolved.response;
|
|
528
|
-
const branches = await getSourceBranches(resolved.
|
|
593
|
+
const branches = await getSourceBranches(resolved.place);
|
|
529
594
|
if (!branches) {
|
|
530
595
|
return { status: 200, body: { ok: true, branches: [] } };
|
|
531
596
|
}
|
|
@@ -596,7 +661,7 @@ async function handleGitDiff(req, state) {
|
|
|
596
661
|
// `getSourceGitDiff` passes the repo-relative path straight into
|
|
597
662
|
// `GitRunner.diff`, whose containment floor rejects any escape (→ null →
|
|
598
663
|
// empty diff here).
|
|
599
|
-
const result = await getSourceGitDiff(resolved.
|
|
664
|
+
const result = await getSourceGitDiff(resolved.place, rawPath, target);
|
|
600
665
|
if (result && !result.ok) {
|
|
601
666
|
// The engine KILLED the read past its ~4 MB accumulation bound (MINOR-6):
|
|
602
667
|
// there is no leading-hunk prefix to serve, so this is a typed refusal
|
|
@@ -651,7 +716,7 @@ async function handleGitShow(req, state) {
|
|
|
651
716
|
const resolved = await resolveGitSource(req, state, { policyPath: rawPath });
|
|
652
717
|
if (!resolved.ok)
|
|
653
718
|
return resolved.response;
|
|
654
|
-
const result = await getSourceGitShow(resolved.
|
|
719
|
+
const result = await getSourceGitShow(resolved.place, rawPath, rev);
|
|
655
720
|
if (!result) {
|
|
656
721
|
// Fail-soft: non-repo / path missing at the rev / timeout — never an error
|
|
657
722
|
// that leaks path detail.
|
|
@@ -695,7 +760,7 @@ async function handleGitLog(req, state) {
|
|
|
695
760
|
const skip = Number.isFinite(skipRaw) && skipRaw > 0 ? Math.min(skipRaw, MAX_LOG_SKIP) : 0;
|
|
696
761
|
const ref = typeof req.query?.ref === 'string' && req.query.ref ? req.query.ref : undefined;
|
|
697
762
|
// Fail-soft: an empty repo (no commits) / slow / broken checkout → `commits:[]`.
|
|
698
|
-
const result = await getSourceGitLog(resolved.
|
|
763
|
+
const result = await getSourceGitLog(resolved.place, {
|
|
699
764
|
maxCount: maxCount + 1,
|
|
700
765
|
...(skip > 0 ? { skip } : {}),
|
|
701
766
|
...(ref ? { ref } : {}),
|
|
@@ -756,7 +821,7 @@ async function handleGitLogStats(req, state) {
|
|
|
756
821
|
const skipRaw = typeof req.query?.skip === 'string' ? Number.parseInt(req.query.skip, 10) : NaN;
|
|
757
822
|
const skip = Number.isFinite(skipRaw) && skipRaw > 0 ? Math.min(skipRaw, MAX_LOG_SKIP) : 0;
|
|
758
823
|
const ref = typeof req.query?.ref === 'string' && req.query.ref ? req.query.ref : undefined;
|
|
759
|
-
const result = await getSourceGitLogStats(resolved.
|
|
824
|
+
const result = await getSourceGitLogStats(resolved.place, {
|
|
760
825
|
maxCount,
|
|
761
826
|
...(skip > 0 ? { skip } : {}),
|
|
762
827
|
...(ref ? { ref } : {}),
|
|
@@ -828,7 +893,7 @@ async function handleGitCommitFiles(req, state) {
|
|
|
828
893
|
const resolved = await resolveGitSource(req, state);
|
|
829
894
|
if (!resolved.ok)
|
|
830
895
|
return resolved.response;
|
|
831
|
-
const files = await getSourceGitCommitFiles(resolved.
|
|
896
|
+
const files = await getSourceGitCommitFiles(resolved.place, sha);
|
|
832
897
|
if (!files) {
|
|
833
898
|
return { status: 200, body: { ok: false, code: 'unavailable', sha } };
|
|
834
899
|
}
|
|
@@ -856,7 +921,7 @@ async function handleGitOutgoing(req, state) {
|
|
|
856
921
|
const resolved = await resolveGitSource(req, state);
|
|
857
922
|
if (!resolved.ok)
|
|
858
923
|
return resolved.response;
|
|
859
|
-
const outgoing = await getSourceGitOutgoing(resolved.
|
|
924
|
+
const outgoing = await getSourceGitOutgoing(resolved.place);
|
|
860
925
|
if (!outgoing) {
|
|
861
926
|
// Dialect unification (3b): a DEEP failure (non-repo / unborn HEAD / any git
|
|
862
927
|
// error → `getSourceGitOutgoing` returns null) is a typed refusal, matching
|
|
@@ -901,7 +966,7 @@ async function handleGitRepos(req, state) {
|
|
|
901
966
|
const resolved = await resolveGitSource(req, state, { ignoreRepo: true });
|
|
902
967
|
if (!resolved.ok)
|
|
903
968
|
return resolved.response;
|
|
904
|
-
const discovered = await discoverSourceRepoRoots(resolved.
|
|
969
|
+
const discovered = await discoverSourceRepoRoots(resolved.place);
|
|
905
970
|
// Q1 closure (m3): per-path READ-gate the NESTED repo DIRS — the exact call
|
|
906
971
|
// the file tree already makes (`filterVisibleRepoNames`, `${source}://<rel>/`).
|
|
907
972
|
// The root-repo `rel:''` row is source-root-gated already (a `${source}:///`
|
|
@@ -915,7 +980,7 @@ async function handleGitRepos(req, state) {
|
|
|
915
980
|
const repos = await Promise.all(visibleRepos.map(async (entry) => {
|
|
916
981
|
// Branch enrichment is the on-demand step split OFF the discovery
|
|
917
982
|
// (MINOR-5); fail-soft — any git problem just omits the branch.
|
|
918
|
-
const summary = await getSourceGitSummary(entry.
|
|
983
|
+
const summary = await getSourceGitSummary(entry.place);
|
|
919
984
|
return {
|
|
920
985
|
rel: entry.rel,
|
|
921
986
|
name: entry.name,
|
|
@@ -932,7 +997,7 @@ async function handleGitRepos(req, state) {
|
|
|
932
997
|
}
|
|
933
998
|
/**
|
|
934
999
|
* Per-repo write capability for a `/git/repos` row (MAJOR-C tail) — the SAME
|
|
935
|
-
* two-plane gate as `resolveGitCanWrite` (the caller must HOLD `
|
|
1000
|
+
* two-plane gate as `resolveGitCanWrite` (the caller must HOLD `files.write`
|
|
936
1001
|
* AND the resolved uri-policy `write` must allow), evaluated at the REPO
|
|
937
1002
|
* granularity `<slug>://<repo>/`: root-level canWrite is the wrong
|
|
938
1003
|
* granularity in both directions — a `<repo>/**` write-deny must flip that
|
|
@@ -949,7 +1014,7 @@ function resolveRepoCanWrite(resolved, rel) {
|
|
|
949
1014
|
return false;
|
|
950
1015
|
if (rel === '')
|
|
951
1016
|
return resolveGitCanWrite(permissions, source, createdBy, ctx);
|
|
952
|
-
if (!hasFeature({ grantedFeatures: ctx.grantedFeatures }, '
|
|
1017
|
+
if (!hasFeature({ grantedFeatures: ctx.grantedFeatures }, 'files.write'))
|
|
953
1018
|
return false;
|
|
954
1019
|
return (resolvePermissions(permissions, `${source}://${rel}/`, {
|
|
955
1020
|
userId: ctx.userId,
|
|
@@ -965,7 +1030,7 @@ function resolveRepoCanWrite(resolved, rel) {
|
|
|
965
1030
|
* Per-repo REMOTE-op capability (NIT-2) — the uri-policy `exec` capability at
|
|
966
1031
|
* the repo granularity `<slug>://[<rel>/]`, exactly what the remote-op route
|
|
967
1032
|
* gates on (`resolveGitSource({capability:'exec'})`). Unlike canWrite this does
|
|
968
|
-
* NOT gate `
|
|
1033
|
+
* NOT gate `files.write` (the route gates that separately) — it reflects ONLY
|
|
969
1034
|
* whether `exec` is granted, so the panel can show a precise "grant exec to
|
|
970
1035
|
* enable remote ops" affordance. PURE — no git I/O.
|
|
971
1036
|
*/
|
|
@@ -1035,7 +1100,7 @@ function filterReadableChanges(resolved, rows) {
|
|
|
1035
1100
|
});
|
|
1036
1101
|
}
|
|
1037
1102
|
// ---------------------------------------------------------------------------
|
|
1038
|
-
// WRITE handlers (G2.3) — the HUMAN face; each gates `
|
|
1103
|
+
// WRITE handlers (G2.3) — the HUMAN face; each gates `files.write` + uri-policy
|
|
1039
1104
|
// `write`, and returns { ok:true, ... } / a structured error.
|
|
1040
1105
|
// ---------------------------------------------------------------------------
|
|
1041
1106
|
async function handleStage(req, state) {
|
|
@@ -1062,7 +1127,7 @@ async function handleStage(req, state) {
|
|
|
1062
1127
|
}
|
|
1063
1128
|
let runner;
|
|
1064
1129
|
try {
|
|
1065
|
-
runner = await GitRunner.create({
|
|
1130
|
+
runner = await GitRunner.create({ root: resolved.place });
|
|
1066
1131
|
}
|
|
1067
1132
|
catch (error) {
|
|
1068
1133
|
return gitWriteError(error, resolved.root);
|
|
@@ -1100,7 +1165,7 @@ async function handleUnstage(req, state) {
|
|
|
1100
1165
|
}
|
|
1101
1166
|
let runner;
|
|
1102
1167
|
try {
|
|
1103
|
-
runner = await GitRunner.create({
|
|
1168
|
+
runner = await GitRunner.create({ root: resolved.place });
|
|
1104
1169
|
}
|
|
1105
1170
|
catch (error) {
|
|
1106
1171
|
return gitWriteError(error, resolved.root);
|
|
@@ -1134,7 +1199,7 @@ async function handleDiscard(req, state) {
|
|
|
1134
1199
|
return pathDenied;
|
|
1135
1200
|
let runner;
|
|
1136
1201
|
try {
|
|
1137
|
-
runner = await GitRunner.create({
|
|
1202
|
+
runner = await GitRunner.create({ root: resolved.place });
|
|
1138
1203
|
}
|
|
1139
1204
|
catch (error) {
|
|
1140
1205
|
return gitWriteError(error, resolved.root);
|
|
@@ -1176,7 +1241,7 @@ async function handleCommit(req, state) {
|
|
|
1176
1241
|
return resolved.response;
|
|
1177
1242
|
let runner;
|
|
1178
1243
|
try {
|
|
1179
|
-
runner = await GitRunner.create({
|
|
1244
|
+
runner = await GitRunner.create({ root: resolved.place });
|
|
1180
1245
|
}
|
|
1181
1246
|
catch (error) {
|
|
1182
1247
|
return gitWriteError(error, resolved.root);
|
|
@@ -1209,7 +1274,7 @@ async function handleBranchWrite(req, state) {
|
|
|
1209
1274
|
return resolved.response;
|
|
1210
1275
|
let runner;
|
|
1211
1276
|
try {
|
|
1212
|
-
runner = await GitRunner.create({
|
|
1277
|
+
runner = await GitRunner.create({ root: resolved.place });
|
|
1213
1278
|
}
|
|
1214
1279
|
catch (error) {
|
|
1215
1280
|
return gitWriteError(error, resolved.root);
|
|
@@ -1263,7 +1328,7 @@ async function handleCheckout(req, state) {
|
|
|
1263
1328
|
return resolved.response;
|
|
1264
1329
|
let runner;
|
|
1265
1330
|
try {
|
|
1266
|
-
runner = await GitRunner.create({
|
|
1331
|
+
runner = await GitRunner.create({ root: resolved.place });
|
|
1267
1332
|
}
|
|
1268
1333
|
catch (error) {
|
|
1269
1334
|
return gitWriteError(error, resolved.root);
|
|
@@ -1287,7 +1352,7 @@ async function handleCheckout(req, state) {
|
|
|
1287
1352
|
* - `{kind:'branch', ref:<anchor>, name}` → `git branch <name> <anchor>`
|
|
1288
1353
|
* (recreate a deleted branch; NON-force → git 409s on an existing name, never
|
|
1289
1354
|
* clobbers a live branch).
|
|
1290
|
-
* Gated EXACTLY like the other writes: `
|
|
1355
|
+
* Gated EXACTLY like the other writes: `files.write` + uri-policy `write` (root) —
|
|
1291
1356
|
* NO new capability, NO exec, NO per-path gate (stash-apply is whole-WT), NO
|
|
1292
1357
|
* `PendingInteraction` (a human-consent WRITE route). The caller-echoed
|
|
1293
1358
|
* `ref`/`name` are `#guardRef`-validated at the GitRunner boundary (m4). A
|
|
@@ -1318,7 +1383,7 @@ async function handleRestore(req, state) {
|
|
|
1318
1383
|
return resolved.response;
|
|
1319
1384
|
let runner;
|
|
1320
1385
|
try {
|
|
1321
|
-
runner = await GitRunner.create({
|
|
1386
|
+
runner = await GitRunner.create({ root: resolved.place });
|
|
1322
1387
|
}
|
|
1323
1388
|
catch (error) {
|
|
1324
1389
|
return gitWriteError(error, resolved.root);
|
|
@@ -1351,7 +1416,7 @@ const GIT_REMOTE = 'origin';
|
|
|
1351
1416
|
* POST /git/fetch | /git/pull | /git/push — the network-reaching git ops
|
|
1352
1417
|
* (G4 keyring-kestrel Slice 2). The security-heaviest git surface:
|
|
1353
1418
|
*
|
|
1354
|
-
* 1. `
|
|
1419
|
+
* 1. `files.write` (feature) + uri-policy `exec` on the SOURCE ROOT
|
|
1355
1420
|
* (`capability:'exec'`) — remote ops are exec-class, deny-by-default
|
|
1356
1421
|
* (`exec:false` out of the box → 403 with the D4 grant-exec affordance).
|
|
1357
1422
|
* 2. Derive the remote HOST server-side from the checkout's `origin` URL
|
|
@@ -1382,9 +1447,16 @@ async function handleRemoteOp(req, state, op) {
|
|
|
1382
1447
|
});
|
|
1383
1448
|
if (!resolved.ok)
|
|
1384
1449
|
return resolved.response;
|
|
1450
|
+
// A paired device's folder: its OWNER runs fetch/pull/push there with their own
|
|
1451
|
+
// git credentials on their own machine — no binding or server token is read,
|
|
1452
|
+
// and nothing of this server's crosses the link. Anyone else is refused: the
|
|
1453
|
+
// only credential on that machine is the owner's, and using it for another
|
|
1454
|
+
// person would be borrowing it.
|
|
1455
|
+
if (resolved.place.device)
|
|
1456
|
+
return handleDeviceRemoteOp(resolved, op);
|
|
1385
1457
|
let runner;
|
|
1386
1458
|
try {
|
|
1387
|
-
runner = await GitRunner.create({
|
|
1459
|
+
runner = await GitRunner.create({ root: resolved.place });
|
|
1388
1460
|
}
|
|
1389
1461
|
catch (error) {
|
|
1390
1462
|
return gitWriteError(error, resolved.root);
|
|
@@ -1414,7 +1486,7 @@ async function handleRemoteOp(req, state, op) {
|
|
|
1414
1486
|
projectId: ctx.projectId,
|
|
1415
1487
|
...(ctx.agentId ? { agentId: ctx.agentId } : {}),
|
|
1416
1488
|
callerId: '@neuralis/brain-core',
|
|
1417
|
-
callerTrust: '
|
|
1489
|
+
callerTrust: 'native',
|
|
1418
1490
|
...(ctx.grantedFeatures ? { grantedFeatures: ctx.grantedFeatures } : {}),
|
|
1419
1491
|
};
|
|
1420
1492
|
const agentCore = state.ctx.packages.getPackageApi('@neuralis/agent-core', session);
|
|
@@ -1455,12 +1527,58 @@ async function handleRemoteOp(req, state, op) {
|
|
|
1455
1527
|
catch (error) {
|
|
1456
1528
|
return gitWriteError(error, resolved.root);
|
|
1457
1529
|
}
|
|
1458
|
-
invalidateSourceGitCache(resolved.
|
|
1530
|
+
invalidateSourceGitCache(resolved.place);
|
|
1459
1531
|
publishInAppGitBadge(resolved.services, resolved.ctx, resolved.source);
|
|
1460
1532
|
// fetch/pull re-read status so the UI shows TRUE divergence immediately.
|
|
1461
1533
|
const body = { ok: true, op, host };
|
|
1462
1534
|
if (op !== 'push') {
|
|
1463
|
-
|
|
1535
|
+
// The op landed: a status the device can no longer answer only leaves it out.
|
|
1536
|
+
const status = await getSourceGitStatus(resolved.place).catch(() => null);
|
|
1537
|
+
if (status)
|
|
1538
|
+
body.status = capStatus(status);
|
|
1539
|
+
}
|
|
1540
|
+
return { status: 200, body };
|
|
1541
|
+
}
|
|
1542
|
+
/** fetch / pull / push on a device folder — its owner's own git, on that device. */
|
|
1543
|
+
async function handleDeviceRemoteOp(resolved, op) {
|
|
1544
|
+
if (resolved.place.device?.owner !== true) {
|
|
1545
|
+
return {
|
|
1546
|
+
status: 409,
|
|
1547
|
+
body: { ok: false, code: 'git_remote_owner_only', error: "Only the device's owner fetches, pulls and pushes a folder on it — with their own git credentials there." },
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
let runner;
|
|
1551
|
+
let host;
|
|
1552
|
+
try {
|
|
1553
|
+
runner = await GitRunner.create({ root: resolved.place });
|
|
1554
|
+
host = await runner.remoteHost(GIT_REMOTE, { push: op === 'push' });
|
|
1555
|
+
}
|
|
1556
|
+
catch (error) {
|
|
1557
|
+
return gitWriteError(error, resolved.root);
|
|
1558
|
+
}
|
|
1559
|
+
if (!host) {
|
|
1560
|
+
return {
|
|
1561
|
+
status: 409,
|
|
1562
|
+
body: { ok: false, code: 'no_https_remote', error: 'This checkout has no HTTPS remote configured (SSH remotes are not supported).' },
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
try {
|
|
1566
|
+
if (op === 'fetch')
|
|
1567
|
+
await runner.fetch({ remote: GIT_REMOTE });
|
|
1568
|
+
else if (op === 'push')
|
|
1569
|
+
await runner.push({ remote: GIT_REMOTE });
|
|
1570
|
+
else
|
|
1571
|
+
await runner.pull({ remote: GIT_REMOTE });
|
|
1572
|
+
}
|
|
1573
|
+
catch (error) {
|
|
1574
|
+
return gitWriteError(error, resolved.root);
|
|
1575
|
+
}
|
|
1576
|
+
invalidateSourceGitCache(resolved.place);
|
|
1577
|
+
publishInAppGitBadge(resolved.services, resolved.ctx, resolved.source);
|
|
1578
|
+
const body = { ok: true, op, host };
|
|
1579
|
+
if (op !== 'push') {
|
|
1580
|
+
// The op landed: a status the device can no longer answer only leaves it out.
|
|
1581
|
+
const status = await getSourceGitStatus(resolved.place).catch(() => null);
|
|
1464
1582
|
if (status)
|
|
1465
1583
|
body.status = capStatus(status);
|
|
1466
1584
|
}
|
|
@@ -1470,17 +1588,17 @@ async function handleRemoteOp(req, state, op) {
|
|
|
1470
1588
|
// WRITE helpers
|
|
1471
1589
|
// ---------------------------------------------------------------------------
|
|
1472
1590
|
/**
|
|
1473
|
-
* The `
|
|
1591
|
+
* The `files.write` feature gate for every WRITE route. Checked FIRST (before the
|
|
1474
1592
|
* source is resolved) so a write-lacking caller gets a UNIFORM 403 regardless of
|
|
1475
1593
|
* whether the source exists — no existence inference. The file-level `feature`
|
|
1476
|
-
* export only guarantees `
|
|
1594
|
+
* export only guarantees `files.read`; the mutation op-class needs `files.write`.
|
|
1477
1595
|
*/
|
|
1478
1596
|
function denyIfNoWrite(req) {
|
|
1479
|
-
if (hasFeature(req.session, '
|
|
1597
|
+
if (hasFeature(req.session, 'files.write'))
|
|
1480
1598
|
return null;
|
|
1481
1599
|
return {
|
|
1482
1600
|
status: 403,
|
|
1483
|
-
body: { error: "Forbidden: missing feature '
|
|
1601
|
+
body: { error: "Forbidden: missing feature 'files.write'", code: 'feature_denied', feature: 'files.write' },
|
|
1484
1602
|
};
|
|
1485
1603
|
}
|
|
1486
1604
|
/**
|
|
@@ -1544,7 +1662,7 @@ async function guardWritePaths(resolved, paths) {
|
|
|
1544
1662
|
* SOURCE-scoped — an in-source over-refresh is fine.
|
|
1545
1663
|
*/
|
|
1546
1664
|
function finishWrite(resolved, extra) {
|
|
1547
|
-
invalidateSourceGitCache(resolved.
|
|
1665
|
+
invalidateSourceGitCache(resolved.place);
|
|
1548
1666
|
publishInAppGitBadge(resolved.services, resolved.ctx, resolved.source);
|
|
1549
1667
|
return { status: 200, body: { ok: true, ...extra } };
|
|
1550
1668
|
}
|
|
@@ -1556,6 +1674,8 @@ function finishWrite(resolved, extra) {
|
|
|
1556
1674
|
* unmerged branch delete, …) → 409 with git's own (repo-relative) message.
|
|
1557
1675
|
*/
|
|
1558
1676
|
function gitWriteError(error, root) {
|
|
1677
|
+
if (error instanceof GitDeviceUnavailableError)
|
|
1678
|
+
return gitDeviceUnavailable(error.reason);
|
|
1559
1679
|
if (error instanceof GitArgumentError || error instanceof GitOutsideSourceError) {
|
|
1560
1680
|
return { status: 400, body: { ok: false, code: error.code, error: scrubHostPath(error.message, root) } };
|
|
1561
1681
|
}
|
|
@@ -1566,6 +1686,15 @@ function gitWriteError(error, root) {
|
|
|
1566
1686
|
const message = error instanceof Error ? error.message : 'git operation failed';
|
|
1567
1687
|
return { status: 409, body: { ok: false, code, error: scrubHostPath(message, root) } };
|
|
1568
1688
|
}
|
|
1689
|
+
/**
|
|
1690
|
+
* git could not run on a paired device — its plane is down (`devices_disabled`,
|
|
1691
|
+
* `runtime_offline`, `broker_outdated`, …) or it refused the spawn
|
|
1692
|
+
* (`others_denied`, …). ONE answer for reads and writes: the fixed code, never
|
|
1693
|
+
* the broker's text.
|
|
1694
|
+
*/
|
|
1695
|
+
function gitDeviceUnavailable(reason) {
|
|
1696
|
+
return { status: 409, body: { ok: false, code: 'git_device_unavailable', reason, error: `git could not run on the device (${reason})` } };
|
|
1697
|
+
}
|
|
1569
1698
|
/** Replace any leaked container-root prefix in a git message with `<repo>`. */
|
|
1570
1699
|
function scrubHostPath(message, root) {
|
|
1571
1700
|
return root ? message.split(root).join('<repo>') : message;
|