@skaile/workspaces 0.52.0 → 1.0.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/CHANGELOG.md +20 -0
- package/dist/{asset-feeds-OEGJ6NOF.js → asset-feeds-677S5NX7.js} +7 -7
- package/dist/{asset-feeds-OEGJ6NOF.js.map → asset-feeds-677S5NX7.js.map} +1 -1
- package/dist/asset-manager/index.js +5 -5
- package/dist/asset-manager/installer.js +4 -4
- package/dist/{chunk-6K4RVXAM.js → chunk-43JDPQJ4.js} +3 -3
- package/dist/{chunk-6K4RVXAM.js.map → chunk-43JDPQJ4.js.map} +1 -1
- package/dist/{chunk-EOINYN5T.js → chunk-DW5MIJK3.js} +3 -3
- package/dist/{chunk-EOINYN5T.js.map → chunk-DW5MIJK3.js.map} +1 -1
- package/dist/{chunk-VE2UIJ7Y.js → chunk-EXNTTJGP.js} +3 -43
- package/dist/chunk-EXNTTJGP.js.map +1 -0
- package/dist/{chunk-7ZWLZ5H3.js → chunk-FLRHP3UC.js} +3 -3
- package/dist/{chunk-7ZWLZ5H3.js.map → chunk-FLRHP3UC.js.map} +1 -1
- package/dist/{chunk-MGB657EG.js → chunk-K5JIAHDV.js} +16 -7
- package/dist/chunk-K5JIAHDV.js.map +1 -0
- package/dist/{chunk-3IHBGDCG.js → chunk-KSISPZV3.js} +2 -2
- package/dist/{chunk-3IHBGDCG.js.map → chunk-KSISPZV3.js.map} +1 -1
- package/dist/{chunk-WD2NRI3T.js → chunk-PLQVAEM2.js} +2 -2
- package/dist/{chunk-WD2NRI3T.js.map → chunk-PLQVAEM2.js.map} +1 -1
- package/dist/{chunk-TCNYSHPM.js → chunk-SFVEHSCI.js} +4 -4
- package/dist/{chunk-TCNYSHPM.js.map → chunk-SFVEHSCI.js.map} +1 -1
- package/dist/{chunk-QLLFCQU5.js → chunk-WSQ5J7CE.js} +403 -1469
- package/dist/chunk-WSQ5J7CE.js.map +1 -0
- package/dist/{chunk-BXC5547R.js → chunk-XD7GYMAU.js} +3 -3
- package/dist/{chunk-BXC5547R.js.map → chunk-XD7GYMAU.js.map} +1 -1
- package/dist/cli/index.js +30 -32
- package/dist/cli/index.js.map +1 -1
- package/dist/connectors/config.js +4 -4
- package/dist/connectors/index.js +5 -7
- package/dist/connectors/src/config.d.ts +1 -74
- package/dist/connectors/src/config.d.ts.map +1 -1
- package/dist/connectors/src/connector-manager.d.ts +13 -137
- package/dist/connectors/src/connector-manager.d.ts.map +1 -1
- package/dist/connectors/src/connector-types.d.ts +3 -25
- package/dist/connectors/src/connector-types.d.ts.map +1 -1
- package/dist/connectors/src/fleet-utils.d.ts +2 -30
- package/dist/connectors/src/fleet-utils.d.ts.map +1 -1
- package/dist/connectors/src/fs-utils.d.ts +1 -2
- package/dist/connectors/src/fs-utils.d.ts.map +1 -1
- package/dist/connectors/src/index.d.ts +2 -12
- package/dist/connectors/src/index.d.ts.map +1 -1
- package/dist/connectors/src/watcher.d.ts +1 -5
- package/dist/connectors/src/watcher.d.ts.map +1 -1
- package/dist/core/index.js +3 -3
- package/dist/deploy/index.js +3 -3
- package/dist/{ensure-sources-GJOXWVWO.js → ensure-sources-OCMHK7LT.js} +5 -5
- package/dist/{ensure-sources-GJOXWVWO.js.map → ensure-sources-OCMHK7LT.js.map} +1 -1
- package/dist/factory-assets/connectors/deploy.js +5 -7
- package/dist/factory-assets/connectors/devserver.js +5 -7
- package/dist/factory-assets/connectors/flow/adapter.js +5 -7
- package/dist/factory-assets/connectors/flow/engine.js +3 -3
- package/dist/factory-assets/connectors/flow/run-flow.js +6 -8
- package/dist/factory-assets/connectors/flow.js +5 -7
- package/dist/factory-assets/connectors/git.js +5 -7
- package/dist/factory-assets/connectors/gmail.js +5 -7
- package/dist/factory-assets/connectors/googledrive/CONNECTOR.md +14 -128
- package/dist/factory-assets/connectors/googledrive/driver.d.ts +6 -13
- package/dist/factory-assets/connectors/googledrive/driver.d.ts.map +1 -1
- package/dist/factory-assets/connectors/googledrive.js +5 -7
- package/dist/factory-assets/connectors/local.js +5 -7
- package/dist/factory-assets/connectors/mattermost.js +5 -7
- package/dist/factory-assets/connectors/memory.js +5 -7
- package/dist/factory-assets/connectors/minio.js +5 -7
- package/dist/factory-assets/connectors/postgres.js +5 -7
- package/dist/factory-assets/connectors/s3.js +5 -7
- package/dist/factory-assets/connectors/sharepoint/CONNECTOR.md +13 -159
- package/dist/factory-assets/connectors/sharepoint/driver.d.ts +6 -12
- package/dist/factory-assets/connectors/sharepoint/driver.d.ts.map +1 -1
- package/dist/factory-assets/connectors/sharepoint.js +5 -7
- package/dist/factory-assets/connectors/sqlite.js +5 -7
- package/dist/factory-assets/connectors/static-server.js +5 -7
- package/dist/factory-assets/connectors/tunnel.js +5 -7
- package/dist/factory-assets/connectors/webdav/CONNECTOR.md +13 -118
- package/dist/factory-assets/connectors/webdav/driver.d.ts +6 -15
- package/dist/factory-assets/connectors/webdav/driver.d.ts.map +1 -1
- package/dist/factory-assets/connectors/webdav.js +5 -7
- package/dist/factory-assets/connectors/xstate-store.js +5 -7
- package/dist/factory-assets/connectors/xstate.js +5 -7
- package/dist/{flows-AGPIE6JM.js → flows-36PZESON.js} +4 -4
- package/dist/{flows-AGPIE6JM.js.map → flows-36PZESON.js.map} +1 -1
- package/dist/library/index.js +4 -4
- package/dist/library/install.js +3 -3
- package/dist/{open-library-ERKRGH4B.js → open-library-WRFYABFP.js} +7 -7
- package/dist/{open-library-ERKRGH4B.js.map → open-library-WRFYABFP.js.map} +1 -1
- package/dist/{paths-D6RUQ522.js → paths-EUNVMONC.js} +5 -5
- package/dist/{paths-D6RUQ522.js.map → paths-EUNVMONC.js.map} +1 -1
- package/dist/{plugin-store-SGNJTBYS.js → plugin-store-35KIMX6W.js} +5 -5
- package/dist/{plugin-store-SGNJTBYS.js.map → plugin-store-35KIMX6W.js.map} +1 -1
- package/dist/runner/index.js +7 -9
- package/dist/runner/src/serve.d.ts +6 -4
- package/dist/runner/src/serve.d.ts.map +1 -1
- package/dist/sdk/asset-manager.js +5 -5
- package/dist/sdk/core.js +3 -3
- package/dist/sdk/flow.js +3 -3
- package/dist/sdk/index.js +7 -9
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/runner.js +7 -9
- package/dist/{setup-IV2JFW3T.js → setup-HM6HATAC.js} +5 -5
- package/dist/{setup-IV2JFW3T.js.map → setup-HM6HATAC.js.map} +1 -1
- package/dist/{source-FGOMQMZT.js → source-EZ5Y3CKP.js} +8 -8
- package/dist/{source-FGOMQMZT.js.map → source-EZ5Y3CKP.js.map} +1 -1
- package/dist/{store-client-GJT2XQ5Z.js → store-client-BUUF7BPK.js} +5 -5
- package/dist/{store-client-GJT2XQ5Z.js.map → store-client-BUUF7BPK.js.map} +1 -1
- package/dist/tui/index.js +7 -9
- package/dist/tui/index.js.map +1 -1
- package/dist/workspace-plugin/adapters/mcp.js +3 -3
- package/dist/workspace-plugin/adapters/omp.js +3 -3
- package/dist/workspace-plugin/index.js +1 -1
- package/package.json +1 -13
- package/dist/chunk-MGB657EG.js.map +0 -1
- package/dist/chunk-OVRSNIKJ.js +0 -72
- package/dist/chunk-OVRSNIKJ.js.map +0 -1
- package/dist/chunk-QLLFCQU5.js.map +0 -1
- package/dist/chunk-RAIRWFAN.js +0 -843
- package/dist/chunk-RAIRWFAN.js.map +0 -1
- package/dist/chunk-VE2UIJ7Y.js.map +0 -1
- package/dist/connectors/rclone-config.d.ts +0 -2
- package/dist/connectors/rclone-config.js +0 -4
- package/dist/connectors/rclone-config.js.map +0 -1
- package/dist/connectors/rclone.d.ts +0 -2
- package/dist/connectors/rclone.js +0 -5
- package/dist/connectors/rclone.js.map +0 -1
- package/dist/connectors/src/fleet-health-gate.d.ts +0 -105
- package/dist/connectors/src/fleet-health-gate.d.ts.map +0 -1
- package/dist/connectors/src/mount-placeholder.d.ts +0 -131
- package/dist/connectors/src/mount-placeholder.d.ts.map +0 -1
- package/dist/connectors/src/rclone-config/googledrive.d.ts +0 -59
- package/dist/connectors/src/rclone-config/googledrive.d.ts.map +0 -1
- package/dist/connectors/src/rclone-config/index.d.ts +0 -9
- package/dist/connectors/src/rclone-config/index.d.ts.map +0 -1
- package/dist/connectors/src/rclone-config/onedrive.d.ts +0 -48
- package/dist/connectors/src/rclone-config/onedrive.d.ts.map +0 -1
- package/dist/connectors/src/rclone-config/sanitize.d.ts +0 -9
- package/dist/connectors/src/rclone-config/sanitize.d.ts.map +0 -1
- package/dist/connectors/src/rclone-config/webdav.d.ts +0 -50
- package/dist/connectors/src/rclone-config/webdav.d.ts.map +0 -1
- package/dist/connectors/src/rclone-process-manager.d.ts +0 -316
- package/dist/connectors/src/rclone-process-manager.d.ts.map +0 -1
- package/dist/connectors/src/rclone-token-refresh.d.ts +0 -84
- package/dist/connectors/src/rclone-token-refresh.d.ts.map +0 -1
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolveAuthRef, resolveMountCredentialString, resolveMountCredential } from './chunk-VE2UIJ7Y.js';
|
|
3
|
-
import { PortPool, createFsWatcher, RcloneProcessManager, ensureDirMode } from './chunk-RAIRWFAN.js';
|
|
1
|
+
import { resolveAuthRef } from './chunk-EXNTTJGP.js';
|
|
4
2
|
import { pluginRegistry } from './chunk-6E6PKKAD.js';
|
|
5
3
|
import { computeFlowStateFromSnapshots } from './chunk-SKADPHAM.js';
|
|
6
4
|
import { renderStimulusPrompt } from './chunk-DEYYRFQU.js';
|
|
@@ -8,15 +6,16 @@ import { computeStimulus } from './chunk-IFGTRL2I.js';
|
|
|
8
6
|
import { portableSpawn } from './chunk-3KPNDTMR.js';
|
|
9
7
|
import { createLogger } from './chunk-JB35MVBN.js';
|
|
10
8
|
import { existsSync, mkdirSync, renameSync, writeFileSync, readFileSync, cpSync, statSync, createReadStream, readdirSync, openSync, writeSync, closeSync, chmodSync } from 'fs';
|
|
11
|
-
import
|
|
9
|
+
import { join, dirname, resolve, sep, relative } from 'path';
|
|
12
10
|
import * as zNS from 'zod';
|
|
13
|
-
import {
|
|
11
|
+
import { execSync } from 'child_process';
|
|
14
12
|
import { createServer } from 'http';
|
|
15
13
|
import mime from 'mime';
|
|
16
|
-
import {
|
|
17
|
-
import { stat, readdir, writeFile, chmod, rm, mkdir, readFile } from 'fs/promises';
|
|
14
|
+
import { stat, readdir, mkdir, chmod } from 'fs/promises';
|
|
18
15
|
import { createRequire } from 'module';
|
|
19
16
|
import { pathToFileURL } from 'url';
|
|
17
|
+
import { createConnection } from 'net';
|
|
18
|
+
import chokidar from 'chokidar';
|
|
20
19
|
|
|
21
20
|
// connectors/src/connector-base.ts
|
|
22
21
|
var AbstractConnector = class {
|
|
@@ -2110,198 +2109,54 @@ function createConnector5() {
|
|
|
2110
2109
|
}
|
|
2111
2110
|
|
|
2112
2111
|
// factory-assets/connectors/googledrive/driver.ts
|
|
2113
|
-
var
|
|
2114
|
-
function cacheBase() {
|
|
2115
|
-
return process.env.SKAILE_RCLONE_CACHE_BASE ?? DEFAULT_CACHE_BASE;
|
|
2116
|
-
}
|
|
2117
|
-
var ALLOWED_SCOPES = /* @__PURE__ */ new Set([
|
|
2118
|
-
"drive",
|
|
2119
|
-
"drive.readonly",
|
|
2120
|
-
"drive.file",
|
|
2121
|
-
"drive.appfolder",
|
|
2122
|
-
"drive.metadata.readonly"
|
|
2123
|
-
]);
|
|
2112
|
+
var CLOUD_FLEET_REQUIRED = "Google Drive cloud connectors require host fleet-managed mounts. Add this connector id to SKAILE_FLEET_MANAGED_MOUNTS and bind the host-managed mount into the workspace container.";
|
|
2124
2113
|
var GoogleDriveConnector = class extends AbstractConnector {
|
|
2125
2114
|
name = "googledrive";
|
|
2126
|
-
rclone = new RcloneProcessManager();
|
|
2127
2115
|
filesystem = {
|
|
2128
|
-
|
|
2129
|
-
sync: (handle, options) => {
|
|
2130
|
-
return this._sync(handle, options);
|
|
2131
|
-
}
|
|
2116
|
+
sync: (_handle, options) => syncFleetManaged(options)
|
|
2132
2117
|
};
|
|
2133
2118
|
async connect(declaration, ctx) {
|
|
2134
2119
|
const targetDir = ctx.mountTarget;
|
|
2135
2120
|
if (!targetDir) {
|
|
2136
2121
|
throw new Error("GoogleDriveConnector requires ctx.mountTarget to be set by the manager");
|
|
2137
2122
|
}
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
const opts = declaration.options;
|
|
2141
|
-
this.log.info("mount called", {
|
|
2142
|
-
optionKeys: opts ? Object.keys(opts) : []
|
|
2143
|
-
});
|
|
2144
|
-
if (isFleetManaged(declaration.id)) {
|
|
2145
|
-
this.log.info("mount handled by host fleet \u2014 skipping in-container rclone spawn", {
|
|
2146
|
-
connectorId: declaration.id,
|
|
2147
|
-
targetDir
|
|
2148
|
-
});
|
|
2149
|
-
await ensureFleetMounted(targetDir, this.log);
|
|
2150
|
-
const fleetState = {
|
|
2151
|
-
rcloneHandle: null,
|
|
2152
|
-
fleetManaged: true
|
|
2153
|
-
};
|
|
2154
|
-
return this.makeHandle(declaration, fleetState, targetDir);
|
|
2155
|
-
}
|
|
2156
|
-
const scopeRaw = opts?.scope !== void 0 ? String(opts.scope) : "drive";
|
|
2157
|
-
if (!ALLOWED_SCOPES.has(scopeRaw)) {
|
|
2158
|
-
throw new Error(
|
|
2159
|
-
`Google Drive connector: invalid scope "${scopeRaw}" (allowed: ${[...ALLOWED_SCOPES].join(", ")})`
|
|
2160
|
-
);
|
|
2123
|
+
if (!isFleetManaged(declaration.id)) {
|
|
2124
|
+
throw new Error(CLOUD_FLEET_REQUIRED);
|
|
2161
2125
|
}
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
this.log
|
|
2166
|
-
);
|
|
2167
|
-
if (!auth.accessToken && !auth.serviceAccountFile) {
|
|
2168
|
-
throw new Error(
|
|
2169
|
-
"Google Drive connector: no access token or serviceAccountFile resolved. Use 'auth: backend' (platform) or 'auth: env:GDRIVE_TOKEN' (standalone)."
|
|
2170
|
-
);
|
|
2171
|
-
}
|
|
2172
|
-
const rootFolderId = opts?.rootFolderId !== void 0 ? String(opts.rootFolderId) : null;
|
|
2173
|
-
const teamDrive = opts?.teamDrive !== void 0 ? String(opts.teamDrive) : null;
|
|
2174
|
-
const basePath = String(opts?.basePath ?? "").replace(/^\/+|\/+$/g, "");
|
|
2175
|
-
const remoteName = `skaile-${declaration.id}`;
|
|
2176
|
-
const remotePath = basePath.length > 0 ? `/${basePath}` : "/";
|
|
2177
|
-
const remote = `${remoteName}:${remotePath}`;
|
|
2178
|
-
const base = cacheBase();
|
|
2179
|
-
const cacheDir = `${base}/${declaration.id}`;
|
|
2180
|
-
const vfsCacheMode = declaration.access === "read-only" ? "minimal" : "full";
|
|
2181
|
-
this.log.info("mount params resolved", {
|
|
2182
|
-
scope,
|
|
2183
|
-
remote,
|
|
2184
|
-
cacheDir,
|
|
2185
|
-
vfsCacheMode,
|
|
2186
|
-
hasRefresh: auth.refreshToken !== null,
|
|
2187
|
-
hasServiceAccount: auth.serviceAccountFile !== null,
|
|
2188
|
-
teamDrive
|
|
2189
|
-
});
|
|
2190
|
-
const rcloneConfig = renderGoogleDriveConfig({
|
|
2191
|
-
remoteName,
|
|
2192
|
-
accessToken: auth.accessToken,
|
|
2193
|
-
refreshToken: auth.refreshToken,
|
|
2194
|
-
expiresAtMs: auth.expiresAtMs,
|
|
2195
|
-
clientId: auth.clientId,
|
|
2196
|
-
clientSecret: auth.clientSecret,
|
|
2197
|
-
scope,
|
|
2198
|
-
rootFolderId,
|
|
2199
|
-
teamDrive,
|
|
2200
|
-
serviceAccountFile: auth.serviceAccountFile
|
|
2201
|
-
});
|
|
2202
|
-
await ensureDirMode(base, this.log);
|
|
2203
|
-
await ensureDirMode(cacheDir, this.log);
|
|
2204
|
-
await ensureDirMode(targetDir, this.log);
|
|
2205
|
-
const cacheMaxSize = opts?.cacheMaxSize !== void 0 ? String(opts.cacheMaxSize) : void 0;
|
|
2206
|
-
const cacheMaxAge = opts?.cacheMaxAge !== void 0 ? String(opts.cacheMaxAge) : void 0;
|
|
2207
|
-
const vfsWriteBack = opts?.vfsWriteBack !== void 0 ? String(opts.vfsWriteBack) : void 0;
|
|
2208
|
-
const rcloneHandle = await this.rclone.spawn({
|
|
2209
|
-
mountId: declaration.id,
|
|
2210
|
-
driverName: "googledrive",
|
|
2211
|
-
mountPoint: targetDir,
|
|
2212
|
-
cacheDir,
|
|
2213
|
-
rcloneConfig,
|
|
2214
|
-
remote,
|
|
2215
|
-
vfsCacheMode,
|
|
2216
|
-
...cacheMaxSize !== void 0 ? { cacheMaxSize } : {},
|
|
2217
|
-
...cacheMaxAge !== void 0 ? { cacheMaxAge } : {},
|
|
2218
|
-
...vfsWriteBack !== void 0 ? { vfsWriteBack } : {},
|
|
2219
|
-
log: this.log
|
|
2126
|
+
this.log.info("mount handled by host fleet", {
|
|
2127
|
+
connectorId: declaration.id,
|
|
2128
|
+
targetDir
|
|
2220
2129
|
});
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
// added here, switch this to the handleRef indirection or it will query a
|
|
2228
|
-
// stale handle.
|
|
2229
|
-
authoritativeSize: (relPath) => this.rclone.statSize(rcloneHandle, relPath)
|
|
2230
|
-
};
|
|
2231
|
-
return this.makeHandle(declaration, state, targetDir);
|
|
2232
|
-
}
|
|
2233
|
-
async disconnect(handle) {
|
|
2234
|
-
const s = handle.state;
|
|
2235
|
-
if (s.fleetManaged || !s.rcloneHandle) return;
|
|
2236
|
-
await this.rclone.stop(s.rcloneHandle, { graceMs: 3e4 });
|
|
2130
|
+
await ensureFleetMounted(targetDir, this.log);
|
|
2131
|
+
return this.makeHandle(
|
|
2132
|
+
declaration,
|
|
2133
|
+
{ fleetManaged: true },
|
|
2134
|
+
targetDir
|
|
2135
|
+
);
|
|
2237
2136
|
}
|
|
2238
|
-
async
|
|
2239
|
-
const s = handle.state;
|
|
2240
|
-
if (s.fleetManaged || !s.rcloneHandle) {
|
|
2241
|
-
options?.onSyncStatus?.({
|
|
2242
|
-
phase: "initial_sync_complete",
|
|
2243
|
-
totalFiles: 0,
|
|
2244
|
-
downloadedFiles: 0,
|
|
2245
|
-
stubFiles: 0,
|
|
2246
|
-
message: "fleet-managed mount \u2014 flush handled by host"
|
|
2247
|
-
});
|
|
2248
|
-
return { filesWritten: 0, filesDeleted: 0, bytesTransferred: 0 };
|
|
2249
|
-
}
|
|
2250
|
-
await this.rclone.flush(s.rcloneHandle);
|
|
2251
|
-
options?.onSyncStatus?.({
|
|
2252
|
-
phase: "initial_sync_complete",
|
|
2253
|
-
totalFiles: 0,
|
|
2254
|
-
downloadedFiles: 0,
|
|
2255
|
-
stubFiles: 0,
|
|
2256
|
-
message: "rclone flush complete"
|
|
2257
|
-
});
|
|
2258
|
-
return { filesWritten: 0, filesDeleted: 0, bytesTransferred: 0 };
|
|
2137
|
+
async disconnect(_handle) {
|
|
2259
2138
|
}
|
|
2260
2139
|
async onHibernate(handle) {
|
|
2261
|
-
|
|
2262
|
-
if (s.fleetManaged) return "flushed";
|
|
2263
|
-
await this._sync(handle);
|
|
2140
|
+
await this.filesystem.sync(handle);
|
|
2264
2141
|
return "flushed";
|
|
2265
2142
|
}
|
|
2266
2143
|
async onSessionClose(handle) {
|
|
2267
|
-
|
|
2268
|
-
if (s.fleetManaged) return "flushed";
|
|
2269
|
-
await this._sync(handle);
|
|
2144
|
+
await this.filesystem.sync(handle);
|
|
2270
2145
|
return "flushed";
|
|
2271
2146
|
}
|
|
2272
2147
|
};
|
|
2273
2148
|
function createConnector6() {
|
|
2274
2149
|
return new GoogleDriveConnector();
|
|
2275
2150
|
}
|
|
2276
|
-
function
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
};
|
|
2285
|
-
if (!raw) return empty;
|
|
2286
|
-
if (!raw.startsWith("{")) {
|
|
2287
|
-
return { ...empty, accessToken: raw };
|
|
2288
|
-
}
|
|
2289
|
-
try {
|
|
2290
|
-
const parsed = JSON.parse(raw);
|
|
2291
|
-
return {
|
|
2292
|
-
accessToken: parsed.accessToken ?? "",
|
|
2293
|
-
refreshToken: parsed.refreshToken ?? null,
|
|
2294
|
-
expiresAtMs: parsed.expiresAt ?? null,
|
|
2295
|
-
clientId: parsed.clientId ?? null,
|
|
2296
|
-
clientSecret: parsed.clientSecret ?? null,
|
|
2297
|
-
serviceAccountFile: parsed.serviceAccountFile ?? null
|
|
2298
|
-
};
|
|
2299
|
-
} catch (err) {
|
|
2300
|
-
log?.warn("auth blob starts with '{' but is not valid JSON \u2014 treating as raw token", {
|
|
2301
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2302
|
-
});
|
|
2303
|
-
return { ...empty, accessToken: raw };
|
|
2304
|
-
}
|
|
2151
|
+
function syncFleetManaged(options) {
|
|
2152
|
+
options?.onSyncStatus?.({
|
|
2153
|
+
phase: "initial_sync_complete",
|
|
2154
|
+
totalFiles: 0,
|
|
2155
|
+
downloadedFiles: 0,
|
|
2156
|
+
stubFiles: 0,
|
|
2157
|
+
message: "fleet-managed mount \u2014 flush handled by host"
|
|
2158
|
+
});
|
|
2159
|
+
return Promise.resolve({ filesWritten: 0, filesDeleted: 0, bytesTransferred: 0 });
|
|
2305
2160
|
}
|
|
2306
2161
|
var LocalConnector = class extends AbstractConnector {
|
|
2307
2162
|
name = "local";
|
|
@@ -2381,8 +2236,8 @@ var MattermostConnector = class extends AbstractConnector {
|
|
|
2381
2236
|
constructor() {
|
|
2382
2237
|
super();
|
|
2383
2238
|
this.tools = {
|
|
2384
|
-
read: (handle,
|
|
2385
|
-
write: (handle,
|
|
2239
|
+
read: (handle, path) => this._read(handle, path),
|
|
2240
|
+
write: (handle, path, content) => this._write(handle, path, content),
|
|
2386
2241
|
describeOperations: (handle) => this._describeOperations(handle),
|
|
2387
2242
|
executeOp: (handle, operation, args) => this._executeOp(handle, operation, args),
|
|
2388
2243
|
describeSkill: (handle, declaration) => this._describeSkill(handle, declaration)
|
|
@@ -2454,23 +2309,23 @@ var MattermostConnector = class extends AbstractConnector {
|
|
|
2454
2309
|
handle.status = "disconnected";
|
|
2455
2310
|
}
|
|
2456
2311
|
// ── Tool face methods (private, wired in constructor) ─────────────────────
|
|
2457
|
-
async _read(handle,
|
|
2312
|
+
async _read(handle, path) {
|
|
2458
2313
|
const state = handle.state;
|
|
2459
2314
|
try {
|
|
2460
|
-
const post = await this._apiGet(state.url, state.token, `/api/v4/posts/${
|
|
2315
|
+
const post = await this._apiGet(state.url, state.token, `/api/v4/posts/${path}`);
|
|
2461
2316
|
this._assertChannelAccess(String(post.channel_id ?? ""), state.channelFilter);
|
|
2462
2317
|
return { data: JSON.stringify(post, null, 2), contentType: "application/json" };
|
|
2463
2318
|
} catch {
|
|
2464
2319
|
return null;
|
|
2465
2320
|
}
|
|
2466
2321
|
}
|
|
2467
|
-
async _write(handle,
|
|
2322
|
+
async _write(handle, path, content) {
|
|
2468
2323
|
if (handle.access === "read-only")
|
|
2469
2324
|
throw new Error("MattermostConnector: write denied \u2014 read-only");
|
|
2470
2325
|
const state = handle.state;
|
|
2471
2326
|
const message = typeof content === "string" ? content : String(content.data ?? "");
|
|
2472
|
-
await this._apiPut(state.url, state.token, `/api/v4/posts/${
|
|
2473
|
-
this.emitChange?.({ path
|
|
2327
|
+
await this._apiPut(state.url, state.token, `/api/v4/posts/${path}`, { id: path, message });
|
|
2328
|
+
this.emitChange?.({ path, action: "edit", source: "operation" });
|
|
2474
2329
|
}
|
|
2475
2330
|
_describeSkill(handle, declaration) {
|
|
2476
2331
|
const state = handle.state;
|
|
@@ -2728,31 +2583,31 @@ var MattermostConnector = class extends AbstractConnector {
|
|
|
2728
2583
|
return typeof id === "string" && filter.has(id);
|
|
2729
2584
|
});
|
|
2730
2585
|
}
|
|
2731
|
-
async _apiGet(baseUrl, token,
|
|
2732
|
-
const res = await fetch(baseUrl +
|
|
2586
|
+
async _apiGet(baseUrl, token, path) {
|
|
2587
|
+
const res = await fetch(baseUrl + path, { headers: { Authorization: `Bearer ${token}` } });
|
|
2733
2588
|
if (!res.ok) {
|
|
2734
2589
|
const body = await res.text();
|
|
2735
|
-
this.log.warn("REST error", { method: "GET", endpoint:
|
|
2736
|
-
throw new Error(`Mattermost GET ${
|
|
2590
|
+
this.log.warn("REST error", { method: "GET", endpoint: path, status: res.status });
|
|
2591
|
+
throw new Error(`Mattermost GET ${path}: ${res.status} ${body}`);
|
|
2737
2592
|
}
|
|
2738
2593
|
return res.json();
|
|
2739
2594
|
}
|
|
2740
|
-
async _apiPost(baseUrl, token,
|
|
2741
|
-
const res = await fetch(baseUrl +
|
|
2595
|
+
async _apiPost(baseUrl, token, path, body) {
|
|
2596
|
+
const res = await fetch(baseUrl + path, {
|
|
2742
2597
|
method: "POST",
|
|
2743
2598
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
2744
2599
|
body: JSON.stringify(body)
|
|
2745
2600
|
});
|
|
2746
|
-
if (!res.ok) throw new Error(`Mattermost POST ${
|
|
2601
|
+
if (!res.ok) throw new Error(`Mattermost POST ${path}: ${res.status} ${await res.text()}`);
|
|
2747
2602
|
return res.json();
|
|
2748
2603
|
}
|
|
2749
|
-
async _apiPut(baseUrl, token,
|
|
2750
|
-
const res = await fetch(baseUrl +
|
|
2604
|
+
async _apiPut(baseUrl, token, path, body) {
|
|
2605
|
+
const res = await fetch(baseUrl + path, {
|
|
2751
2606
|
method: "PUT",
|
|
2752
2607
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
2753
2608
|
body: JSON.stringify(body)
|
|
2754
2609
|
});
|
|
2755
|
-
if (!res.ok) throw new Error(`Mattermost PUT ${
|
|
2610
|
+
if (!res.ok) throw new Error(`Mattermost PUT ${path}: ${res.status} ${await res.text()}`);
|
|
2756
2611
|
return res.json();
|
|
2757
2612
|
}
|
|
2758
2613
|
};
|
|
@@ -2767,10 +2622,10 @@ var MemoryConnector = class extends AbstractConnector {
|
|
|
2767
2622
|
constructor() {
|
|
2768
2623
|
super();
|
|
2769
2624
|
this.tools = {
|
|
2770
|
-
read: (handle,
|
|
2771
|
-
write: (handle,
|
|
2772
|
-
delete: (handle,
|
|
2773
|
-
list: (handle,
|
|
2625
|
+
read: (handle, path) => this._read(handle, path),
|
|
2626
|
+
write: (handle, path, content) => this._write(handle, path, content),
|
|
2627
|
+
delete: (handle, path) => this._delete(handle, path),
|
|
2628
|
+
list: (handle, path, options) => this._list(handle, path, options),
|
|
2774
2629
|
search: (handle, query, options) => this._search(handle, query, options),
|
|
2775
2630
|
describeOperations: (handle) => this._describeOperations(handle),
|
|
2776
2631
|
executeOp: (handle, operation, args) => this._executeOp(handle, operation, args)
|
|
@@ -2789,26 +2644,26 @@ var MemoryConnector = class extends AbstractConnector {
|
|
|
2789
2644
|
store(handle) {
|
|
2790
2645
|
return handle.state.store;
|
|
2791
2646
|
}
|
|
2792
|
-
async _read(handle,
|
|
2793
|
-
const value = this.store(handle).get(
|
|
2647
|
+
async _read(handle, path) {
|
|
2648
|
+
const value = this.store(handle).get(path);
|
|
2794
2649
|
if (value === void 0) return null;
|
|
2795
2650
|
return { data: value, contentType: "application/json" };
|
|
2796
2651
|
}
|
|
2797
|
-
async _write(handle,
|
|
2798
|
-
const isNew = !this.store(handle).has(
|
|
2652
|
+
async _write(handle, path, content) {
|
|
2653
|
+
const isNew = !this.store(handle).has(path);
|
|
2799
2654
|
this.store(handle).set(
|
|
2800
|
-
|
|
2655
|
+
path,
|
|
2801
2656
|
typeof content.data === "string" ? content.data : content.data.toString("utf-8")
|
|
2802
2657
|
);
|
|
2803
|
-
this.emitChange?.({ path
|
|
2658
|
+
this.emitChange?.({ path, action: isNew ? "create" : "edit", source: "operation" });
|
|
2804
2659
|
}
|
|
2805
|
-
async _delete(handle,
|
|
2806
|
-
const deleted = this.store(handle).delete(
|
|
2807
|
-
if (deleted) this.emitChange?.({ path
|
|
2660
|
+
async _delete(handle, path) {
|
|
2661
|
+
const deleted = this.store(handle).delete(path);
|
|
2662
|
+
if (deleted) this.emitChange?.({ path, action: "delete", source: "operation" });
|
|
2808
2663
|
return deleted;
|
|
2809
2664
|
}
|
|
2810
|
-
async _list(handle,
|
|
2811
|
-
const prefix =
|
|
2665
|
+
async _list(handle, path, options) {
|
|
2666
|
+
const prefix = path ?? "";
|
|
2812
2667
|
const entries = [];
|
|
2813
2668
|
for (const key of this.store(handle).keys()) {
|
|
2814
2669
|
if (prefix && !key.startsWith(prefix)) continue;
|
|
@@ -2921,8 +2776,8 @@ var MinIOConnector = class extends AbstractConnector {
|
|
|
2921
2776
|
constructor() {
|
|
2922
2777
|
super();
|
|
2923
2778
|
this.tools = {
|
|
2924
|
-
list: (handle,
|
|
2925
|
-
read: (handle,
|
|
2779
|
+
list: (handle, path) => this._list(handle, path),
|
|
2780
|
+
read: (handle, path) => this._read(handle, path),
|
|
2926
2781
|
describeOperations: (handle) => this._describeOperations(handle),
|
|
2927
2782
|
executeOp: (handle, operation, args) => this._executeOp(handle, operation, args),
|
|
2928
2783
|
describeSkill: (handle, declaration) => this._describeSkill(handle, declaration)
|
|
@@ -3100,10 +2955,10 @@ var MinIOConnector = class extends AbstractConnector {
|
|
|
3100
2955
|
}
|
|
3101
2956
|
return ops;
|
|
3102
2957
|
}
|
|
3103
|
-
async _list(handle,
|
|
2958
|
+
async _list(handle, path, recursive = false) {
|
|
3104
2959
|
const { client, bucket } = handle.state;
|
|
3105
2960
|
const entries = [];
|
|
3106
|
-
const stream = client.listObjects(bucket,
|
|
2961
|
+
const stream = client.listObjects(bucket, path ?? "", recursive);
|
|
3107
2962
|
return new Promise((resolve5, reject) => {
|
|
3108
2963
|
stream.on("data", (obj) => {
|
|
3109
2964
|
entries.push({
|
|
@@ -3118,10 +2973,10 @@ var MinIOConnector = class extends AbstractConnector {
|
|
|
3118
2973
|
stream.on("error", reject);
|
|
3119
2974
|
});
|
|
3120
2975
|
}
|
|
3121
|
-
async _read(handle,
|
|
2976
|
+
async _read(handle, path) {
|
|
3122
2977
|
const { client, bucket } = handle.state;
|
|
3123
2978
|
const chunks = [];
|
|
3124
|
-
const stream = await client.getObject(bucket,
|
|
2979
|
+
const stream = await client.getObject(bucket, path);
|
|
3125
2980
|
return new Promise((resolve5, reject) => {
|
|
3126
2981
|
stream.on("data", (chunk) => chunks.push(chunk));
|
|
3127
2982
|
stream.on("end", () => resolve5({ data: Buffer.concat(chunks).toString() }));
|
|
@@ -3179,8 +3034,8 @@ var PostgresConnector = class extends AbstractConnector {
|
|
|
3179
3034
|
constructor() {
|
|
3180
3035
|
super();
|
|
3181
3036
|
this.tools = {
|
|
3182
|
-
read: (handle,
|
|
3183
|
-
list: (handle,
|
|
3037
|
+
read: (handle, path) => this._read(handle, path),
|
|
3038
|
+
list: (handle, path, options) => this._list(handle, path, options),
|
|
3184
3039
|
search: (handle, query, options) => this._search(handle, query, options),
|
|
3185
3040
|
describeOperations: (handle) => this._describeOperations(handle),
|
|
3186
3041
|
executeOp: (handle, operation, args) => this._executeOp(handle, operation, args),
|
|
@@ -3268,8 +3123,8 @@ var PostgresConnector = class extends AbstractConnector {
|
|
|
3268
3123
|
return handle.state.pool;
|
|
3269
3124
|
}
|
|
3270
3125
|
// Base ops map to table/row access via path convention: <table>/<pk>
|
|
3271
|
-
async _read(handle,
|
|
3272
|
-
const parts =
|
|
3126
|
+
async _read(handle, path) {
|
|
3127
|
+
const parts = path.split("/");
|
|
3273
3128
|
if (parts.length === 1) {
|
|
3274
3129
|
const table2 = parts[0];
|
|
3275
3130
|
const result2 = await this.pool(handle).query(
|
|
@@ -3288,8 +3143,8 @@ var PostgresConnector = class extends AbstractConnector {
|
|
|
3288
3143
|
if (result.rows.length === 0) return null;
|
|
3289
3144
|
return { data: JSON.stringify(result.rows[0], null, 2), contentType: "application/json" };
|
|
3290
3145
|
}
|
|
3291
|
-
async _list(handle,
|
|
3292
|
-
if (!
|
|
3146
|
+
async _list(handle, path, options) {
|
|
3147
|
+
if (!path) {
|
|
3293
3148
|
const result2 = await this.pool(handle).query(
|
|
3294
3149
|
`SELECT table_name FROM information_schema.tables
|
|
3295
3150
|
WHERE table_schema = 'public' ORDER BY table_name`
|
|
@@ -3301,12 +3156,12 @@ var PostgresConnector = class extends AbstractConnector {
|
|
|
3301
3156
|
}));
|
|
3302
3157
|
}
|
|
3303
3158
|
const limit = options?.limit ?? 100;
|
|
3304
|
-
const result = await this.pool(handle).query(`SELECT * FROM "${quoteIdent(
|
|
3159
|
+
const result = await this.pool(handle).query(`SELECT * FROM "${quoteIdent(path)}" LIMIT $1`, [
|
|
3305
3160
|
limit
|
|
3306
3161
|
]);
|
|
3307
3162
|
return result.rows.map((row, i) => ({
|
|
3308
3163
|
name: String(row.id ?? i),
|
|
3309
|
-
path: `${
|
|
3164
|
+
path: `${path}/${row.id ?? i}`,
|
|
3310
3165
|
type: "row"
|
|
3311
3166
|
}));
|
|
3312
3167
|
}
|
|
@@ -3586,216 +3441,54 @@ function createConnector12(sdkOverride) {
|
|
|
3586
3441
|
}
|
|
3587
3442
|
|
|
3588
3443
|
// factory-assets/connectors/sharepoint/driver.ts
|
|
3589
|
-
var
|
|
3590
|
-
function cacheBase2() {
|
|
3591
|
-
return process.env.SKAILE_RCLONE_CACHE_BASE ?? DEFAULT_CACHE_BASE2;
|
|
3592
|
-
}
|
|
3444
|
+
var CLOUD_FLEET_REQUIRED2 = "SharePoint cloud connectors require host fleet-managed mounts. Add this connector id to SKAILE_FLEET_MANAGED_MOUNTS and bind the host-managed mount into the workspace container.";
|
|
3593
3445
|
var SharePointConnector = class extends AbstractConnector {
|
|
3594
3446
|
name = "sharepoint";
|
|
3595
|
-
rclone = new RcloneProcessManager();
|
|
3596
3447
|
filesystem = {
|
|
3597
|
-
|
|
3598
|
-
sync: (handle, options) => {
|
|
3599
|
-
return this._sync(handle, options);
|
|
3600
|
-
}
|
|
3448
|
+
sync: (_handle, options) => syncFleetManaged2(options)
|
|
3601
3449
|
};
|
|
3602
3450
|
async connect(declaration, ctx) {
|
|
3603
3451
|
const targetDir = ctx.mountTarget;
|
|
3604
3452
|
if (!targetDir) {
|
|
3605
3453
|
throw new Error("SharePointConnector requires ctx.mountTarget to be set by the manager");
|
|
3606
3454
|
}
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
const opts = declaration.options;
|
|
3610
|
-
this.log.info("mount called", {
|
|
3611
|
-
optionKeys: opts ? Object.keys(opts) : []
|
|
3612
|
-
});
|
|
3613
|
-
if (isFleetManaged(declaration.id)) {
|
|
3614
|
-
this.log.info("mount handled by host fleet \u2014 skipping in-container rclone spawn", {
|
|
3615
|
-
connectorId: declaration.id,
|
|
3616
|
-
targetDir
|
|
3617
|
-
});
|
|
3618
|
-
await ensureFleetMounted(targetDir, this.log);
|
|
3619
|
-
const fleetState = {
|
|
3620
|
-
handleRef: { current: null },
|
|
3621
|
-
fleetManaged: true
|
|
3622
|
-
};
|
|
3623
|
-
return this.makeHandle(declaration, fleetState, targetDir);
|
|
3624
|
-
}
|
|
3625
|
-
const driveId = String(opts?.driveId ?? "");
|
|
3626
|
-
if (!driveId) {
|
|
3627
|
-
throw new Error("SharePoint connector requires options.driveId");
|
|
3455
|
+
if (!isFleetManaged(declaration.id)) {
|
|
3456
|
+
throw new Error(CLOUD_FLEET_REQUIRED2);
|
|
3628
3457
|
}
|
|
3629
|
-
|
|
3630
|
-
if (!folderPath) {
|
|
3631
|
-
throw new Error(
|
|
3632
|
-
"SharePoint connector requires options.folderPath (rclone backend needs a path string, not just folderId)"
|
|
3633
|
-
);
|
|
3634
|
-
}
|
|
3635
|
-
const basePath = String(opts?.basePath ?? "").replace(/^\/+|\/+$/g, "");
|
|
3636
|
-
const cred = await resolveMountCredential(declaration, secrets, tokenMediator, "initial");
|
|
3637
|
-
const auth = parseAuthBlob2(cred.token, this.log);
|
|
3638
|
-
if (!auth.accessToken) {
|
|
3639
|
-
throw new Error(
|
|
3640
|
-
"SharePoint connector: no access token resolved. Use 'auth: backend' (platform) or 'auth: env:SHAREPOINT_TOKEN' (standalone)."
|
|
3641
|
-
);
|
|
3642
|
-
}
|
|
3643
|
-
const remoteName = `skaile-${declaration.id}`;
|
|
3644
|
-
const remotePath = buildRemotePath(folderPath, basePath);
|
|
3645
|
-
const remote = `${remoteName}:${remotePath}`;
|
|
3646
|
-
const base = cacheBase2();
|
|
3647
|
-
const cacheDir = `${base}/${declaration.id}`;
|
|
3648
|
-
const vfsCacheMode = declaration.access === "read-only" ? "minimal" : "full";
|
|
3649
|
-
this.log.info("mount params resolved", {
|
|
3650
|
-
driveId,
|
|
3651
|
-
remote,
|
|
3652
|
-
cacheDir,
|
|
3653
|
-
vfsCacheMode,
|
|
3654
|
-
hasRefresh: auth.refreshToken !== null
|
|
3655
|
-
});
|
|
3656
|
-
const renderConfig = (rawToken) => {
|
|
3657
|
-
const a = parseAuthBlob2(rawToken, this.log);
|
|
3658
|
-
return renderOneDriveConfig({
|
|
3659
|
-
remoteName,
|
|
3660
|
-
accessToken: a.accessToken,
|
|
3661
|
-
refreshToken: a.refreshToken,
|
|
3662
|
-
expiresAtMs: a.expiresAtMs,
|
|
3663
|
-
tenantId: a.tenantId,
|
|
3664
|
-
clientId: a.clientId,
|
|
3665
|
-
clientSecret: a.clientSecret,
|
|
3666
|
-
driveId
|
|
3667
|
-
});
|
|
3668
|
-
};
|
|
3669
|
-
const handleRef = { current: null };
|
|
3670
|
-
const refresh = declaration.auth === "backend" && tokenMediator ? createRcloneTokenRefresh({
|
|
3458
|
+
this.log.info("mount handled by host fleet", {
|
|
3671
3459
|
connectorId: declaration.id,
|
|
3672
|
-
|
|
3673
|
-
renderConfig,
|
|
3674
|
-
rclone: this.rclone,
|
|
3675
|
-
handleRef,
|
|
3676
|
-
log: (line) => this.log.info(line)
|
|
3677
|
-
}) : void 0;
|
|
3678
|
-
const rcloneConfig = renderConfig(cred.token ?? "");
|
|
3679
|
-
await ensureDirMode(base, this.log);
|
|
3680
|
-
await ensureDirMode(cacheDir, this.log);
|
|
3681
|
-
await ensureDirMode(targetDir, this.log);
|
|
3682
|
-
const cacheMaxSize = opts?.cacheMaxSize !== void 0 ? String(opts.cacheMaxSize) : void 0;
|
|
3683
|
-
const cacheMaxAge = opts?.cacheMaxAge !== void 0 ? String(opts.cacheMaxAge) : void 0;
|
|
3684
|
-
const vfsWriteBack = opts?.vfsWriteBack !== void 0 ? String(opts.vfsWriteBack) : void 0;
|
|
3685
|
-
const ignoreSizeCheck = opts?.ignoreSizeCheck === true;
|
|
3686
|
-
if (ignoreSizeCheck) {
|
|
3687
|
-
this.log.warn(
|
|
3688
|
-
"ignoreSizeCheck enabled \u2014 rclone post-transfer size/checksum verification disabled for this mount",
|
|
3689
|
-
{ connectorId: declaration.id }
|
|
3690
|
-
);
|
|
3691
|
-
}
|
|
3692
|
-
const rcloneHandle = await this.rclone.spawn({
|
|
3693
|
-
mountId: declaration.id,
|
|
3694
|
-
driverName: "sharepoint",
|
|
3695
|
-
mountPoint: targetDir,
|
|
3696
|
-
cacheDir,
|
|
3697
|
-
rcloneConfig,
|
|
3698
|
-
remote,
|
|
3699
|
-
vfsCacheMode,
|
|
3700
|
-
...cacheMaxSize !== void 0 ? { cacheMaxSize } : {},
|
|
3701
|
-
...cacheMaxAge !== void 0 ? { cacheMaxAge } : {},
|
|
3702
|
-
...vfsWriteBack !== void 0 ? { vfsWriteBack } : {},
|
|
3703
|
-
...ignoreSizeCheck ? { ignoreSizeCheck: true } : {},
|
|
3704
|
-
...refresh ? { onAuthError: () => refresh.onAuthError() } : {},
|
|
3705
|
-
log: this.log
|
|
3460
|
+
targetDir
|
|
3706
3461
|
});
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
authoritativeSize: (relPath) => {
|
|
3714
|
-
const h = handleRef.current;
|
|
3715
|
-
return h ? this.rclone.statSize(h, relPath) : Promise.resolve(null);
|
|
3716
|
-
}
|
|
3717
|
-
};
|
|
3718
|
-
return this.makeHandle(declaration, state, targetDir);
|
|
3719
|
-
}
|
|
3720
|
-
async disconnect(handle) {
|
|
3721
|
-
const s = handle.state;
|
|
3722
|
-
s.refresh?.cancel();
|
|
3723
|
-
if (s.fleetManaged || !s.handleRef.current) return;
|
|
3724
|
-
await this.rclone.stop(s.handleRef.current, { graceMs: 3e4 });
|
|
3462
|
+
await ensureFleetMounted(targetDir, this.log);
|
|
3463
|
+
return this.makeHandle(
|
|
3464
|
+
declaration,
|
|
3465
|
+
{ fleetManaged: true },
|
|
3466
|
+
targetDir
|
|
3467
|
+
);
|
|
3725
3468
|
}
|
|
3726
|
-
async
|
|
3727
|
-
const s = handle.state;
|
|
3728
|
-
if (s.fleetManaged || !s.handleRef.current) {
|
|
3729
|
-
options?.onSyncStatus?.({
|
|
3730
|
-
phase: "initial_sync_complete",
|
|
3731
|
-
totalFiles: 0,
|
|
3732
|
-
downloadedFiles: 0,
|
|
3733
|
-
stubFiles: 0,
|
|
3734
|
-
message: "fleet-managed mount \u2014 flush handled by host"
|
|
3735
|
-
});
|
|
3736
|
-
return { filesWritten: 0, filesDeleted: 0, bytesTransferred: 0 };
|
|
3737
|
-
}
|
|
3738
|
-
await this.rclone.flush(s.handleRef.current);
|
|
3739
|
-
options?.onSyncStatus?.({
|
|
3740
|
-
phase: "initial_sync_complete",
|
|
3741
|
-
totalFiles: 0,
|
|
3742
|
-
downloadedFiles: 0,
|
|
3743
|
-
stubFiles: 0,
|
|
3744
|
-
message: "rclone flush complete"
|
|
3745
|
-
});
|
|
3746
|
-
return { filesWritten: 0, filesDeleted: 0, bytesTransferred: 0 };
|
|
3469
|
+
async disconnect(_handle) {
|
|
3747
3470
|
}
|
|
3748
3471
|
async onHibernate(handle) {
|
|
3749
|
-
|
|
3750
|
-
if (s.fleetManaged) return "flushed";
|
|
3751
|
-
await this._sync(handle);
|
|
3472
|
+
await this.filesystem.sync(handle);
|
|
3752
3473
|
return "flushed";
|
|
3753
3474
|
}
|
|
3754
3475
|
async onSessionClose(handle) {
|
|
3755
|
-
|
|
3756
|
-
if (s.fleetManaged) return "flushed";
|
|
3757
|
-
await this._sync(handle);
|
|
3476
|
+
await this.filesystem.sync(handle);
|
|
3758
3477
|
return "flushed";
|
|
3759
3478
|
}
|
|
3760
3479
|
};
|
|
3761
3480
|
function createConnector13() {
|
|
3762
3481
|
return new SharePointConnector();
|
|
3763
3482
|
}
|
|
3764
|
-
function
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
};
|
|
3773
|
-
if (!raw) return empty;
|
|
3774
|
-
if (!raw.startsWith("{")) {
|
|
3775
|
-
return { ...empty, accessToken: raw };
|
|
3776
|
-
}
|
|
3777
|
-
try {
|
|
3778
|
-
const parsed = JSON.parse(raw);
|
|
3779
|
-
return {
|
|
3780
|
-
accessToken: parsed.accessToken ?? "",
|
|
3781
|
-
refreshToken: parsed.refreshToken ?? null,
|
|
3782
|
-
expiresAtMs: parsed.expiresAt ?? null,
|
|
3783
|
-
tenantId: parsed.tenantId ?? null,
|
|
3784
|
-
clientId: parsed.clientId ?? null,
|
|
3785
|
-
clientSecret: parsed.clientSecret ?? null
|
|
3786
|
-
};
|
|
3787
|
-
} catch (err) {
|
|
3788
|
-
log?.warn("auth blob starts with '{' but is not valid JSON \u2014 treating as raw token", {
|
|
3789
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3790
|
-
});
|
|
3791
|
-
return { ...empty, accessToken: raw };
|
|
3792
|
-
}
|
|
3793
|
-
}
|
|
3794
|
-
function buildRemotePath(folderPath, basePath) {
|
|
3795
|
-
const folder = folderPath.replace(/^\/+|\/+$/g, "");
|
|
3796
|
-
const base = basePath.replace(/^\/+|\/+$/g, "");
|
|
3797
|
-
const joined = [folder, base].filter((p) => p.length > 0).join("/");
|
|
3798
|
-
return joined.length > 0 ? `/${joined}` : "/";
|
|
3483
|
+
function syncFleetManaged2(options) {
|
|
3484
|
+
options?.onSyncStatus?.({
|
|
3485
|
+
phase: "initial_sync_complete",
|
|
3486
|
+
totalFiles: 0,
|
|
3487
|
+
downloadedFiles: 0,
|
|
3488
|
+
stubFiles: 0,
|
|
3489
|
+
message: "fleet-managed mount \u2014 flush handled by host"
|
|
3490
|
+
});
|
|
3491
|
+
return Promise.resolve({ filesWritten: 0, filesDeleted: 0, bytesTransferred: 0 });
|
|
3799
3492
|
}
|
|
3800
3493
|
function quoteIdent2(ident) {
|
|
3801
3494
|
return ident.replace(/"/g, '""');
|
|
@@ -3806,8 +3499,8 @@ var SQLiteConnector = class extends AbstractConnector {
|
|
|
3806
3499
|
constructor() {
|
|
3807
3500
|
super();
|
|
3808
3501
|
this.tools = {
|
|
3809
|
-
read: (handle,
|
|
3810
|
-
list: (handle,
|
|
3502
|
+
read: (handle, path) => this._read(handle, path),
|
|
3503
|
+
list: (handle, path, options) => this._list(handle, path, options),
|
|
3811
3504
|
search: (handle, query, options) => this._search(handle, query, options),
|
|
3812
3505
|
describeOperations: (handle) => this._describeOperations(handle),
|
|
3813
3506
|
executeOp: (handle, operation, args) => this._executeOp(handle, operation, args)
|
|
@@ -3891,8 +3584,8 @@ var SQLiteConnector = class extends AbstractConnector {
|
|
|
3891
3584
|
const changes = db.getRowsModified();
|
|
3892
3585
|
return changes;
|
|
3893
3586
|
}
|
|
3894
|
-
async _read(handle,
|
|
3895
|
-
const parts =
|
|
3587
|
+
async _read(handle, path) {
|
|
3588
|
+
const parts = path.split("/");
|
|
3896
3589
|
if (parts.length === 1) {
|
|
3897
3590
|
const table2 = parts[0];
|
|
3898
3591
|
const info = this.queryRows(this.db(handle), `PRAGMA table_info("${quoteIdent2(table2)}")`);
|
|
@@ -3908,8 +3601,8 @@ var SQLiteConnector = class extends AbstractConnector {
|
|
|
3908
3601
|
if (rows.length === 0) return null;
|
|
3909
3602
|
return { data: JSON.stringify(rows[0], null, 2), contentType: "application/json" };
|
|
3910
3603
|
}
|
|
3911
|
-
async _list(handle,
|
|
3912
|
-
if (!
|
|
3604
|
+
async _list(handle, path, options) {
|
|
3605
|
+
if (!path) {
|
|
3913
3606
|
const tables = this.queryRows(
|
|
3914
3607
|
this.db(handle),
|
|
3915
3608
|
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
|
@@ -3923,12 +3616,12 @@ var SQLiteConnector = class extends AbstractConnector {
|
|
|
3923
3616
|
const limit = options?.limit ?? 100;
|
|
3924
3617
|
const rows = this.queryRows(
|
|
3925
3618
|
this.db(handle),
|
|
3926
|
-
`SELECT rowid, * FROM "${quoteIdent2(
|
|
3619
|
+
`SELECT rowid, * FROM "${quoteIdent2(path)}" LIMIT ?`,
|
|
3927
3620
|
[limit]
|
|
3928
3621
|
);
|
|
3929
3622
|
return rows.map((row) => ({
|
|
3930
3623
|
name: String(row.rowid ?? row.id ?? "?"),
|
|
3931
|
-
path: `${
|
|
3624
|
+
path: `${path}/${row.rowid ?? row.id ?? "?"}`,
|
|
3932
3625
|
type: "row"
|
|
3933
3626
|
}));
|
|
3934
3627
|
}
|
|
@@ -4538,215 +4231,52 @@ async function spawnNgrokTunnel(port) {
|
|
|
4538
4231
|
function createConnector16() {
|
|
4539
4232
|
return new TunnelConnector();
|
|
4540
4233
|
}
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
}
|
|
4545
|
-
var ALLOWED_VENDORS = /* @__PURE__ */ new Set([
|
|
4546
|
-
"nextcloud",
|
|
4547
|
-
"owncloud",
|
|
4548
|
-
"sharepoint",
|
|
4549
|
-
"fastmail",
|
|
4550
|
-
"other"
|
|
4551
|
-
]);
|
|
4552
|
-
var execFileP = promisify(execFile);
|
|
4234
|
+
|
|
4235
|
+
// factory-assets/connectors/webdav/driver.ts
|
|
4236
|
+
var CLOUD_FLEET_REQUIRED3 = "WebDAV cloud connectors require host fleet-managed mounts. Add this connector id to SKAILE_FLEET_MANAGED_MOUNTS and bind the host-managed mount into the workspace container.";
|
|
4553
4237
|
var WebDAVConnector = class extends AbstractConnector {
|
|
4554
|
-
// Injectable so tests can stub the `rclone obscure` shell-out; production
|
|
4555
|
-
// wires the real binary via createConnector().
|
|
4556
|
-
constructor(obscure = obscurePassword) {
|
|
4557
|
-
super();
|
|
4558
|
-
this.obscure = obscure;
|
|
4559
|
-
}
|
|
4560
|
-
obscure;
|
|
4561
4238
|
name = "webdav";
|
|
4562
|
-
rclone = new RcloneProcessManager();
|
|
4563
4239
|
filesystem = {
|
|
4564
|
-
|
|
4565
|
-
sync: (handle, options) => {
|
|
4566
|
-
return this._sync(handle, options);
|
|
4567
|
-
}
|
|
4240
|
+
sync: (_handle, options) => syncFleetManaged3(options)
|
|
4568
4241
|
};
|
|
4569
4242
|
async connect(declaration, ctx) {
|
|
4570
4243
|
const targetDir = ctx.mountTarget;
|
|
4571
4244
|
if (!targetDir) {
|
|
4572
4245
|
throw new Error("WebDAVConnector requires ctx.mountTarget to be set by the manager");
|
|
4573
4246
|
}
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
const opts = declaration.options;
|
|
4577
|
-
this.log.info("mount called", {
|
|
4578
|
-
optionKeys: opts ? Object.keys(opts) : [],
|
|
4579
|
-
hasAuth: declaration.auth !== void 0
|
|
4580
|
-
});
|
|
4581
|
-
if (isFleetManaged(declaration.id)) {
|
|
4582
|
-
this.log.info("mount handled by host fleet \u2014 skipping in-container rclone spawn", {
|
|
4583
|
-
connectorId: declaration.id,
|
|
4584
|
-
targetDir
|
|
4585
|
-
});
|
|
4586
|
-
await ensureFleetMounted(targetDir, this.log);
|
|
4587
|
-
const fleetState = {
|
|
4588
|
-
handleRef: { current: null },
|
|
4589
|
-
fleetManaged: true
|
|
4590
|
-
};
|
|
4591
|
-
return this.makeHandle(declaration, fleetState, targetDir);
|
|
4592
|
-
}
|
|
4593
|
-
const url = String(declaration.mount?.source ?? "").trim();
|
|
4594
|
-
if (!url) {
|
|
4595
|
-
throw new Error("WebDAV connector requires declaration.mount.source (server URL)");
|
|
4596
|
-
}
|
|
4597
|
-
const vendorRaw = opts?.vendor !== void 0 ? String(opts.vendor) : "other";
|
|
4598
|
-
if (!ALLOWED_VENDORS.has(vendorRaw)) {
|
|
4599
|
-
throw new Error(
|
|
4600
|
-
`WebDAV connector: invalid vendor "${vendorRaw}" (allowed: ${[...ALLOWED_VENDORS].join(", ")})`
|
|
4601
|
-
);
|
|
4247
|
+
if (!isFleetManaged(declaration.id)) {
|
|
4248
|
+
throw new Error(CLOUD_FLEET_REQUIRED3);
|
|
4602
4249
|
}
|
|
4603
|
-
|
|
4604
|
-
const basePath = String(opts?.basePath ?? "").replace(/^\/+|\/+$/g, "");
|
|
4605
|
-
const remotePath = basePath.length > 0 ? `/${basePath}` : "/";
|
|
4606
|
-
const cred = await resolveMountCredential(declaration, secrets, tokenMediator, "initial");
|
|
4607
|
-
const auth = parseAuth(cred.token);
|
|
4608
|
-
const remoteName = `skaile-${declaration.id}`;
|
|
4609
|
-
const remote = `${remoteName}:${remotePath}`;
|
|
4610
|
-
const base = cacheBase3();
|
|
4611
|
-
const cacheDir = `${base}/${declaration.id}`;
|
|
4612
|
-
const vfsCacheMode = declaration.access === "read-only" ? "minimal" : "full";
|
|
4613
|
-
this.log.info("mount params resolved", {
|
|
4614
|
-
url,
|
|
4615
|
-
vendor,
|
|
4616
|
-
remote,
|
|
4617
|
-
cacheDir,
|
|
4618
|
-
vfsCacheMode,
|
|
4619
|
-
authKind: auth.bearerToken ? "bearer" : auth.username ? "basic" : "anonymous"
|
|
4620
|
-
});
|
|
4621
|
-
const renderConfig = async (rawToken) => {
|
|
4622
|
-
const a = parseAuth(rawToken);
|
|
4623
|
-
const obscured = a.password ? await this.obscure(a.password) : null;
|
|
4624
|
-
return renderWebDAVConfig({
|
|
4625
|
-
remoteName,
|
|
4626
|
-
url,
|
|
4627
|
-
vendor,
|
|
4628
|
-
username: a.username,
|
|
4629
|
-
obscuredPassword: obscured,
|
|
4630
|
-
bearerToken: a.bearerToken
|
|
4631
|
-
});
|
|
4632
|
-
};
|
|
4633
|
-
const handleRef = { current: null };
|
|
4634
|
-
const refresh = declaration.auth === "backend" && tokenMediator ? createRcloneTokenRefresh({
|
|
4250
|
+
this.log.info("mount handled by host fleet", {
|
|
4635
4251
|
connectorId: declaration.id,
|
|
4636
|
-
|
|
4637
|
-
renderConfig,
|
|
4638
|
-
rclone: this.rclone,
|
|
4639
|
-
handleRef,
|
|
4640
|
-
log: (line) => this.log.info(line)
|
|
4641
|
-
}) : void 0;
|
|
4642
|
-
const rcloneConfig = await renderConfig(cred.token ?? "");
|
|
4643
|
-
await ensureDirMode(base, this.log);
|
|
4644
|
-
await ensureDirMode(cacheDir, this.log);
|
|
4645
|
-
await ensureDirMode(targetDir, this.log);
|
|
4646
|
-
const cacheMaxSize = opts?.cacheMaxSize !== void 0 ? String(opts.cacheMaxSize) : void 0;
|
|
4647
|
-
const cacheMaxAge = opts?.cacheMaxAge !== void 0 ? String(opts.cacheMaxAge) : void 0;
|
|
4648
|
-
const vfsWriteBack = opts?.vfsWriteBack !== void 0 ? String(opts.vfsWriteBack) : void 0;
|
|
4649
|
-
const rcloneHandle = await this.rclone.spawn({
|
|
4650
|
-
mountId: declaration.id,
|
|
4651
|
-
driverName: "webdav",
|
|
4652
|
-
mountPoint: targetDir,
|
|
4653
|
-
cacheDir,
|
|
4654
|
-
rcloneConfig,
|
|
4655
|
-
remote,
|
|
4656
|
-
vfsCacheMode,
|
|
4657
|
-
...cacheMaxSize !== void 0 ? { cacheMaxSize } : {},
|
|
4658
|
-
...cacheMaxAge !== void 0 ? { cacheMaxAge } : {},
|
|
4659
|
-
...vfsWriteBack !== void 0 ? { vfsWriteBack } : {},
|
|
4660
|
-
...refresh ? { onAuthError: () => refresh.onAuthError() } : {},
|
|
4661
|
-
log: this.log
|
|
4252
|
+
targetDir
|
|
4662
4253
|
});
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
refresh?.schedule(cred.expiresAt);
|
|
4666
|
-
const state = {
|
|
4667
|
-
handleRef,
|
|
4668
|
-
refresh,
|
|
4669
|
-
authoritativeSize: (relPath) => {
|
|
4670
|
-
const h = handleRef.current;
|
|
4671
|
-
return h ? this.rclone.statSize(h, relPath) : Promise.resolve(null);
|
|
4672
|
-
}
|
|
4673
|
-
};
|
|
4674
|
-
return this.makeHandle(declaration, state, targetDir);
|
|
4675
|
-
}
|
|
4676
|
-
async disconnect(handle) {
|
|
4677
|
-
const s = handle.state;
|
|
4678
|
-
s.refresh?.cancel();
|
|
4679
|
-
if (s.fleetManaged || !s.handleRef.current) return;
|
|
4680
|
-
await this.rclone.stop(s.handleRef.current, { graceMs: 3e4 });
|
|
4254
|
+
await ensureFleetMounted(targetDir, this.log);
|
|
4255
|
+
return this.makeHandle(declaration, { fleetManaged: true }, targetDir);
|
|
4681
4256
|
}
|
|
4682
|
-
async
|
|
4683
|
-
const s = handle.state;
|
|
4684
|
-
if (s.fleetManaged || !s.handleRef.current) {
|
|
4685
|
-
options?.onSyncStatus?.({
|
|
4686
|
-
phase: "initial_sync_complete",
|
|
4687
|
-
totalFiles: 0,
|
|
4688
|
-
downloadedFiles: 0,
|
|
4689
|
-
stubFiles: 0,
|
|
4690
|
-
message: "fleet-managed mount \u2014 flush handled by host"
|
|
4691
|
-
});
|
|
4692
|
-
return { filesWritten: 0, filesDeleted: 0, bytesTransferred: 0 };
|
|
4693
|
-
}
|
|
4694
|
-
await this.rclone.flush(s.handleRef.current);
|
|
4695
|
-
options?.onSyncStatus?.({
|
|
4696
|
-
phase: "initial_sync_complete",
|
|
4697
|
-
totalFiles: 0,
|
|
4698
|
-
downloadedFiles: 0,
|
|
4699
|
-
stubFiles: 0,
|
|
4700
|
-
message: "rclone flush complete"
|
|
4701
|
-
});
|
|
4702
|
-
return { filesWritten: 0, filesDeleted: 0, bytesTransferred: 0 };
|
|
4257
|
+
async disconnect(_handle) {
|
|
4703
4258
|
}
|
|
4704
4259
|
async onHibernate(handle) {
|
|
4705
|
-
|
|
4706
|
-
if (s.fleetManaged) return "flushed";
|
|
4707
|
-
await this._sync(handle);
|
|
4260
|
+
await this.filesystem.sync(handle);
|
|
4708
4261
|
return "flushed";
|
|
4709
4262
|
}
|
|
4710
4263
|
async onSessionClose(handle) {
|
|
4711
|
-
|
|
4712
|
-
if (s.fleetManaged) return "flushed";
|
|
4713
|
-
await this._sync(handle);
|
|
4264
|
+
await this.filesystem.sync(handle);
|
|
4714
4265
|
return "flushed";
|
|
4715
4266
|
}
|
|
4716
4267
|
};
|
|
4717
4268
|
function createConnector17() {
|
|
4718
4269
|
return new WebDAVConnector();
|
|
4719
4270
|
}
|
|
4720
|
-
function
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
return { ...empty, bearerToken: raw };
|
|
4730
|
-
}
|
|
4731
|
-
const username = raw.slice(0, colonIdx);
|
|
4732
|
-
const password = raw.slice(colonIdx + 1);
|
|
4733
|
-
return { username, password, bearerToken: null };
|
|
4734
|
-
}
|
|
4735
|
-
async function obscurePassword(plain) {
|
|
4736
|
-
try {
|
|
4737
|
-
const { stdout } = await execFileP("rclone", ["obscure", plain]);
|
|
4738
|
-
return stdout.trim();
|
|
4739
|
-
} catch (err) {
|
|
4740
|
-
const code = err?.code;
|
|
4741
|
-
if (code === "ENOENT") {
|
|
4742
|
-
throw new Error(
|
|
4743
|
-
"WebDAV connector: rclone binary not found on PATH (install rclone to use the webdav connector)"
|
|
4744
|
-
);
|
|
4745
|
-
}
|
|
4746
|
-
throw new Error(
|
|
4747
|
-
`WebDAV connector: 'rclone obscure' failed: ${err instanceof Error ? err.message : String(err)}`
|
|
4748
|
-
);
|
|
4749
|
-
}
|
|
4271
|
+
function syncFleetManaged3(options) {
|
|
4272
|
+
options?.onSyncStatus?.({
|
|
4273
|
+
phase: "initial_sync_complete",
|
|
4274
|
+
totalFiles: 0,
|
|
4275
|
+
downloadedFiles: 0,
|
|
4276
|
+
stubFiles: 0,
|
|
4277
|
+
message: "fleet-managed mount \u2014 flush handled by host"
|
|
4278
|
+
});
|
|
4279
|
+
return Promise.resolve({ filesWritten: 0, filesDeleted: 0, bytesTransferred: 0 });
|
|
4750
4280
|
}
|
|
4751
4281
|
var XStateConnector = class extends AbstractConnector {
|
|
4752
4282
|
name = "xstate";
|
|
@@ -4754,8 +4284,8 @@ var XStateConnector = class extends AbstractConnector {
|
|
|
4754
4284
|
constructor() {
|
|
4755
4285
|
super();
|
|
4756
4286
|
this.tools = {
|
|
4757
|
-
read: (handle,
|
|
4758
|
-
list: (handle,
|
|
4287
|
+
read: (handle, path) => this._read(handle, path),
|
|
4288
|
+
list: (handle, path, options) => this._list(handle, path, options),
|
|
4759
4289
|
search: (handle, query) => this._search(handle, query),
|
|
4760
4290
|
describeOperations: (handle) => this._describeOperations(handle),
|
|
4761
4291
|
executeOp: (handle, operation, args) => this._executeOp(handle, operation, args)
|
|
@@ -4857,15 +4387,15 @@ var XStateConnector = class extends AbstractConnector {
|
|
|
4857
4387
|
this.s(handle).onChange = cb;
|
|
4858
4388
|
}
|
|
4859
4389
|
// ── Tool face methods (private, wired in constructor) ─────────────────────
|
|
4860
|
-
async _read(handle,
|
|
4390
|
+
async _read(handle, path) {
|
|
4861
4391
|
const snapshot = this.s(handle).actor.getSnapshot();
|
|
4862
|
-
if (
|
|
4392
|
+
if (path === "" || path === "state") {
|
|
4863
4393
|
return { data: JSON.stringify(snapshot.value), contentType: "application/json" };
|
|
4864
4394
|
}
|
|
4865
|
-
if (
|
|
4395
|
+
if (path === "context") {
|
|
4866
4396
|
return { data: JSON.stringify(snapshot.context, null, 2), contentType: "application/json" };
|
|
4867
4397
|
}
|
|
4868
|
-
const value = snapshot.context?.[
|
|
4398
|
+
const value = snapshot.context?.[path];
|
|
4869
4399
|
if (value === void 0) return null;
|
|
4870
4400
|
return { data: JSON.stringify(value), contentType: "application/json" };
|
|
4871
4401
|
}
|
|
@@ -5059,10 +4589,10 @@ var XStateStoreConnector = class extends AbstractConnector {
|
|
|
5059
4589
|
constructor() {
|
|
5060
4590
|
super();
|
|
5061
4591
|
this.tools = {
|
|
5062
|
-
read: (handle,
|
|
5063
|
-
write: (handle,
|
|
5064
|
-
delete: (handle,
|
|
5065
|
-
list: (handle,
|
|
4592
|
+
read: (handle, path) => this._read(handle, path),
|
|
4593
|
+
write: (handle, path, content) => this._write(handle, path, content),
|
|
4594
|
+
delete: (handle, path) => this._delete(handle, path),
|
|
4595
|
+
list: (handle, path, options) => this._list(handle, path, options),
|
|
5066
4596
|
search: (handle, query, options) => this._search(handle, query, options),
|
|
5067
4597
|
describeOperations: (handle) => this._describeOperations(handle),
|
|
5068
4598
|
executeOp: (handle, operation, args) => this._executeOp(handle, operation, args)
|
|
@@ -5116,17 +4646,17 @@ var XStateStoreConnector = class extends AbstractConnector {
|
|
|
5116
4646
|
handle.state.onChange = cb;
|
|
5117
4647
|
}
|
|
5118
4648
|
// ── Tool face methods (private, wired in constructor) ─────────────────────
|
|
5119
|
-
async _read(handle,
|
|
4649
|
+
async _read(handle, path) {
|
|
5120
4650
|
const ctx = this.context(handle);
|
|
5121
|
-
if (
|
|
4651
|
+
if (path === "*") {
|
|
5122
4652
|
if (Object.keys(ctx).length === 0) return null;
|
|
5123
4653
|
return { data: JSON.stringify(ctx, null, 2), contentType: "application/json" };
|
|
5124
4654
|
}
|
|
5125
|
-
const value = ctx[
|
|
4655
|
+
const value = ctx[path];
|
|
5126
4656
|
if (value === void 0) return null;
|
|
5127
4657
|
return { data: JSON.stringify(value), contentType: "application/json" };
|
|
5128
4658
|
}
|
|
5129
|
-
async _write(handle,
|
|
4659
|
+
async _write(handle, path, content) {
|
|
5130
4660
|
const data = typeof content.data === "string" ? content.data : content.data.toString("utf-8");
|
|
5131
4661
|
let value;
|
|
5132
4662
|
try {
|
|
@@ -5134,14 +4664,14 @@ var XStateStoreConnector = class extends AbstractConnector {
|
|
|
5134
4664
|
} catch {
|
|
5135
4665
|
value = data;
|
|
5136
4666
|
}
|
|
5137
|
-
this.storeState(handle).store.send({ type: "set", key:
|
|
5138
|
-
this._recordHistory(handle, "set", { key:
|
|
4667
|
+
this.storeState(handle).store.send({ type: "set", key: path, value });
|
|
4668
|
+
this._recordHistory(handle, "set", { key: path, value });
|
|
5139
4669
|
}
|
|
5140
|
-
async _delete(handle,
|
|
4670
|
+
async _delete(handle, path) {
|
|
5141
4671
|
const ctx = this.context(handle);
|
|
5142
|
-
if (!(
|
|
5143
|
-
this.storeState(handle).store.send({ type: "delete", key:
|
|
5144
|
-
this._recordHistory(handle, "delete", { key:
|
|
4672
|
+
if (!(path in ctx)) return false;
|
|
4673
|
+
this.storeState(handle).store.send({ type: "delete", key: path });
|
|
4674
|
+
this._recordHistory(handle, "delete", { key: path });
|
|
5145
4675
|
return true;
|
|
5146
4676
|
}
|
|
5147
4677
|
async _list(handle, _path, options) {
|
|
@@ -5446,418 +4976,8 @@ var ConnectorStartupError = class extends Error {
|
|
|
5446
4976
|
}
|
|
5447
4977
|
};
|
|
5448
4978
|
|
|
5449
|
-
// connectors/src/fleet-health-gate.ts
|
|
5450
|
-
function verdictToSurface(verdict) {
|
|
5451
|
-
if (verdict === "healthy") return "open";
|
|
5452
|
-
if (verdict === "degraded") return "ro";
|
|
5453
|
-
return "masked";
|
|
5454
|
-
}
|
|
5455
|
-
var FleetHealthGate = class {
|
|
5456
|
-
constructor(target, barrier, log, opts) {
|
|
5457
|
-
this.target = target;
|
|
5458
|
-
this.barrier = barrier;
|
|
5459
|
-
this.log = log;
|
|
5460
|
-
this.unhealthyThreshold = Math.max(1, opts?.unhealthyThreshold ?? 2);
|
|
5461
|
-
this.healthyThreshold = Math.max(1, opts?.healthyThreshold ?? 2);
|
|
5462
|
-
const initial = opts?.initialSurface ?? "open";
|
|
5463
|
-
this.surface = initial;
|
|
5464
|
-
this.pendingSurface = initial;
|
|
5465
|
-
this.runSurface = initial;
|
|
5466
|
-
}
|
|
5467
|
-
target;
|
|
5468
|
-
barrier;
|
|
5469
|
-
log;
|
|
5470
|
-
unhealthyThreshold;
|
|
5471
|
-
healthyThreshold;
|
|
5472
|
-
/** The barrier surface actually applied to the target right now. */
|
|
5473
|
-
surface;
|
|
5474
|
-
/**
|
|
5475
|
-
* The surface the most recently *enqueued* transition will settle on.
|
|
5476
|
-
* Hysteresis and re-enqueue de-duping are measured against this, not
|
|
5477
|
-
* `surface`, so verdicts arriving while a (slow, async) barrier op is still in
|
|
5478
|
-
* flight are counted toward the post-transition surface, not the stale one.
|
|
5479
|
-
*/
|
|
5480
|
-
pendingSurface;
|
|
5481
|
-
/** The surface of the *current* same-verdict run, and its length. */
|
|
5482
|
-
runSurface;
|
|
5483
|
-
runLength = 0;
|
|
5484
|
-
/** Serialises barrier ops so overlapping observe() calls can't race. */
|
|
5485
|
-
transition = Promise.resolve();
|
|
5486
|
-
/** True while any read-only barrier (ro or masked) is established. */
|
|
5487
|
-
get isGated() {
|
|
5488
|
-
return this.surface !== "open";
|
|
5489
|
-
}
|
|
5490
|
-
/** The currently-applied surface (test/observability hook). */
|
|
5491
|
-
get currentSurface() {
|
|
5492
|
-
return this.surface;
|
|
5493
|
-
}
|
|
5494
|
-
/** The mount target this gate guards (recovery-probe lookup). */
|
|
5495
|
-
get mountTarget() {
|
|
5496
|
-
return this.target;
|
|
5497
|
-
}
|
|
5498
|
-
/**
|
|
5499
|
-
* Feed one health verdict. Applies hysteresis, then drives the barrier toward
|
|
5500
|
-
* the target surface on a change. Resolves once the (possibly no-op)
|
|
5501
|
-
* transition is applied. Never throws — a failed barrier op stays gated
|
|
5502
|
-
* (fail-closed).
|
|
5503
|
-
*/
|
|
5504
|
-
observe(verdict) {
|
|
5505
|
-
const desired = verdictToSurface(verdict);
|
|
5506
|
-
if (desired === this.runSurface) {
|
|
5507
|
-
this.runLength += 1;
|
|
5508
|
-
} else {
|
|
5509
|
-
this.runSurface = desired;
|
|
5510
|
-
this.runLength = 1;
|
|
5511
|
-
}
|
|
5512
|
-
const threshold = desired === "open" ? this.healthyThreshold : this.unhealthyThreshold;
|
|
5513
|
-
if (desired !== this.pendingSurface && this.runLength >= threshold) {
|
|
5514
|
-
this.pendingSurface = desired;
|
|
5515
|
-
this.runLength = 0;
|
|
5516
|
-
return this.enqueue(() => this.apply(desired));
|
|
5517
|
-
}
|
|
5518
|
-
return this.transition;
|
|
5519
|
-
}
|
|
5520
|
-
/**
|
|
5521
|
-
* Tear the gate down. If any barrier is established (or in flight), removes it
|
|
5522
|
-
* so a leftover read-only surface is not leaked. Call only after the connector
|
|
5523
|
-
* above the target is disconnected (top-down, no leaked mount).
|
|
5524
|
-
*/
|
|
5525
|
-
dispose() {
|
|
5526
|
-
if (this.surface === "open" && this.pendingSurface === "open") return this.transition;
|
|
5527
|
-
this.pendingSurface = "open";
|
|
5528
|
-
return this.enqueue(() => this.apply("open"));
|
|
5529
|
-
}
|
|
5530
|
-
enqueue(step) {
|
|
5531
|
-
this.transition = this.transition.then(step, step);
|
|
5532
|
-
return this.transition;
|
|
5533
|
-
}
|
|
5534
|
-
/** Drive the barrier to `desired`, fail-closed to `masked` on any throw. */
|
|
5535
|
-
async apply(desired) {
|
|
5536
|
-
if (this.surface === desired) {
|
|
5537
|
-
this.pendingSurface = desired;
|
|
5538
|
-
return;
|
|
5539
|
-
}
|
|
5540
|
-
try {
|
|
5541
|
-
if (desired === "open") {
|
|
5542
|
-
await this.barrier.remove(this.target, this.log);
|
|
5543
|
-
this.surface = "open";
|
|
5544
|
-
this.log?.info("fleet mount recovered \u2014 gate lifted, writable again", {
|
|
5545
|
-
target: this.target
|
|
5546
|
-
});
|
|
5547
|
-
return;
|
|
5548
|
-
}
|
|
5549
|
-
if (desired === "ro") {
|
|
5550
|
-
if (this.barrier.exposeReadOnly) {
|
|
5551
|
-
await this.barrier.exposeReadOnly(this.target, this.log);
|
|
5552
|
-
} else {
|
|
5553
|
-
await this.barrier.establish(this.target, this.log);
|
|
5554
|
-
}
|
|
5555
|
-
this.surface = "ro";
|
|
5556
|
-
this.log?.warn("fleet mount upload-degraded \u2014 gated read-only (content visible)", {
|
|
5557
|
-
target: this.target
|
|
5558
|
-
});
|
|
5559
|
-
return;
|
|
5560
|
-
}
|
|
5561
|
-
await this.barrier.establish(this.target, this.log);
|
|
5562
|
-
this.surface = "masked";
|
|
5563
|
-
this.log?.warn("fleet mount unhealthy \u2014 gated read-only (masked)", { target: this.target });
|
|
5564
|
-
} catch (err) {
|
|
5565
|
-
const safe = "masked";
|
|
5566
|
-
this.surface = safe;
|
|
5567
|
-
this.pendingSurface = safe;
|
|
5568
|
-
this.log?.error(
|
|
5569
|
-
`fleet read-only gate transition to '${desired}' threw \u2014 biasing to '${safe}' (fail-closed)`,
|
|
5570
|
-
err,
|
|
5571
|
-
{ target: this.target }
|
|
5572
|
-
);
|
|
5573
|
-
}
|
|
5574
|
-
}
|
|
5575
|
-
};
|
|
5576
|
-
function isFleetManaged(connectorId) {
|
|
5577
|
-
const raw = process.env.SKAILE_FLEET_MANAGED_MOUNTS ?? "";
|
|
5578
|
-
if (raw.length === 0) return false;
|
|
5579
|
-
return raw.split(",").map((segment) => segment.trim()).filter((segment) => segment.length > 0).includes(connectorId);
|
|
5580
|
-
}
|
|
5581
|
-
async function ensureFleetMounted(dir, log, options) {
|
|
5582
|
-
const timeoutMs = options?.timeoutMs ?? 3e3;
|
|
5583
|
-
const pollIntervalMs = options?.pollIntervalMs ?? 100;
|
|
5584
|
-
const deadline = Date.now() + timeoutMs;
|
|
5585
|
-
let lastErr = null;
|
|
5586
|
-
while (Date.now() < deadline) {
|
|
5587
|
-
try {
|
|
5588
|
-
const st = await stat(dir);
|
|
5589
|
-
if (!st.isDirectory()) {
|
|
5590
|
-
throw new Error(`fleet mountpoint ${dir} is not a directory`);
|
|
5591
|
-
}
|
|
5592
|
-
await readdir(dir);
|
|
5593
|
-
log.info("fleet bind mount verified", { dir });
|
|
5594
|
-
return;
|
|
5595
|
-
} catch (err) {
|
|
5596
|
-
lastErr = err;
|
|
5597
|
-
await new Promise((resolve5) => setTimeout(resolve5, pollIntervalMs));
|
|
5598
|
-
}
|
|
5599
|
-
}
|
|
5600
|
-
throw new Error(
|
|
5601
|
-
`fleet mount ${dir} not ready after ${timeoutMs}ms \u2014 check SKAILE_FLEET_MANAGED_MOUNTS env var and host bind: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
|
|
5602
|
-
);
|
|
5603
|
-
}
|
|
5604
|
-
async function probeFleetHealth(dir) {
|
|
5605
|
-
try {
|
|
5606
|
-
const st = await stat(dir);
|
|
5607
|
-
if (!st.isDirectory()) return "error";
|
|
5608
|
-
await readdir(dir);
|
|
5609
|
-
return "healthy";
|
|
5610
|
-
} catch {
|
|
5611
|
-
return "error";
|
|
5612
|
-
}
|
|
5613
|
-
}
|
|
5614
|
-
async function probeFleetMountTablePresent(dir) {
|
|
5615
|
-
try {
|
|
5616
|
-
const text = await readFile("/proc/self/mounts", "utf8");
|
|
5617
|
-
const resolved = path2.resolve(dir);
|
|
5618
|
-
for (const raw of text.split("\n")) {
|
|
5619
|
-
if (raw.length === 0) continue;
|
|
5620
|
-
const fields = raw.split(/\s+/);
|
|
5621
|
-
const mp = fields[1];
|
|
5622
|
-
const fstype = fields[2];
|
|
5623
|
-
if (!mp || !fstype) continue;
|
|
5624
|
-
if (mp.replace(/\\040/g, " ") !== resolved) continue;
|
|
5625
|
-
if (fstype !== "tmpfs") return true;
|
|
5626
|
-
}
|
|
5627
|
-
return false;
|
|
5628
|
-
} catch {
|
|
5629
|
-
return false;
|
|
5630
|
-
}
|
|
5631
|
-
}
|
|
5632
|
-
var NOT_READY_SENTINEL = ".skaile-workspace-not-ready";
|
|
5633
|
-
var TARGET_ALREADY_PRESENT_CODES = /* @__PURE__ */ new Set(["EEXIST", "ENOTCONN", "EBUSY", "ENOTDIR"]);
|
|
5634
|
-
function errnoCode(err) {
|
|
5635
|
-
return typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
|
|
5636
|
-
}
|
|
5637
|
-
var SENTINEL_BODY = [
|
|
5638
|
-
"This workspace is backed by a remote mount that is not live yet.",
|
|
5639
|
-
"It is mounted read-only on purpose: writing here would create a phantom",
|
|
5640
|
-
"workspace that diverges from the remote once the mount comes up.",
|
|
5641
|
-
"Wait for the mount to finish syncing (watch connector_status) before writing.",
|
|
5642
|
-
""
|
|
5643
|
-
].join("\n");
|
|
5644
|
-
var defaultMkdir = async (target) => {
|
|
5645
|
-
await mkdir(target, { recursive: true });
|
|
5646
|
-
};
|
|
5647
|
-
var defaultRunner = (cmd, args) => new Promise((resolve5, reject) => {
|
|
5648
|
-
const proc = spawn(cmd, args, { stdio: "ignore" });
|
|
5649
|
-
proc.once("error", reject);
|
|
5650
|
-
proc.once("close", (code, signal) => {
|
|
5651
|
-
if (code === 0) resolve5();
|
|
5652
|
-
else reject(new Error(`${cmd} ${args.join(" ")} exited ${code ?? `signal ${signal}`}`));
|
|
5653
|
-
});
|
|
5654
|
-
});
|
|
5655
|
-
var RoMountPlaceholderBarrier = class {
|
|
5656
|
-
run;
|
|
5657
|
-
mkdirFn;
|
|
5658
|
-
modes = /* @__PURE__ */ new Map();
|
|
5659
|
-
constructor(opts) {
|
|
5660
|
-
this.run = opts?.run ?? defaultRunner;
|
|
5661
|
-
this.mkdirFn = opts?.mkdirFn ?? defaultMkdir;
|
|
5662
|
-
}
|
|
5663
|
-
async establish(target, log) {
|
|
5664
|
-
if (this.modes.has(target)) return;
|
|
5665
|
-
try {
|
|
5666
|
-
await this.mkdirFn(target);
|
|
5667
|
-
} catch (err) {
|
|
5668
|
-
const code = errnoCode(err);
|
|
5669
|
-
if (!code || !TARGET_ALREADY_PRESENT_CODES.has(code)) {
|
|
5670
|
-
await this.chmodFallback(target, err, log);
|
|
5671
|
-
return;
|
|
5672
|
-
}
|
|
5673
|
-
log?.debug("barrier target already present (live/broken mount) \u2014 protecting in place", {
|
|
5674
|
-
target,
|
|
5675
|
-
code
|
|
5676
|
-
});
|
|
5677
|
-
}
|
|
5678
|
-
let tmpfsMounted = false;
|
|
5679
|
-
try {
|
|
5680
|
-
await this.run("mount", [
|
|
5681
|
-
"-t",
|
|
5682
|
-
"tmpfs",
|
|
5683
|
-
"-o",
|
|
5684
|
-
"mode=0755,size=1m",
|
|
5685
|
-
"skaile-not-ready",
|
|
5686
|
-
target
|
|
5687
|
-
]);
|
|
5688
|
-
tmpfsMounted = true;
|
|
5689
|
-
await writeFile(path2.join(target, NOT_READY_SENTINEL), SENTINEL_BODY);
|
|
5690
|
-
await this.run("mount", ["-o", "remount,ro", target]);
|
|
5691
|
-
this.modes.set(target, "mount");
|
|
5692
|
-
log?.info("deferred mount barrier established (read-only tmpfs)", { target });
|
|
5693
|
-
} catch (err) {
|
|
5694
|
-
if (tmpfsMounted) await this.run("umount", [target]).catch(() => {
|
|
5695
|
-
});
|
|
5696
|
-
await this.chmodFallback(target, err, log);
|
|
5697
|
-
}
|
|
5698
|
-
}
|
|
5699
|
-
async prepareForStackedMount(target, log) {
|
|
5700
|
-
const mode = this.modes.get(target);
|
|
5701
|
-
if (mode === "mount") {
|
|
5702
|
-
try {
|
|
5703
|
-
await this.run("mount", ["-o", "remount,rw", target]);
|
|
5704
|
-
log?.debug("deferred mount barrier lifted to read-write for stacked mount", { target });
|
|
5705
|
-
} catch (err) {
|
|
5706
|
-
log?.error(
|
|
5707
|
-
"deferred mount barrier: remount read-write before stacked mount failed \u2014 the mount may be rejected with EACCES",
|
|
5708
|
-
err instanceof Error ? err : void 0,
|
|
5709
|
-
{ target }
|
|
5710
|
-
);
|
|
5711
|
-
}
|
|
5712
|
-
return;
|
|
5713
|
-
}
|
|
5714
|
-
}
|
|
5715
|
-
async restoreBarrier(target, log) {
|
|
5716
|
-
const mode = this.modes.get(target);
|
|
5717
|
-
if (mode === "mount") {
|
|
5718
|
-
let restored = false;
|
|
5719
|
-
await this.run("mount", ["-o", "remount,ro", target]).then(() => {
|
|
5720
|
-
restored = true;
|
|
5721
|
-
}).catch((err) => {
|
|
5722
|
-
log?.error(
|
|
5723
|
-
"deferred mount barrier: remount read-only after failed mount failed \u2014 target may be writable",
|
|
5724
|
-
err instanceof Error ? err : void 0,
|
|
5725
|
-
{ target }
|
|
5726
|
-
);
|
|
5727
|
-
});
|
|
5728
|
-
if (restored) log?.debug("deferred mount barrier re-asserted read-only after failed mount", { target });
|
|
5729
|
-
}
|
|
5730
|
-
}
|
|
5731
|
-
async onMountLive(target, log) {
|
|
5732
|
-
const mode = this.modes.get(target);
|
|
5733
|
-
if (mode === "mount") {
|
|
5734
|
-
log?.debug("deferred mount live; placeholder shadowed", { target });
|
|
5735
|
-
return;
|
|
5736
|
-
}
|
|
5737
|
-
if (mode === "chmod" || mode === "unprotected") {
|
|
5738
|
-
if (mode === "chmod") await chmod(target, 493).catch(() => {
|
|
5739
|
-
});
|
|
5740
|
-
await rm(path2.join(target, NOT_READY_SENTINEL), { force: true }).catch(() => {
|
|
5741
|
-
});
|
|
5742
|
-
log?.debug("deferred mount live; degraded barrier lifted", { target, mode });
|
|
5743
|
-
}
|
|
5744
|
-
}
|
|
5745
|
-
async exposeReadOnly(target, log) {
|
|
5746
|
-
const mode = this.modes.get(target);
|
|
5747
|
-
if (mode === "ro-bind") return;
|
|
5748
|
-
if (mode === "mount") {
|
|
5749
|
-
let unmounted = await this.run("umount", [target]).then(
|
|
5750
|
-
() => true,
|
|
5751
|
-
() => false
|
|
5752
|
-
);
|
|
5753
|
-
if (!unmounted) {
|
|
5754
|
-
unmounted = await this.run("umount", ["-l", target]).then(
|
|
5755
|
-
() => true,
|
|
5756
|
-
() => false
|
|
5757
|
-
);
|
|
5758
|
-
}
|
|
5759
|
-
if (!unmounted) {
|
|
5760
|
-
log?.error(
|
|
5761
|
-
"deferred mount barrier: could not unmount opaque mask to expose read-only surface \u2014 staying masked",
|
|
5762
|
-
void 0,
|
|
5763
|
-
{ target }
|
|
5764
|
-
);
|
|
5765
|
-
return;
|
|
5766
|
-
}
|
|
5767
|
-
}
|
|
5768
|
-
try {
|
|
5769
|
-
await this.run("mount", ["--bind", target, target]);
|
|
5770
|
-
await this.run("mount", ["-o", "remount,ro,bind", target]);
|
|
5771
|
-
this.modes.set(target, "ro-bind");
|
|
5772
|
-
log?.info("deferred mount barrier downgraded to read-only (content visible)", { target });
|
|
5773
|
-
} catch (err) {
|
|
5774
|
-
const chmodOk = await chmod(target, 365).then(
|
|
5775
|
-
() => true,
|
|
5776
|
-
() => false
|
|
5777
|
-
);
|
|
5778
|
-
this.modes.set(target, chmodOk ? "chmod" : "unprotected");
|
|
5779
|
-
log?.error(
|
|
5780
|
-
chmodOk ? "deferred mount read-only-readable surface degraded to chmod (ro bind unavailable) \u2014 a write by the owning uid could bypass it" : "deferred mount read-only-readable surface FAILED (ro bind and chmod both unavailable) \u2014 target may be writable",
|
|
5781
|
-
err instanceof Error ? err : void 0,
|
|
5782
|
-
{ target }
|
|
5783
|
-
);
|
|
5784
|
-
}
|
|
5785
|
-
}
|
|
5786
|
-
isOpaque(target) {
|
|
5787
|
-
return this.modes.get(target) === "mount";
|
|
5788
|
-
}
|
|
5789
|
-
async remove(target, log) {
|
|
5790
|
-
const mode = this.modes.get(target);
|
|
5791
|
-
if (!mode) return;
|
|
5792
|
-
this.modes.delete(target);
|
|
5793
|
-
if (mode === "mount" || mode === "ro-bind") {
|
|
5794
|
-
let unmounted = false;
|
|
5795
|
-
try {
|
|
5796
|
-
await this.run("umount", [target]);
|
|
5797
|
-
unmounted = true;
|
|
5798
|
-
} catch {
|
|
5799
|
-
await this.run("umount", ["-l", target]).then(
|
|
5800
|
-
() => {
|
|
5801
|
-
unmounted = true;
|
|
5802
|
-
},
|
|
5803
|
-
() => {
|
|
5804
|
-
}
|
|
5805
|
-
);
|
|
5806
|
-
}
|
|
5807
|
-
if (unmounted) {
|
|
5808
|
-
log?.debug("deferred mount barrier removed (tmpfs unmounted)", { target });
|
|
5809
|
-
} else {
|
|
5810
|
-
log?.error(
|
|
5811
|
-
"deferred mount barrier: umount and umount -l both failed \u2014 tmpfs may be leaked",
|
|
5812
|
-
void 0,
|
|
5813
|
-
{ target }
|
|
5814
|
-
);
|
|
5815
|
-
}
|
|
5816
|
-
return;
|
|
5817
|
-
}
|
|
5818
|
-
if (mode === "chmod") await chmod(target, 493).catch(() => {
|
|
5819
|
-
});
|
|
5820
|
-
await rm(path2.join(target, NOT_READY_SENTINEL), { force: true }).catch(() => {
|
|
5821
|
-
});
|
|
5822
|
-
log?.debug(
|
|
5823
|
-
mode === "chmod" ? "deferred mount barrier removed (chmod restored)" : "deferred mount barrier removed (was never enforced \u2014 target stayed writable)",
|
|
5824
|
-
{ target }
|
|
5825
|
-
);
|
|
5826
|
-
}
|
|
5827
|
-
async chmodFallback(target, cause, log) {
|
|
5828
|
-
await writeFile(path2.join(target, NOT_READY_SENTINEL), SENTINEL_BODY).catch(() => {
|
|
5829
|
-
});
|
|
5830
|
-
const chmodOk = await chmod(target, 365).then(
|
|
5831
|
-
() => true,
|
|
5832
|
-
() => false
|
|
5833
|
-
);
|
|
5834
|
-
this.modes.set(target, chmodOk ? "chmod" : "unprotected");
|
|
5835
|
-
const detail = { target, error: cause instanceof Error ? cause.message : String(cause) };
|
|
5836
|
-
if (chmodOk) {
|
|
5837
|
-
log?.error(
|
|
5838
|
-
"deferred mount read-only barrier degraded to chmod (mount unavailable) \u2014 a write by the owning uid could bypass it",
|
|
5839
|
-
void 0,
|
|
5840
|
-
detail
|
|
5841
|
-
);
|
|
5842
|
-
} else {
|
|
5843
|
-
log?.error(
|
|
5844
|
-
"deferred mount read-only barrier FAILED (mount and chmod both unavailable) \u2014 target is WRITABLE",
|
|
5845
|
-
void 0,
|
|
5846
|
-
detail
|
|
5847
|
-
);
|
|
5848
|
-
}
|
|
5849
|
-
}
|
|
5850
|
-
};
|
|
5851
|
-
var defaultMountPlaceholderBarrier = new RoMountPlaceholderBarrier();
|
|
5852
|
-
|
|
5853
4979
|
// connectors/src/connector-manager.ts
|
|
5854
4980
|
var KNOWN_PREFIXES = ["env:", "forge:", "op:", "kp:", "oauth:"];
|
|
5855
|
-
var DEFAULT_FLEET_HEALTH_POLL_MS = 5e3;
|
|
5856
|
-
function parseFleetHealthPollMs(raw = process.env.SKAILE_FLEET_HEALTH_POLL_MS) {
|
|
5857
|
-
if (raw === void 0) return DEFAULT_FLEET_HEALTH_POLL_MS;
|
|
5858
|
-
const n = Number(raw);
|
|
5859
|
-
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_FLEET_HEALTH_POLL_MS;
|
|
5860
|
-
}
|
|
5861
4981
|
function hasKnownPrefix(ref) {
|
|
5862
4982
|
return KNOWN_PREFIXES.some((p) => ref.startsWith(p));
|
|
5863
4983
|
}
|
|
@@ -5960,9 +5080,6 @@ var ConnectorManager = class {
|
|
|
5960
5080
|
this.catalogEntries = opts.catalogEntries ?? [];
|
|
5961
5081
|
this.tokenMediator = opts.tokenMediator;
|
|
5962
5082
|
this.preMintedSecrets = opts.preMintedSecrets;
|
|
5963
|
-
this.placeholderBarrier = opts.placeholderBarrier ?? defaultMountPlaceholderBarrier;
|
|
5964
|
-
this.fleetHealthPollMs = opts.fleetHealthPollMs ?? parseFleetHealthPollMs();
|
|
5965
|
-
this.fleetMountRecoveryProbe = opts.fleetMountRecoveryProbe ?? probeFleetMountTablePresent;
|
|
5966
5083
|
const secrets = opts.secrets;
|
|
5967
5084
|
if (secrets && typeof secrets === "object" && "prepareRef" in secrets) {
|
|
5968
5085
|
this.chain = secrets;
|
|
@@ -5985,20 +5102,9 @@ var ConnectorManager = class {
|
|
|
5985
5102
|
catalogEntries;
|
|
5986
5103
|
tokenMediator;
|
|
5987
5104
|
preMintedSecrets;
|
|
5988
|
-
placeholderBarrier;
|
|
5989
5105
|
mgrLog;
|
|
5990
|
-
/** Set once `disconnectAll()` starts, so in-flight deferred mounts bail. */
|
|
5991
|
-
disposed = false;
|
|
5992
5106
|
/** Aborted on dispose; threaded into every `connect()` via `ctx.abortSignal`. */
|
|
5993
5107
|
disposeController = new AbortController();
|
|
5994
|
-
/** Tracks the background task driving deferred (rclone-backed) mounts. */
|
|
5995
|
-
deferredConnect;
|
|
5996
|
-
/** Periodic local fleet-health probe; runs only while a fleet gate is active. */
|
|
5997
|
-
fleetHealthTimer;
|
|
5998
|
-
/** Local fleet-health poll interval (ms). 0 disables the local probe. */
|
|
5999
|
-
fleetHealthPollMs;
|
|
6000
|
-
/** Host-mount recovery detector under an opaque placeholder (injectable for tests). */
|
|
6001
|
-
fleetMountRecoveryProbe;
|
|
6002
5108
|
// ── Private helpers ──────────────────────────────────────────────────────────
|
|
6003
5109
|
/**
|
|
6004
5110
|
* Wrap the user-supplied `TokenMediator` so `reason: 'initial'` calls for an
|
|
@@ -6128,8 +5234,8 @@ var ConnectorManager = class {
|
|
|
6128
5234
|
}
|
|
6129
5235
|
/** Release every mount target owned by `id` (on disconnect). */
|
|
6130
5236
|
releaseMountTarget(id) {
|
|
6131
|
-
for (const [
|
|
6132
|
-
if (owner === id) this.mountTargets.delete(
|
|
5237
|
+
for (const [path, owner] of [...this.mountTargets]) {
|
|
5238
|
+
if (owner === id) this.mountTargets.delete(path);
|
|
6133
5239
|
}
|
|
6134
5240
|
}
|
|
6135
5241
|
/**
|
|
@@ -6193,21 +5299,13 @@ var ConnectorManager = class {
|
|
|
6193
5299
|
* connector with a `mount` config failed → throws `ConnectorStartupError`
|
|
6194
5300
|
* (fail-fast lifecycle semantics for mountable connectors).
|
|
6195
5301
|
*
|
|
6196
|
-
* When `options.deferMounts` is set, connectors whose filesystem face is
|
|
6197
|
-
* marked `deferMount` (rclone-backed FUSE mounts) are NOT awaited here:
|
|
6198
|
-
* their target is pre-registered (so the mount path renders in the system
|
|
6199
|
-
* prompt immediately) and the actual mount is driven on a background task.
|
|
6200
|
-
* The returned report therefore covers only the eagerly-connected set, so a
|
|
6201
|
-
* slow remote can never gate whatever the caller does next (e.g. emitting
|
|
6202
|
-
* `session_init_ack`). Deferred connectors surface their progress through
|
|
6203
|
-
* `onStatusChange` (connecting → connected | error).
|
|
6204
|
-
*
|
|
6205
5302
|
* @param declarations - Connector declarations from `skaile.yaml`.
|
|
6206
5303
|
* @param options.failOnReadWriteError - Throw `ConnectorStartupError` if any
|
|
6207
5304
|
* read-write connector fails. Read-only failures are always tolerated.
|
|
6208
5305
|
* Only the eager set is considered — deferred mounts never throw.
|
|
6209
|
-
* @param options.deferMounts -
|
|
6210
|
-
*
|
|
5306
|
+
* @param options.deferMounts - Deprecated compatibility option. Cloud
|
|
5307
|
+
* connectors now require already-live host fleet bind mounts and connect
|
|
5308
|
+
* synchronously.
|
|
6211
5309
|
*/
|
|
6212
5310
|
async connectAll(declarations, options) {
|
|
6213
5311
|
this.mgrLog.info("connectAll start", { count: declarations.length });
|
|
@@ -6215,36 +5313,8 @@ var ConnectorManager = class {
|
|
|
6215
5313
|
if (decl.mount != null) this.reserveMountTarget(decl.id, this.resolveMountTarget(decl));
|
|
6216
5314
|
}
|
|
6217
5315
|
const results = [];
|
|
6218
|
-
const
|
|
6219
|
-
|
|
6220
|
-
if (options?.deferMounts) {
|
|
6221
|
-
for (const decl of declarations) {
|
|
6222
|
-
let connector;
|
|
6223
|
-
try {
|
|
6224
|
-
connector = await this.resolveConnector(decl.driver);
|
|
6225
|
-
} catch {
|
|
6226
|
-
}
|
|
6227
|
-
if (connector?.filesystem?.deferMount === true) {
|
|
6228
|
-
deferred.push({ decl, connector });
|
|
6229
|
-
} else {
|
|
6230
|
-
eager.push({ decl, connector });
|
|
6231
|
-
}
|
|
6232
|
-
}
|
|
6233
|
-
} else {
|
|
6234
|
-
for (const decl of declarations) eager.push({ decl });
|
|
6235
|
-
}
|
|
6236
|
-
for (const { decl, connector } of eager) {
|
|
6237
|
-
results.push(await this.attemptConnect(decl, connector));
|
|
6238
|
-
}
|
|
6239
|
-
if (deferred.length > 0) {
|
|
6240
|
-
for (const { decl, connector } of deferred) {
|
|
6241
|
-
await this.preRegisterDeferred(decl, connector);
|
|
6242
|
-
}
|
|
6243
|
-
const prior = this.deferredConnect;
|
|
6244
|
-
this.deferredConnect = (async () => {
|
|
6245
|
-
if (prior) await prior;
|
|
6246
|
-
await this.connectDeferred(deferred);
|
|
6247
|
-
})();
|
|
5316
|
+
for (const decl of declarations) {
|
|
5317
|
+
results.push(await this.attemptConnect(decl));
|
|
6248
5318
|
}
|
|
6249
5319
|
const report = { results };
|
|
6250
5320
|
this.mgrLog.info("connectAll complete", {
|
|
@@ -6273,7 +5343,7 @@ var ConnectorManager = class {
|
|
|
6273
5343
|
/**
|
|
6274
5344
|
* Connect a single declaration, emitting `connecting` → `connected | error`.
|
|
6275
5345
|
* Never throws — failures are recorded in the returned result so `connectAll`
|
|
6276
|
-
* can keep going.
|
|
5346
|
+
* can keep going.
|
|
6277
5347
|
*/
|
|
6278
5348
|
async attemptConnect(decl, preResolved) {
|
|
6279
5349
|
this.opts.onStatusChange?.({
|
|
@@ -6299,20 +5369,10 @@ var ConnectorManager = class {
|
|
|
6299
5369
|
},
|
|
6300
5370
|
() => connector.connect(decl, ctx)
|
|
6301
5371
|
);
|
|
6302
|
-
const previous = this.active.get(decl.id);
|
|
6303
|
-
if (previous?.watchHandle) {
|
|
6304
|
-
try {
|
|
6305
|
-
await previous.watchHandle.close();
|
|
6306
|
-
} catch {
|
|
6307
|
-
}
|
|
6308
|
-
}
|
|
6309
5372
|
this.active.set(decl.id, {
|
|
6310
5373
|
connector,
|
|
6311
5374
|
handle,
|
|
6312
|
-
declaration: decl
|
|
6313
|
-
placeholderTarget: previous?.placeholderTarget,
|
|
6314
|
-
fleetHealthGate: previous?.fleetHealthGate,
|
|
6315
|
-
fleetHealthTarget: previous?.fleetHealthTarget
|
|
5375
|
+
declaration: decl
|
|
6316
5376
|
});
|
|
6317
5377
|
this.opts.onStatusChange?.({
|
|
6318
5378
|
type: "connector_status",
|
|
@@ -6343,216 +5403,26 @@ var ConnectorManager = class {
|
|
|
6343
5403
|
}
|
|
6344
5404
|
}
|
|
6345
5405
|
/**
|
|
6346
|
-
*
|
|
6347
|
-
*
|
|
6348
|
-
* prompt before the background mount completes.
|
|
6349
|
-
*
|
|
6350
|
-
* Before exposing the target, a read-only barrier is established so the agent
|
|
6351
|
-
* cannot write a phantom workspace into a not-yet-mounted remote.
|
|
6352
|
-
*
|
|
6353
|
-
* Fleet-managed mounts skip the *static* boot-time barrier (the host already
|
|
6354
|
-
* bound the live share at the target — a static barrier would shadow real
|
|
6355
|
-
* content) but instead get a *dynamic* health gate: while the host fleet rclone
|
|
6356
|
-
* is healthy the target stays writable exactly as before; when it wedges, the
|
|
6357
|
-
* gate establishes the same read-only barrier until the mount recovers.
|
|
6358
|
-
*/
|
|
6359
|
-
async preRegisterDeferred(decl, connector) {
|
|
6360
|
-
const mountTarget = this.resolveMountTarget(decl);
|
|
6361
|
-
if (!existsSync(mountTarget)) mkdirSync(mountTarget, { recursive: true });
|
|
6362
|
-
const fleetManaged = isFleetManaged(decl.id);
|
|
6363
|
-
let placeholderTarget;
|
|
6364
|
-
let fleetHealthGate;
|
|
6365
|
-
let fleetHealthTarget;
|
|
6366
|
-
if (fleetManaged) {
|
|
6367
|
-
fleetHealthGate = new FleetHealthGate(mountTarget, this.placeholderBarrier, this.mgrLog);
|
|
6368
|
-
fleetHealthTarget = mountTarget;
|
|
6369
|
-
} else {
|
|
6370
|
-
try {
|
|
6371
|
-
await this.placeholderBarrier.establish(mountTarget, this.mgrLog);
|
|
6372
|
-
placeholderTarget = mountTarget;
|
|
6373
|
-
} catch (err) {
|
|
6374
|
-
this.mgrLog.error("deferred mount barrier establish failed", err, { id: decl.id });
|
|
6375
|
-
}
|
|
6376
|
-
}
|
|
6377
|
-
const handle = {
|
|
6378
|
-
id: decl.id,
|
|
6379
|
-
driver: connector.name ?? decl.driver,
|
|
6380
|
-
access: decl.access,
|
|
6381
|
-
status: "connecting",
|
|
6382
|
-
mountPath: mountTarget,
|
|
6383
|
-
// Empty (not null) so a best-effort disconnect of a still-provisional
|
|
6384
|
-
// entry — e.g. a deferred mount that errored — finds no live handle and
|
|
6385
|
-
// returns cleanly instead of dereferencing null state.
|
|
6386
|
-
state: {}
|
|
6387
|
-
};
|
|
6388
|
-
this.active.set(decl.id, {
|
|
6389
|
-
connector,
|
|
6390
|
-
handle,
|
|
6391
|
-
declaration: decl,
|
|
6392
|
-
placeholderTarget,
|
|
6393
|
-
fleetHealthGate,
|
|
6394
|
-
fleetHealthTarget
|
|
6395
|
-
});
|
|
6396
|
-
if (fleetHealthGate) this.ensureFleetHealthPoll();
|
|
6397
|
-
}
|
|
6398
|
-
/**
|
|
6399
|
-
* Start the periodic local fleet-health probe if it is not already running and
|
|
6400
|
-
* the poll interval is enabled. Each tick probes every fleet target and feeds
|
|
6401
|
-
* the verdict to its gate (hysteresis lives in the gate). The probe catches both
|
|
6402
|
-
* directly-detectable failures (EIO / dead bind) AND host-mount recovery while a
|
|
6403
|
-
* placeholder is up; silent degradation is covered by the platform signal via
|
|
6404
|
-
* `reportFleetHealth`.
|
|
6405
|
-
*/
|
|
6406
|
-
ensureFleetHealthPoll() {
|
|
6407
|
-
if (this.fleetHealthTimer || this.fleetHealthPollMs <= 0 || this.disposed) return;
|
|
6408
|
-
this.fleetHealthTimer = setInterval(() => {
|
|
6409
|
-
void this.runFleetHealthProbe();
|
|
6410
|
-
}, this.fleetHealthPollMs);
|
|
6411
|
-
this.fleetHealthTimer.unref?.();
|
|
6412
|
-
}
|
|
6413
|
-
async runFleetHealthProbe() {
|
|
6414
|
-
if (this.disposed) return;
|
|
6415
|
-
const gates = [...this.active.values()].filter(
|
|
6416
|
-
(e) => e.fleetHealthGate != null && e.fleetHealthTarget != null
|
|
6417
|
-
);
|
|
6418
|
-
await Promise.all(
|
|
6419
|
-
gates.map(async (e) => {
|
|
6420
|
-
const verdict = e.fleetHealthGate.isGated ? await this.probeGatedRecovery(e.fleetHealthGate) : await probeFleetHealth(e.fleetHealthTarget);
|
|
6421
|
-
await e.fleetHealthGate.observe(verdict).catch(() => {
|
|
6422
|
-
});
|
|
6423
|
-
})
|
|
6424
|
-
);
|
|
6425
|
-
}
|
|
6426
|
-
/**
|
|
6427
|
-
* Detect host-mount RECOVERY for an already-gated target so the placeholder lifts
|
|
6428
|
-
* edge-triggered (no restart). Over an opaque mask a stat/readdir reads the 0555
|
|
6429
|
-
* mask and falsely reads healthy, so recovery is detected via the mount table;
|
|
6430
|
-
* non-opaque surfaces hold their current surface until the platform signal lifts.
|
|
6431
|
-
* Rationale: `_devlog/notes/2026-06-17-fleet-gate-edge-triggered-recovery.md`.
|
|
6432
|
-
*/
|
|
6433
|
-
async probeGatedRecovery(gate) {
|
|
6434
|
-
const target = gate.mountTarget;
|
|
6435
|
-
if (!(this.placeholderBarrier.isOpaque?.(target) ?? false)) {
|
|
6436
|
-
return gate.currentSurface === "ro" ? "degraded" : "error";
|
|
6437
|
-
}
|
|
6438
|
-
return await this.fleetMountRecoveryProbe(target) ? "healthy" : "error";
|
|
6439
|
-
}
|
|
6440
|
-
/**
|
|
6441
|
-
* Feed an externally-derived fleet-health verdict for a connector into its gate.
|
|
6442
|
-
*
|
|
6443
|
-
* This is the consume side of the platform fleet-health signal: the host fleet
|
|
6444
|
-
* health poll knows `core/stats.errors` and can detect a *silently-empty-but-
|
|
6445
|
-
* writable* degraded mount that the local probe cannot. The runner watches the
|
|
6446
|
-
* `<workspace>/.skaile/fleet-health/<connectorId>` flag and forwards its verdict
|
|
6447
|
-
* here. Unknown / non-fleet ids are ignored.
|
|
6448
|
-
*
|
|
6449
|
-
* The verdict drives the gate across three surfaces (writable / read-only-
|
|
6450
|
-
* readable / masked): `healthy` lifts any barrier, `degraded` downgrades to a
|
|
6451
|
-
* read-only surface that keeps the live content visible, `error` keeps the
|
|
6452
|
-
* opaque mask.
|
|
6453
|
-
*
|
|
6454
|
-
* A deferred remote mount may have been brought up with the *static* boot-time
|
|
6455
|
-
* barrier (it was not fleet-managed at connect — e.g. the host bind landed
|
|
6456
|
-
* late, after `connectAll` placed the not-ready barrier). When a fleet verdict
|
|
6457
|
-
* later arrives for such a connector, a gate is created lazily over that same
|
|
6458
|
-
* barrier (seeded `masked`, matching the static barrier already in place) so the
|
|
6459
|
-
* authoritative platform signal can lift or downgrade it — otherwise a late-
|
|
6460
|
-
* arriving live mount stays masked forever even after it goes healthy.
|
|
6461
|
-
*/
|
|
6462
|
-
async reportFleetHealth(connectorId, verdict) {
|
|
6463
|
-
const entry = this.active.get(connectorId);
|
|
6464
|
-
if (!entry) return;
|
|
6465
|
-
if (!entry.fleetHealthGate) {
|
|
6466
|
-
if (!entry.placeholderTarget) return;
|
|
6467
|
-
entry.fleetHealthTarget = entry.placeholderTarget;
|
|
6468
|
-
entry.fleetHealthGate = new FleetHealthGate(
|
|
6469
|
-
entry.placeholderTarget,
|
|
6470
|
-
this.placeholderBarrier,
|
|
6471
|
-
this.mgrLog,
|
|
6472
|
-
{ initialSurface: "masked" }
|
|
6473
|
-
);
|
|
6474
|
-
}
|
|
6475
|
-
await entry.fleetHealthGate.observe(verdict).catch(() => {
|
|
6476
|
-
});
|
|
6477
|
-
}
|
|
6478
|
-
/** Drive all deferred mounts concurrently; resolves when every one settles. */
|
|
6479
|
-
async connectDeferred(deferred) {
|
|
6480
|
-
await Promise.allSettled(
|
|
6481
|
-
deferred.map(({ decl, connector }) => this.connectDeferredOne(decl, connector))
|
|
6482
|
-
);
|
|
6483
|
-
}
|
|
6484
|
-
/**
|
|
6485
|
-
* Background-connect one deferred mount. On success `attemptConnect` swaps the
|
|
6486
|
-
* provisional entry for the real handle; on failure the provisional entry is
|
|
6487
|
-
* marked `error` so the mount path keeps rendering as "still syncing". If the
|
|
6488
|
-
* manager was disposed mid-flight, the connector that came up is torn down
|
|
6489
|
-
* immediately so no rclone process leaks.
|
|
5406
|
+
* Compatibility no-op for the removed in-container fleet health gate. Host
|
|
5407
|
+
* fleet-managed cloud mounts are verified at connect time by their drivers.
|
|
6490
5408
|
*/
|
|
6491
|
-
async
|
|
6492
|
-
if (this.disposed) return;
|
|
6493
|
-
const preEntry = this.active.get(decl.id);
|
|
6494
|
-
if (preEntry?.placeholderTarget && this.placeholderBarrier.prepareForStackedMount) {
|
|
6495
|
-
await this.placeholderBarrier.prepareForStackedMount(preEntry.placeholderTarget, this.mgrLog).catch(() => {
|
|
6496
|
-
});
|
|
6497
|
-
}
|
|
6498
|
-
const result = await this.attemptConnect(decl, connector);
|
|
6499
|
-
if (!result.connected) {
|
|
6500
|
-
const entry2 = this.active.get(decl.id);
|
|
6501
|
-
if (entry2 && entry2.handle.status === "connecting") entry2.handle.status = "error";
|
|
6502
|
-
const failTarget = entry2?.placeholderTarget ?? preEntry?.placeholderTarget;
|
|
6503
|
-
if (failTarget && this.placeholderBarrier.restoreBarrier) {
|
|
6504
|
-
await this.placeholderBarrier.restoreBarrier(failTarget, this.mgrLog).catch(() => {
|
|
6505
|
-
});
|
|
6506
|
-
}
|
|
6507
|
-
return;
|
|
6508
|
-
}
|
|
6509
|
-
const entry = this.active.get(decl.id);
|
|
6510
|
-
if (entry?.placeholderTarget) {
|
|
6511
|
-
await this.placeholderBarrier.onMountLive(entry.placeholderTarget, this.mgrLog).catch(() => {
|
|
6512
|
-
});
|
|
6513
|
-
}
|
|
6514
|
-
if (this.disposed && this.active.has(decl.id)) {
|
|
6515
|
-
await this.disconnect(decl.id).catch(() => {
|
|
6516
|
-
});
|
|
6517
|
-
}
|
|
5409
|
+
async reportFleetHealth(_connectorId, _verdict) {
|
|
6518
5410
|
}
|
|
6519
5411
|
/**
|
|
6520
|
-
*
|
|
6521
|
-
* settled. Resolves immediately when none are in flight. Useful for tests and
|
|
6522
|
-
* for callers that want to await full readiness.
|
|
5412
|
+
* Compatibility no-op for the removed deferred rclone mount path.
|
|
6523
5413
|
*/
|
|
6524
5414
|
async whenDeferredMountsSettled() {
|
|
6525
|
-
if (this.deferredConnect) await this.deferredConnect.catch(() => {
|
|
6526
|
-
});
|
|
6527
5415
|
}
|
|
6528
5416
|
/** Disconnect all connectors and stop all watchers. Best-effort — ignores disconnect errors. */
|
|
6529
5417
|
async disconnectAll() {
|
|
6530
5418
|
this.mgrLog.info("disconnectAll start", { count: this.active.size });
|
|
6531
|
-
this.disposed = true;
|
|
6532
5419
|
this.disposeController.abort();
|
|
6533
|
-
if (this.fleetHealthTimer) {
|
|
6534
|
-
clearInterval(this.fleetHealthTimer);
|
|
6535
|
-
this.fleetHealthTimer = void 0;
|
|
6536
|
-
}
|
|
6537
|
-
if (this.deferredConnect) {
|
|
6538
|
-
try {
|
|
6539
|
-
await this.deferredConnect;
|
|
6540
|
-
} catch {
|
|
6541
|
-
}
|
|
6542
|
-
this.deferredConnect = void 0;
|
|
6543
|
-
}
|
|
6544
5420
|
await this.unwatchAll();
|
|
6545
5421
|
for (const [id, entry] of this.active) {
|
|
6546
5422
|
try {
|
|
6547
5423
|
await entry.connector.disconnect(entry.handle);
|
|
6548
5424
|
} catch {
|
|
6549
5425
|
}
|
|
6550
|
-
if (entry.placeholderTarget) {
|
|
6551
|
-
await this.placeholderBarrier.remove(entry.placeholderTarget, this.mgrLog).catch(() => {
|
|
6552
|
-
});
|
|
6553
|
-
}
|
|
6554
|
-
if (entry.fleetHealthGate) await entry.fleetHealthGate.dispose().catch(() => {
|
|
6555
|
-
});
|
|
6556
5426
|
this.opts.onStatusChange?.({
|
|
6557
5427
|
type: "connector_status",
|
|
6558
5428
|
connectorId: id,
|
|
@@ -6568,10 +5438,6 @@ var ConnectorManager = class {
|
|
|
6568
5438
|
* Connect a single connector (hot-plug). Resolves fields and allocates
|
|
6569
5439
|
* mount target if the connector has a filesystem face.
|
|
6570
5440
|
*
|
|
6571
|
-
* This path connects immediately and does NOT establish the read-only
|
|
6572
|
-
* deferred-mount barrier — bring `deferMount` (rclone-backed) connectors up
|
|
6573
|
-
* through `connectAll({ deferMounts: true })` so the not-ready guarantee holds.
|
|
6574
|
-
*
|
|
6575
5441
|
* @returns The `ConnectorHandle` for subsequent operations.
|
|
6576
5442
|
*/
|
|
6577
5443
|
async connect(declaration) {
|
|
@@ -6641,12 +5507,6 @@ var ConnectorManager = class {
|
|
|
6641
5507
|
try {
|
|
6642
5508
|
await entry.connector.disconnect(entry.handle);
|
|
6643
5509
|
} finally {
|
|
6644
|
-
if (entry.placeholderTarget) {
|
|
6645
|
-
await this.placeholderBarrier.remove(entry.placeholderTarget, this.mgrLog).catch(() => {
|
|
6646
|
-
});
|
|
6647
|
-
}
|
|
6648
|
-
if (entry.fleetHealthGate) await entry.fleetHealthGate.dispose().catch(() => {
|
|
6649
|
-
});
|
|
6650
5510
|
this.active.delete(id);
|
|
6651
5511
|
this.releaseMountTarget(id);
|
|
6652
5512
|
this.opts.onStatusChange?.({
|
|
@@ -6822,7 +5682,7 @@ var ConnectorManager = class {
|
|
|
6822
5682
|
}
|
|
6823
5683
|
return entry.connector.tools;
|
|
6824
5684
|
}
|
|
6825
|
-
async read(id,
|
|
5685
|
+
async read(id, path) {
|
|
6826
5686
|
const entry = this.get(id);
|
|
6827
5687
|
const tools = this.requireToolFace(id, entry);
|
|
6828
5688
|
if (!tools.read) {
|
|
@@ -6835,10 +5695,10 @@ var ConnectorManager = class {
|
|
|
6835
5695
|
"skaile.connector.driver": entry.connector.name,
|
|
6836
5696
|
"skaile.connector.operation": "read"
|
|
6837
5697
|
},
|
|
6838
|
-
() => tools.read(entry.handle,
|
|
5698
|
+
() => tools.read(entry.handle, path)
|
|
6839
5699
|
);
|
|
6840
5700
|
}
|
|
6841
|
-
async write(id,
|
|
5701
|
+
async write(id, path, content) {
|
|
6842
5702
|
const entry = this.get(id);
|
|
6843
5703
|
if (entry.declaration.access === "read-only") {
|
|
6844
5704
|
throw new Error(`Connector "${id}" is read-only`);
|
|
@@ -6854,10 +5714,10 @@ var ConnectorManager = class {
|
|
|
6854
5714
|
"skaile.connector.driver": entry.connector.name,
|
|
6855
5715
|
"skaile.connector.operation": "write"
|
|
6856
5716
|
},
|
|
6857
|
-
() => tools.write(entry.handle,
|
|
5717
|
+
() => tools.write(entry.handle, path, content)
|
|
6858
5718
|
);
|
|
6859
5719
|
}
|
|
6860
|
-
async delete(id,
|
|
5720
|
+
async delete(id, path) {
|
|
6861
5721
|
const entry = this.get(id);
|
|
6862
5722
|
if (entry.declaration.access === "read-only") {
|
|
6863
5723
|
throw new Error(`Connector "${id}" is read-only`);
|
|
@@ -6873,10 +5733,10 @@ var ConnectorManager = class {
|
|
|
6873
5733
|
"skaile.connector.driver": entry.connector.name,
|
|
6874
5734
|
"skaile.connector.operation": "delete"
|
|
6875
5735
|
},
|
|
6876
|
-
() => tools.delete(entry.handle,
|
|
5736
|
+
() => tools.delete(entry.handle, path)
|
|
6877
5737
|
);
|
|
6878
5738
|
}
|
|
6879
|
-
async list(id,
|
|
5739
|
+
async list(id, path, options) {
|
|
6880
5740
|
const entry = this.get(id);
|
|
6881
5741
|
const tools = this.requireToolFace(id, entry);
|
|
6882
5742
|
if (!tools.list) {
|
|
@@ -6889,7 +5749,7 @@ var ConnectorManager = class {
|
|
|
6889
5749
|
"skaile.connector.driver": entry.connector.name,
|
|
6890
5750
|
"skaile.connector.operation": "list"
|
|
6891
5751
|
},
|
|
6892
|
-
() => tools.list(entry.handle,
|
|
5752
|
+
() => tools.list(entry.handle, path, options)
|
|
6893
5753
|
);
|
|
6894
5754
|
}
|
|
6895
5755
|
async search(id, query, options) {
|
|
@@ -7030,9 +5890,9 @@ async function buildSdkConnectorTools(manager, options) {
|
|
|
7030
5890
|
connector_id: z4.string().describe("Connector ID"),
|
|
7031
5891
|
path: z4.string().describe("Path within the connector")
|
|
7032
5892
|
},
|
|
7033
|
-
handler: async ({ connector_id, path
|
|
5893
|
+
handler: async ({ connector_id, path }) => {
|
|
7034
5894
|
try {
|
|
7035
|
-
const content = await manager.read(connector_id,
|
|
5895
|
+
const content = await manager.read(connector_id, path);
|
|
7036
5896
|
return {
|
|
7037
5897
|
content: [{ type: "text", text: content?.data?.toString() ?? "(empty)" }]
|
|
7038
5898
|
};
|
|
@@ -7054,11 +5914,11 @@ async function buildSdkConnectorTools(manager, options) {
|
|
|
7054
5914
|
},
|
|
7055
5915
|
handler: async ({
|
|
7056
5916
|
connector_id,
|
|
7057
|
-
path
|
|
5917
|
+
path,
|
|
7058
5918
|
content
|
|
7059
5919
|
}) => {
|
|
7060
5920
|
try {
|
|
7061
|
-
await manager.write(connector_id,
|
|
5921
|
+
await manager.write(connector_id, path, { data: content });
|
|
7062
5922
|
return { content: [{ type: "text", text: "Written." }] };
|
|
7063
5923
|
} catch (err) {
|
|
7064
5924
|
return {
|
|
@@ -7078,11 +5938,11 @@ async function buildSdkConnectorTools(manager, options) {
|
|
|
7078
5938
|
},
|
|
7079
5939
|
handler: async ({
|
|
7080
5940
|
connector_id,
|
|
7081
|
-
path
|
|
5941
|
+
path,
|
|
7082
5942
|
recursive
|
|
7083
5943
|
}) => {
|
|
7084
5944
|
try {
|
|
7085
|
-
const entries = await manager.list(connector_id,
|
|
5945
|
+
const entries = await manager.list(connector_id, path, { recursive });
|
|
7086
5946
|
return { content: [{ type: "text", text: JSON.stringify(entries, null, 2) }] };
|
|
7087
5947
|
} catch (err) {
|
|
7088
5948
|
return {
|
|
@@ -7118,9 +5978,9 @@ async function buildSdkConnectorTools(manager, options) {
|
|
|
7118
5978
|
connector_id: z4.string().describe("Connector ID"),
|
|
7119
5979
|
path: z4.string().describe("Path to delete")
|
|
7120
5980
|
},
|
|
7121
|
-
handler: async ({ connector_id, path
|
|
5981
|
+
handler: async ({ connector_id, path }) => {
|
|
7122
5982
|
try {
|
|
7123
|
-
const ok = await manager.delete(connector_id,
|
|
5983
|
+
const ok = await manager.delete(connector_id, path);
|
|
7124
5984
|
return { content: [{ type: "text", text: ok ? "Deleted." : "Not found." }] };
|
|
7125
5985
|
} catch (err) {
|
|
7126
5986
|
return {
|
|
@@ -7383,6 +6243,45 @@ printf 'password=%s\\n' "$pass"
|
|
|
7383
6243
|
printf '\\n'
|
|
7384
6244
|
`;
|
|
7385
6245
|
}
|
|
6246
|
+
function isFleetManaged(connectorId) {
|
|
6247
|
+
const raw = process.env.SKAILE_FLEET_MANAGED_MOUNTS ?? "";
|
|
6248
|
+
if (raw.length === 0) return false;
|
|
6249
|
+
return raw.split(",").map((segment) => segment.trim()).filter((segment) => segment.length > 0).includes(connectorId);
|
|
6250
|
+
}
|
|
6251
|
+
async function ensureFleetMounted(dir, log, options) {
|
|
6252
|
+
const timeoutMs = options?.timeoutMs ?? 3e3;
|
|
6253
|
+
const pollIntervalMs = options?.pollIntervalMs ?? 100;
|
|
6254
|
+
const deadline = Date.now() + timeoutMs;
|
|
6255
|
+
let lastErr = null;
|
|
6256
|
+
while (Date.now() < deadline) {
|
|
6257
|
+
try {
|
|
6258
|
+
const st = await stat(dir);
|
|
6259
|
+
if (!st.isDirectory()) {
|
|
6260
|
+
throw new Error(`fleet mountpoint ${dir} is not a directory`);
|
|
6261
|
+
}
|
|
6262
|
+
await readdir(dir);
|
|
6263
|
+
log.info("fleet bind mount verified", { dir });
|
|
6264
|
+
return;
|
|
6265
|
+
} catch (err) {
|
|
6266
|
+
lastErr = err;
|
|
6267
|
+
await new Promise((resolve5) => setTimeout(resolve5, pollIntervalMs));
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
6270
|
+
throw new Error(
|
|
6271
|
+
`fleet mount ${dir} not ready after ${timeoutMs}ms \u2014 check SKAILE_FLEET_MANAGED_MOUNTS env var and host bind: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
|
|
6272
|
+
);
|
|
6273
|
+
}
|
|
6274
|
+
async function ensureDirMode(dir, log) {
|
|
6275
|
+
await mkdir(dir, { recursive: true });
|
|
6276
|
+
try {
|
|
6277
|
+
await chmod(dir, 504);
|
|
6278
|
+
} catch (err) {
|
|
6279
|
+
log?.debug("chmod 0770 skipped (EPERM \u2014 entrypoint heal expected)", {
|
|
6280
|
+
path: dir,
|
|
6281
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6282
|
+
});
|
|
6283
|
+
}
|
|
6284
|
+
}
|
|
7386
6285
|
|
|
7387
6286
|
// connectors/src/log-buffer.ts
|
|
7388
6287
|
var LogBuffer = class {
|
|
@@ -7436,19 +6335,19 @@ var ManagedGitconfigCollisionError = class extends Error {
|
|
|
7436
6335
|
var blockStart = (mountId) => `# skaile-mount: ${mountId}`;
|
|
7437
6336
|
var blockEnd = (mountId) => `# skaile-mount: ${mountId} (end)`;
|
|
7438
6337
|
var credentialTag = (mountId) => `# skaile-mount: ${mountId}`;
|
|
7439
|
-
function readFileOrEmpty(
|
|
6338
|
+
function readFileOrEmpty(path) {
|
|
7440
6339
|
try {
|
|
7441
|
-
return readFileSync(
|
|
6340
|
+
return readFileSync(path, "utf-8");
|
|
7442
6341
|
} catch {
|
|
7443
6342
|
return "";
|
|
7444
6343
|
}
|
|
7445
6344
|
}
|
|
7446
|
-
function atomicWrite(
|
|
7447
|
-
const dir = dirname(
|
|
6345
|
+
function atomicWrite(path, content, mode) {
|
|
6346
|
+
const dir = dirname(path);
|
|
7448
6347
|
if (!existsSync(dir)) {
|
|
7449
6348
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
7450
6349
|
}
|
|
7451
|
-
const tmp = `${
|
|
6350
|
+
const tmp = `${path}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
7452
6351
|
const fd = openSync(tmp, "w", mode);
|
|
7453
6352
|
try {
|
|
7454
6353
|
writeSync(fd, content);
|
|
@@ -7456,7 +6355,7 @@ function atomicWrite(path3, content, mode) {
|
|
|
7456
6355
|
closeSync(fd);
|
|
7457
6356
|
}
|
|
7458
6357
|
chmodSync(tmp, mode);
|
|
7459
|
-
renameSync(tmp,
|
|
6358
|
+
renameSync(tmp, path);
|
|
7460
6359
|
}
|
|
7461
6360
|
function appendForeignLine(blocks, line) {
|
|
7462
6361
|
const last = blocks[blocks.length - 1];
|
|
@@ -7522,13 +6421,13 @@ function buildCredentialLine(urlPrefix, token, mountId) {
|
|
|
7522
6421
|
const encodedToken = encodeURIComponent(token);
|
|
7523
6422
|
return `${scheme}://x-access-token:${encodedToken}@${host} ${credentialTag(mountId)}`;
|
|
7524
6423
|
}
|
|
7525
|
-
function readCredentialLines(
|
|
7526
|
-
return readFileOrEmpty(
|
|
6424
|
+
function readCredentialLines(path) {
|
|
6425
|
+
return readFileOrEmpty(path).split("\n").filter((l) => l.length > 0);
|
|
7527
6426
|
}
|
|
7528
|
-
function writeCredentialLines(
|
|
6427
|
+
function writeCredentialLines(path, lines) {
|
|
7529
6428
|
const content = lines.length > 0 ? `${lines.join("\n")}
|
|
7530
6429
|
` : "";
|
|
7531
|
-
atomicWrite(
|
|
6430
|
+
atomicWrite(path, content, 416);
|
|
7532
6431
|
}
|
|
7533
6432
|
function findExistingBlock(blocks, mountId) {
|
|
7534
6433
|
return blocks.find((b) => b.mountId === mountId);
|
|
@@ -7632,156 +6531,191 @@ function isPackageResolvable(pkg, projectDir) {
|
|
|
7632
6531
|
function findMissingPackages(packages, projectDir) {
|
|
7633
6532
|
return packages.filter((p) => !isPackageResolvable(p, projectDir));
|
|
7634
6533
|
}
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
let disposed = false;
|
|
7651
|
-
let lastRefreshAt = Number.NEGATIVE_INFINITY;
|
|
7652
|
-
function clearTimer() {
|
|
7653
|
-
if (timer !== null) {
|
|
7654
|
-
clearTimeoutImpl(timer);
|
|
7655
|
-
timer = null;
|
|
7656
|
-
}
|
|
7657
|
-
}
|
|
7658
|
-
function cancel() {
|
|
7659
|
-
disposed = true;
|
|
7660
|
-
clearTimer();
|
|
7661
|
-
}
|
|
7662
|
-
async function fire(reason) {
|
|
7663
|
-
if (refreshing || disposed) return;
|
|
7664
|
-
refreshing = true;
|
|
7665
|
-
try {
|
|
7666
|
-
let result;
|
|
7667
|
-
try {
|
|
7668
|
-
result = await deps.tokenMediator({ connectorId: deps.connectorId, reason });
|
|
7669
|
-
} catch (err) {
|
|
7670
|
-
deps.log(
|
|
7671
|
-
`[rclone-token-refresh] mediator threw: ${err instanceof Error ? err.message : String(err)}; reactive auth-error path remains as safety net`
|
|
7672
|
-
);
|
|
7673
|
-
return;
|
|
7674
|
-
}
|
|
7675
|
-
if (disposed) return;
|
|
7676
|
-
if (!result.ok) {
|
|
7677
|
-
deps.log(
|
|
7678
|
-
`[rclone-token-refresh] mediator refused (${result.code}: ${result.message}); reactive auth-error path remains as safety net`
|
|
7679
|
-
);
|
|
7680
|
-
return;
|
|
7681
|
-
}
|
|
7682
|
-
if (!result.token) {
|
|
7683
|
-
deps.log(
|
|
7684
|
-
"[rclone-token-refresh] mediator returned an empty token; reactive auth-error path remains as safety net"
|
|
7685
|
-
);
|
|
7686
|
-
return;
|
|
7687
|
-
}
|
|
7688
|
-
const handle = deps.handleRef.current;
|
|
7689
|
-
if (!handle) {
|
|
7690
|
-
deps.log("[rclone-token-refresh] no live rclone handle to recycle; skipping refresh");
|
|
7691
|
-
return;
|
|
7692
|
-
}
|
|
7693
|
-
const rcloneConfig = await deps.renderConfig(result.token);
|
|
7694
|
-
if (disposed) return;
|
|
7695
|
-
let next;
|
|
7696
|
-
try {
|
|
7697
|
-
next = await deps.rclone.reconfigure(handle, rcloneConfig);
|
|
7698
|
-
} catch (err) {
|
|
7699
|
-
deps.log(
|
|
7700
|
-
`[rclone-token-refresh] reconfigure failed: ${err instanceof Error ? err.message : String(err)}; reactive auth-error path remains as safety net`
|
|
7701
|
-
);
|
|
7702
|
-
return;
|
|
6534
|
+
var PortPool = class _PortPool {
|
|
6535
|
+
min;
|
|
6536
|
+
max;
|
|
6537
|
+
inUse = /* @__PURE__ */ new Set();
|
|
6538
|
+
constructor(range) {
|
|
6539
|
+
this.min = range[0];
|
|
6540
|
+
this.max = range[1];
|
|
6541
|
+
}
|
|
6542
|
+
/** Allocate the next free port from the range. Checks OS for collisions. */
|
|
6543
|
+
async allocate() {
|
|
6544
|
+
for (let port = this.min; port <= this.max; port++) {
|
|
6545
|
+
if (this.inUse.has(port)) continue;
|
|
6546
|
+
if (!await _PortPool.isPortFree(port)) {
|
|
6547
|
+
this.inUse.add(port);
|
|
6548
|
+
continue;
|
|
7703
6549
|
}
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
6550
|
+
this.inUse.add(port);
|
|
6551
|
+
return port;
|
|
6552
|
+
}
|
|
6553
|
+
throw new Error(`No free port in range ${this.min}-${this.max}`);
|
|
6554
|
+
}
|
|
6555
|
+
/** Mark a port as in-use (for explicit port args or registered instances). */
|
|
6556
|
+
reserve(port) {
|
|
6557
|
+
this.inUse.add(port);
|
|
6558
|
+
}
|
|
6559
|
+
/** Return a port to the pool. */
|
|
6560
|
+
release(port) {
|
|
6561
|
+
this.inUse.delete(port);
|
|
6562
|
+
}
|
|
6563
|
+
/** Check if a port is currently tracked as in-use. */
|
|
6564
|
+
isInUse(port) {
|
|
6565
|
+
return this.inUse.has(port);
|
|
6566
|
+
}
|
|
6567
|
+
/** Quick TCP connect test — returns true if nothing is listening. */
|
|
6568
|
+
static isPortFree(port, host = "127.0.0.1") {
|
|
6569
|
+
return new Promise((resolve5) => {
|
|
6570
|
+
const socket = createConnection({ port, host });
|
|
6571
|
+
socket.once("connect", () => {
|
|
6572
|
+
socket.destroy();
|
|
6573
|
+
resolve5(false);
|
|
6574
|
+
});
|
|
6575
|
+
socket.once("error", () => {
|
|
6576
|
+
socket.destroy();
|
|
6577
|
+
resolve5(true);
|
|
6578
|
+
});
|
|
6579
|
+
socket.setTimeout(500, () => {
|
|
6580
|
+
socket.destroy();
|
|
6581
|
+
resolve5(true);
|
|
6582
|
+
});
|
|
6583
|
+
});
|
|
6584
|
+
}
|
|
6585
|
+
};
|
|
6586
|
+
function globToMatcher(pattern) {
|
|
6587
|
+
const segmentMatch = pattern.match(/^\*\*\/([^*/?]+)\/\*\*$/);
|
|
6588
|
+
if (segmentMatch) {
|
|
6589
|
+
const name = segmentMatch[1];
|
|
6590
|
+
return (p) => p.includes(`/${name}/`) || p.endsWith(`/${name}`) || p === name;
|
|
6591
|
+
}
|
|
6592
|
+
const dotSegment = pattern.match(/^\*\*\/(\.[\w#]+)(?:\/\*\*)?$/);
|
|
6593
|
+
if (dotSegment) {
|
|
6594
|
+
const name = dotSegment[1];
|
|
6595
|
+
return (p) => p.includes(`/${name}/`) || p.endsWith(`/${name}`) || p === name;
|
|
6596
|
+
}
|
|
6597
|
+
const extMatch = pattern.match(/^\*\*\/\*(\.\w+(?:\.\*)?)/);
|
|
6598
|
+
if (extMatch) {
|
|
6599
|
+
const suffix = extMatch[1];
|
|
6600
|
+
if (suffix.endsWith(".*")) {
|
|
6601
|
+
const base = suffix.slice(0, -2);
|
|
6602
|
+
return (p) => p.includes(base);
|
|
6603
|
+
}
|
|
6604
|
+
return (p) => p.endsWith(suffix);
|
|
6605
|
+
}
|
|
6606
|
+
const suffixMatch = pattern.match(/^\*\*\/\*(.+)$/);
|
|
6607
|
+
if (suffixMatch) {
|
|
6608
|
+
const sfx = suffixMatch[1];
|
|
6609
|
+
return (p) => p.endsWith(sfx);
|
|
6610
|
+
}
|
|
6611
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
6612
|
+
const re = new RegExp(`^${escaped}$`);
|
|
6613
|
+
return (p) => re.test(p);
|
|
6614
|
+
}
|
|
6615
|
+
var DEFAULT_IGNORED = [
|
|
6616
|
+
(p) => p.includes("/.git/") || p.endsWith("/.git"),
|
|
6617
|
+
(p) => p.includes("/node_modules/") || p.endsWith("/node_modules"),
|
|
6618
|
+
(p) => p.includes("/.connectors/") || p.endsWith("/.connectors"),
|
|
6619
|
+
// Atomic-write tempfiles (Bun.write creates `<name>.tmp.<pid>.<ts>` then renames).
|
|
6620
|
+
// These vanish between readdir and fs.watch, causing EINVAL that crashes the process.
|
|
6621
|
+
(p) => /\.tmp\.\d/.test(p),
|
|
6622
|
+
// Editor swap/backup files that also race with save operations.
|
|
6623
|
+
(p) => p.endsWith(".swp"),
|
|
6624
|
+
(p) => p.endsWith(".swx"),
|
|
6625
|
+
(p) => p.includes("/.#"),
|
|
6626
|
+
(p) => p.endsWith("~")
|
|
6627
|
+
];
|
|
6628
|
+
var DEFAULT_DEBOUNCE_MS = 150;
|
|
6629
|
+
function parseGitignore(rootDir) {
|
|
6630
|
+
const gitignorePath = join(rootDir, ".gitignore");
|
|
6631
|
+
if (!existsSync(gitignorePath)) return [];
|
|
6632
|
+
try {
|
|
6633
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
6634
|
+
const matchers = [];
|
|
6635
|
+
for (const raw of content.split("\n")) {
|
|
6636
|
+
const line = raw.trim();
|
|
6637
|
+
if (!line || line.startsWith("#") || line.startsWith("!")) continue;
|
|
6638
|
+
let pattern = line;
|
|
6639
|
+
if (pattern.startsWith("/")) {
|
|
6640
|
+
pattern = pattern.slice(1);
|
|
6641
|
+
} else if (!pattern.includes("/")) {
|
|
6642
|
+
pattern = `**/${pattern}`;
|
|
6643
|
+
}
|
|
6644
|
+
if (pattern.endsWith("/")) {
|
|
6645
|
+
matchers.push(globToMatcher(`${pattern}**`));
|
|
6646
|
+
} else {
|
|
6647
|
+
matchers.push(globToMatcher(pattern));
|
|
6648
|
+
if (!pattern.includes("*") && !pattern.includes(".")) {
|
|
6649
|
+
matchers.push(globToMatcher(`${pattern}/**`));
|
|
7709
6650
|
}
|
|
7710
|
-
return;
|
|
7711
6651
|
}
|
|
7712
|
-
deps.handleRef.current = next;
|
|
7713
|
-
lastRefreshAt = now2();
|
|
7714
|
-
deps.log(
|
|
7715
|
-
`[rclone-token-refresh] recycled mount with refreshed token (reason=${reason}, expiresAt=${result.expiresAt ?? "none"})`
|
|
7716
|
-
);
|
|
7717
|
-
schedule(result.expiresAt);
|
|
7718
|
-
} finally {
|
|
7719
|
-
refreshing = false;
|
|
7720
|
-
}
|
|
7721
|
-
}
|
|
7722
|
-
function armTimer(fireAtMs) {
|
|
7723
|
-
const delayMs = Math.min(Math.max(0, fireAtMs - now2()), MAX_TIMER_MS);
|
|
7724
|
-
timer = setTimeoutImpl(() => {
|
|
7725
|
-
timer = null;
|
|
7726
|
-
if (now2() < fireAtMs) armTimer(fireAtMs);
|
|
7727
|
-
else void fire("refresh");
|
|
7728
|
-
}, delayMs);
|
|
7729
|
-
timer.unref?.();
|
|
7730
|
-
}
|
|
7731
|
-
function schedule(expiresAtIso) {
|
|
7732
|
-
clearTimer();
|
|
7733
|
-
if (disposed || !expiresAtIso) return;
|
|
7734
|
-
const expiresAtMs = Date.parse(expiresAtIso);
|
|
7735
|
-
if (!Number.isFinite(expiresAtMs)) return;
|
|
7736
|
-
const fireAtMs = expiresAtMs - marginMs;
|
|
7737
|
-
if (fireAtMs - now2() <= 0) {
|
|
7738
|
-
deps.log(`[rclone-token-refresh] token within ${marginMs / 1e3}s of expiry; refreshing now`);
|
|
7739
|
-
} else {
|
|
7740
|
-
deps.log(
|
|
7741
|
-
`[rclone-token-refresh] scheduled in ${Math.round((fireAtMs - now2()) / 1e3)}s (expiresAt=${expiresAtIso})`
|
|
7742
|
-
);
|
|
7743
6652
|
}
|
|
7744
|
-
|
|
6653
|
+
return matchers;
|
|
6654
|
+
} catch {
|
|
6655
|
+
return [];
|
|
7745
6656
|
}
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
6657
|
+
}
|
|
6658
|
+
var CHOKIDAR_EVENT_MAP = {
|
|
6659
|
+
add: "create",
|
|
6660
|
+
change: "edit",
|
|
6661
|
+
unlink: "delete",
|
|
6662
|
+
addDir: "create",
|
|
6663
|
+
unlinkDir: "delete"
|
|
6664
|
+
};
|
|
6665
|
+
async function createFsWatcher(rootDir, callback, options) {
|
|
6666
|
+
const log = createLogger({ kind: "connector", subkind: "watcher", instance: rootDir });
|
|
6667
|
+
const gitignorePatterns = parseGitignore(rootDir);
|
|
6668
|
+
const optionsIgnored = (options?.ignored ?? []).map(
|
|
6669
|
+
(entry) => typeof entry === "string" ? globToMatcher(entry) : entry
|
|
6670
|
+
);
|
|
6671
|
+
const ignored = [...DEFAULT_IGNORED, ...gitignorePatterns, ...optionsIgnored];
|
|
6672
|
+
const debounceMs = options?.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
6673
|
+
const watcher = chokidar.watch(rootDir, {
|
|
6674
|
+
ignoreInitial: true,
|
|
6675
|
+
ignored,
|
|
6676
|
+
awaitWriteFinish: { stabilityThreshold: debounceMs, pollInterval: 50 },
|
|
6677
|
+
usePolling: false
|
|
6678
|
+
});
|
|
6679
|
+
watcher.on("error", (err) => {
|
|
6680
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6681
|
+
log.warn("watcher error (non-fatal)", { message });
|
|
6682
|
+
});
|
|
6683
|
+
for (const [chokidarEvent, action] of Object.entries(CHOKIDAR_EVENT_MAP)) {
|
|
6684
|
+
watcher.on(
|
|
6685
|
+
chokidarEvent,
|
|
6686
|
+
(fullPath) => {
|
|
6687
|
+
const rel = relative(rootDir, fullPath);
|
|
6688
|
+
if (!rel || rel.startsWith("..")) return;
|
|
6689
|
+
const normalizedPath = rel.split("\\").join("/");
|
|
6690
|
+
callback({ path: normalizedPath, action, source: "filesystem" });
|
|
6691
|
+
}
|
|
6692
|
+
);
|
|
7756
6693
|
}
|
|
7757
6694
|
return {
|
|
7758
|
-
|
|
7759
|
-
onAuthError,
|
|
7760
|
-
cancel,
|
|
7761
|
-
isPending: () => timer !== null
|
|
6695
|
+
close: () => watcher.close()
|
|
7762
6696
|
};
|
|
7763
6697
|
}
|
|
7764
6698
|
var WorktreeRegistryImpl = class {
|
|
7765
6699
|
entries = /* @__PURE__ */ new Map();
|
|
7766
|
-
register(
|
|
7767
|
-
let owners = this.entries.get(
|
|
6700
|
+
register(path, owner) {
|
|
6701
|
+
let owners = this.entries.get(path);
|
|
7768
6702
|
if (!owners) {
|
|
7769
6703
|
owners = /* @__PURE__ */ new Set();
|
|
7770
|
-
this.entries.set(
|
|
6704
|
+
this.entries.set(path, owners);
|
|
7771
6705
|
}
|
|
7772
6706
|
owners.add(owner);
|
|
7773
6707
|
}
|
|
7774
|
-
deregister(
|
|
7775
|
-
const owners = this.entries.get(
|
|
6708
|
+
deregister(path, owner) {
|
|
6709
|
+
const owners = this.entries.get(path);
|
|
7776
6710
|
if (!owners) return;
|
|
7777
6711
|
owners.delete(owner);
|
|
7778
|
-
if (owners.size === 0) this.entries.delete(
|
|
6712
|
+
if (owners.size === 0) this.entries.delete(path);
|
|
7779
6713
|
}
|
|
7780
|
-
isInUse(
|
|
7781
|
-
return (this.entries.get(
|
|
6714
|
+
isInUse(path) {
|
|
6715
|
+
return (this.entries.get(path)?.size ?? 0) > 0;
|
|
7782
6716
|
}
|
|
7783
|
-
owners(
|
|
7784
|
-
return [...this.entries.get(
|
|
6717
|
+
owners(path) {
|
|
6718
|
+
return [...this.entries.get(path) ?? []];
|
|
7785
6719
|
}
|
|
7786
6720
|
};
|
|
7787
6721
|
var worktreeRegistry = new WorktreeRegistryImpl();
|
|
@@ -7910,8 +6844,8 @@ var FlowAdapter = class extends AbstractConnector {
|
|
|
7910
6844
|
constructor() {
|
|
7911
6845
|
super();
|
|
7912
6846
|
this.tools = {
|
|
7913
|
-
read: (handle,
|
|
7914
|
-
list: (handle,
|
|
6847
|
+
read: (handle, path) => this.read(handle, path),
|
|
6848
|
+
list: (handle, path, options) => this.list(handle, path, options),
|
|
7915
6849
|
search: (handle, query, options) => this.search(handle, query, options),
|
|
7916
6850
|
describeOperations: (handle) => this.describeOperations(handle),
|
|
7917
6851
|
executeOp: (handle, operation, args) => this.executeOp(handle, operation, args)
|
|
@@ -7955,16 +6889,16 @@ var FlowAdapter = class extends AbstractConnector {
|
|
|
7955
6889
|
s.execution = this.recompute(s);
|
|
7956
6890
|
}
|
|
7957
6891
|
// ── Base operations (private — wired into tools face in constructor) ──
|
|
7958
|
-
async read(handle,
|
|
6892
|
+
async read(handle, path) {
|
|
7959
6893
|
const s = this.s(handle);
|
|
7960
|
-
if (
|
|
6894
|
+
if (path === "" || path === "state") {
|
|
7961
6895
|
return {
|
|
7962
6896
|
data: JSON.stringify(s.execution, null, 2),
|
|
7963
6897
|
contentType: "application/json"
|
|
7964
6898
|
};
|
|
7965
6899
|
}
|
|
7966
|
-
if (
|
|
7967
|
-
const nodeId =
|
|
6900
|
+
if (path.startsWith("node/")) {
|
|
6901
|
+
const nodeId = path.slice(5);
|
|
7968
6902
|
const node = s.flowDef.nodes.find((n) => n.id === nodeId);
|
|
7969
6903
|
if (!node) return null;
|
|
7970
6904
|
const exec = s.execution.nodes[nodeId];
|
|
@@ -8781,6 +7715,6 @@ function createConnector() {
|
|
|
8781
7715
|
return new FlowAdapter();
|
|
8782
7716
|
}
|
|
8783
7717
|
|
|
8784
|
-
export { AbstractConnector, BUILTIN_CONNECTOR_CATALOG, ConnectorFieldMissingError, ConnectorManager, ConnectorStartupError,
|
|
8785
|
-
//# sourceMappingURL=chunk-
|
|
8786
|
-
//# sourceMappingURL=chunk-
|
|
7718
|
+
export { AbstractConnector, BUILTIN_CONNECTOR_CATALOG, ConnectorFieldMissingError, ConnectorManager, ConnectorStartupError, DeployConnector, DevServerConnector, FlowAdapter, GitConnector, GmailConnector, GoogleDriveConnector, LocalConnector, LogBuffer, ManagedGitconfigCollisionError, MattermostConnector, MemoryConnector, MinIOConnector, PortPool, PostgresConnector, S3Connector, SQLiteConnector, SharePointConnector, StaticServerConnector, TunnelConnector, WebDAVConnector, XStateConnector, XStateStoreConnector, __test, atomicReplaceCredential, buildConnectorPromptSection, buildSdkConnectorTools, createConnector, createConnector10, createConnector11, createConnector12, createConnector13, createConnector14, createConnector15, createConnector16, createConnector17, createConnector18, createConnector19, createConnector2, createConnector3, createConnector4, createConnector5, createConnector6, createConnector7, createConnector8, createConnector9, createFsWatcher, createWorktree, defaultSpawn, ensureDirMode, ensureFleetMounted, findGitRoot, findMissingPackages, getConnector, installNpmPackages, isFleetManaged, isPackageResolvable, listConnectors, listWorktrees, readGitSyncStatus, registerBuiltinConnectors, removeMountBlock, renderCredentialHelperScript, tryGetConnector, worktreeRegistry, writeHelperScript, writeMountBlock };
|
|
7719
|
+
//# sourceMappingURL=chunk-WSQ5J7CE.js.map
|
|
7720
|
+
//# sourceMappingURL=chunk-WSQ5J7CE.js.map
|