@skaile/workspaces 3.7.2 → 3.8.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.
Files changed (148) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/dist/app-sdk/index.js +1 -1
  3. package/dist/{asset-feeds-44M2GV2W.js → asset-feeds-7HM3HONM.js} +4 -4
  4. package/dist/{asset-feeds-44M2GV2W.js.map → asset-feeds-7HM3HONM.js.map} +1 -1
  5. package/dist/asset-manager/index.js +1 -1
  6. package/dist/bridge/drivers/claude-sdk.js +231 -24
  7. package/dist/bridge/drivers/claude-sdk.js.map +1 -1
  8. package/dist/bridge/drivers/codex.js +1 -1
  9. package/dist/bridge/drivers/continue.js +2 -2
  10. package/dist/bridge/drivers/echo.js +1 -1
  11. package/dist/bridge/drivers/gemini.js +3 -3
  12. package/dist/bridge/drivers/goose.js +2 -2
  13. package/dist/bridge/drivers/omp.js +1 -1
  14. package/dist/bridge/drivers/opencode.js +2 -2
  15. package/dist/bridge/drivers/qwen.js +3 -3
  16. package/dist/bridge/index.js +2 -2
  17. package/dist/bridge/src/drivers/claude-sdk.d.ts +96 -1
  18. package/dist/bridge/src/drivers/claude-sdk.d.ts.map +1 -1
  19. package/dist/bridge/src/types.d.ts +103 -0
  20. package/dist/bridge/src/types.d.ts.map +1 -1
  21. package/dist/{chunk-IKNQW3AW.js → chunk-2GREL3OD.js} +3 -3
  22. package/dist/{chunk-IKNQW3AW.js.map → chunk-2GREL3OD.js.map} +1 -1
  23. package/dist/{chunk-Z7ILXHIT.js → chunk-2JW5SWIU.js} +6 -6
  24. package/dist/{chunk-Z7ILXHIT.js.map → chunk-2JW5SWIU.js.map} +1 -1
  25. package/dist/{chunk-J4I2YPGE.js → chunk-4WTC6BSV.js} +3 -3
  26. package/dist/{chunk-J4I2YPGE.js.map → chunk-4WTC6BSV.js.map} +1 -1
  27. package/dist/{chunk-JTBF4OB2.js → chunk-6GDVGAIZ.js} +587 -114
  28. package/dist/chunk-6GDVGAIZ.js.map +1 -0
  29. package/dist/{chunk-SID35UAK.js → chunk-7SHXLJQP.js} +3 -3
  30. package/dist/{chunk-SID35UAK.js.map → chunk-7SHXLJQP.js.map} +1 -1
  31. package/dist/{chunk-JF73DP5W.js → chunk-B7TA2XI7.js} +2 -2
  32. package/dist/{chunk-JF73DP5W.js.map → chunk-B7TA2XI7.js.map} +1 -1
  33. package/dist/{chunk-32TJ55W5.js → chunk-BGAAZMBK.js} +2 -2
  34. package/dist/chunk-BGAAZMBK.js.map +1 -0
  35. package/dist/{chunk-XCG63ZNJ.js → chunk-DGDELPOD.js} +3 -3
  36. package/dist/{chunk-XCG63ZNJ.js.map → chunk-DGDELPOD.js.map} +1 -1
  37. package/dist/{chunk-26L2B6P6.js → chunk-FUCYTGAE.js} +4 -4
  38. package/dist/chunk-FUCYTGAE.js.map +1 -0
  39. package/dist/{chunk-F7S6NDIW.js → chunk-IH2KJYAU.js} +142 -13
  40. package/dist/chunk-IH2KJYAU.js.map +1 -0
  41. package/dist/{chunk-VXITGB7G.js → chunk-KNPBMR2P.js} +3 -3
  42. package/dist/{chunk-VXITGB7G.js.map → chunk-KNPBMR2P.js.map} +1 -1
  43. package/dist/{chunk-BCXI27BH.js → chunk-NVYEK4FI.js} +3 -3
  44. package/dist/{chunk-BCXI27BH.js.map → chunk-NVYEK4FI.js.map} +1 -1
  45. package/dist/{chunk-D4VHBAJC.js → chunk-O3LEV6KB.js} +4 -4
  46. package/dist/{chunk-D4VHBAJC.js.map → chunk-O3LEV6KB.js.map} +1 -1
  47. package/dist/{chunk-2GA2UGQI.js → chunk-OZZCPTOG.js} +6 -6
  48. package/dist/{chunk-2GA2UGQI.js.map → chunk-OZZCPTOG.js.map} +1 -1
  49. package/dist/{chunk-NXHRSUKL.js → chunk-PRUCNSBI.js} +4 -4
  50. package/dist/{chunk-NXHRSUKL.js.map → chunk-PRUCNSBI.js.map} +1 -1
  51. package/dist/{chunk-J7SEUNPD.js → chunk-QVQH6GV2.js} +2 -2
  52. package/dist/{chunk-J7SEUNPD.js.map → chunk-QVQH6GV2.js.map} +1 -1
  53. package/dist/{chunk-I5TSXDTD.js → chunk-RVXBOKHK.js} +3 -3
  54. package/dist/{chunk-I5TSXDTD.js.map → chunk-RVXBOKHK.js.map} +1 -1
  55. package/dist/cli/index.js +30 -30
  56. package/dist/connectors/index.js +3 -3
  57. package/dist/connectors/src/connector-manager.d.ts +34 -2
  58. package/dist/connectors/src/connector-manager.d.ts.map +1 -1
  59. package/dist/connectors/src/connector-prompt.d.ts.map +1 -1
  60. package/dist/connectors/src/connector-types.d.ts +25 -0
  61. package/dist/connectors/src/connector-types.d.ts.map +1 -1
  62. package/dist/connectors/src/credential-helper-script.d.ts +48 -0
  63. package/dist/connectors/src/credential-helper-script.d.ts.map +1 -1
  64. package/dist/connectors/src/index.d.ts +3 -3
  65. package/dist/connectors/src/index.d.ts.map +1 -1
  66. package/dist/connectors/src/managed-gitconfig.d.ts +80 -0
  67. package/dist/connectors/src/managed-gitconfig.d.ts.map +1 -1
  68. package/dist/{connectors-DY7UBBV5.js → connectors-2OHRZDJZ.js} +3 -3
  69. package/dist/{connectors-DY7UBBV5.js.map → connectors-2OHRZDJZ.js.map} +1 -1
  70. package/dist/{ensure-sources-QRURV6JB.js → ensure-sources-42LTJ5QI.js} +4 -4
  71. package/dist/{ensure-sources-QRURV6JB.js.map → ensure-sources-42LTJ5QI.js.map} +1 -1
  72. package/dist/factory-assets/connectors/box.js +3 -3
  73. package/dist/factory-assets/connectors/deploy.js +3 -3
  74. package/dist/factory-assets/connectors/devserver.js +3 -3
  75. package/dist/factory-assets/connectors/flow/adapter.js +3 -3
  76. package/dist/factory-assets/connectors/flow/run-flow.js +4 -4
  77. package/dist/factory-assets/connectors/flow.js +3 -3
  78. package/dist/factory-assets/connectors/git/driver.d.ts +64 -1
  79. package/dist/factory-assets/connectors/git/driver.d.ts.map +1 -1
  80. package/dist/factory-assets/connectors/git.js +3 -3
  81. package/dist/factory-assets/connectors/gmail.js +3 -3
  82. package/dist/factory-assets/connectors/googledrive.js +3 -3
  83. package/dist/factory-assets/connectors/local.js +3 -3
  84. package/dist/factory-assets/connectors/mattermost.js +3 -3
  85. package/dist/factory-assets/connectors/memory.js +3 -3
  86. package/dist/factory-assets/connectors/minio.js +3 -3
  87. package/dist/factory-assets/connectors/postgres.js +3 -3
  88. package/dist/factory-assets/connectors/s3.js +3 -3
  89. package/dist/factory-assets/connectors/sharepoint.js +3 -3
  90. package/dist/factory-assets/connectors/sqlite.js +3 -3
  91. package/dist/factory-assets/connectors/static-server.js +3 -3
  92. package/dist/factory-assets/connectors/tunnel.js +3 -3
  93. package/dist/factory-assets/connectors/webdav.js +3 -3
  94. package/dist/factory-assets/connectors/xstate-store.js +3 -3
  95. package/dist/factory-assets/connectors/xstate.js +3 -3
  96. package/dist/{helpers-TLTVVZRR.js → helpers-BP7FX6CO.js} +4 -4
  97. package/dist/{helpers-TLTVVZRR.js.map → helpers-BP7FX6CO.js.map} +1 -1
  98. package/dist/{library-publish-6HXFLOXC.js → library-publish-X5Z32WJY.js} +6 -6
  99. package/dist/{library-publish-6HXFLOXC.js.map → library-publish-X5Z32WJY.js.map} +1 -1
  100. package/dist/{open-library-FMVJICL5.js → open-library-PHUMBYHP.js} +5 -5
  101. package/dist/{open-library-FMVJICL5.js.map → open-library-PHUMBYHP.js.map} +1 -1
  102. package/dist/{project-BG3N4I3V.js → project-HAMCXPNE.js} +5 -5
  103. package/dist/{project-BG3N4I3V.js.map → project-HAMCXPNE.js.map} +1 -1
  104. package/dist/runner/index.js +7 -7
  105. package/dist/runner/src/ai-provider-switch.d.ts +95 -0
  106. package/dist/runner/src/ai-provider-switch.d.ts.map +1 -0
  107. package/dist/runner/src/credential-rotated.d.ts +23 -4
  108. package/dist/runner/src/credential-rotated.d.ts.map +1 -1
  109. package/dist/runner/src/serve.d.ts.map +1 -1
  110. package/dist/runner/src/session-builder.d.ts +19 -0
  111. package/dist/runner/src/session-builder.d.ts.map +1 -1
  112. package/dist/sdk/asset-manager.js +1 -1
  113. package/dist/sdk/bridge.js +2 -2
  114. package/dist/sdk/index.js +7 -7
  115. package/dist/sdk/runner.js +7 -7
  116. package/dist/sdk/session.js +2 -2
  117. package/dist/sdk/types.js +1 -1
  118. package/dist/session/index.js +2 -2
  119. package/dist/{setup-O2N6PA7I.js → setup-IOOGR3EL.js} +4 -4
  120. package/dist/{setup-O2N6PA7I.js.map → setup-IOOGR3EL.js.map} +1 -1
  121. package/dist/{source-JDIBJZ3X.js → source-772GLNM6.js} +6 -6
  122. package/dist/{source-JDIBJZ3X.js.map → source-772GLNM6.js.map} +1 -1
  123. package/dist/{source-manifest-publish-567T25M6.js → source-manifest-publish-LAUGDQ5G.js} +7 -7
  124. package/dist/{source-manifest-publish-567T25M6.js.map → source-manifest-publish-LAUGDQ5G.js.map} +1 -1
  125. package/dist/tui/index.js +7 -7
  126. package/dist/types/index.js +1 -1
  127. package/dist/types/src/events.d.ts +34 -1
  128. package/dist/types/src/events.d.ts.map +1 -1
  129. package/dist/types/src/index.d.ts +1 -1
  130. package/dist/types/src/index.d.ts.map +1 -1
  131. package/dist/types/src/lifecycle.d.ts +13 -0
  132. package/dist/types/src/lifecycle.d.ts.map +1 -1
  133. package/dist/types/src/version.d.ts +22 -1
  134. package/dist/types/src/version.d.ts.map +1 -1
  135. package/dist/workspace-plugin/adapters/mcp.js +2 -2
  136. package/dist/workspace-plugin/adapters/mcp.js.map +1 -1
  137. package/dist/workspace-plugin/adapters/omp.js +8 -8
  138. package/dist/workspace-plugin/adapters/omp.js.map +1 -1
  139. package/dist/workspace-plugin/index.js +1 -1
  140. package/dist/workspace-plugin/src/adapters/mcp.d.ts.map +1 -1
  141. package/dist/workspace-plugin/src/adapters/omp.d.ts.map +1 -1
  142. package/dist/workspace-plugin/src/tools/connectors.d.ts +7 -3
  143. package/dist/workspace-plugin/src/tools/connectors.d.ts.map +1 -1
  144. package/package.json +1 -1
  145. package/dist/chunk-26L2B6P6.js.map +0 -1
  146. package/dist/chunk-32TJ55W5.js.map +0 -1
  147. package/dist/chunk-F7S6NDIW.js.map +0 -1
  148. package/dist/chunk-JTBF4OB2.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { resolveAuthRef } from './chunk-3UU3D2Q3.js';
2
- import { EventNormalizer } from './chunk-VXITGB7G.js';
2
+ import { EventNormalizer } from './chunk-KNPBMR2P.js';
3
3
  import { resolveModelTier } from './chunk-V34ZEXSE.js';
4
- import { createDriver } from './chunk-32TJ55W5.js';
4
+ import { createDriver } from './chunk-BGAAZMBK.js';
5
5
  import { pluginRegistry } from './chunk-6E6PKKAD.js';
6
6
  import { computeStimulus } from './chunk-IFGTRL2I.js';
7
7
  import { resolveNodeBindings, getConsumedOutputSchema, validateConsumedNodeOutput, appendAttempt, resolveTerminalFailureStatus, remainingActiveExecutionMs, interpolateNodeInstruction, deriveCheckEvidence, computeFlowStateFromSnapshots, availableRouterFrontier, evaluateRouterSelection, routerSelectionOutput, planRouterSkips, unsettledRouterBranchIds, countFailedAttempts, hasAutomaticRetryBudget, persistedRouterSelection } from './chunk-OYZIURPH.js';
@@ -17,7 +17,7 @@ import { mkdir, rename, writeFile, readFile, stat, cp, readdir, chmod, realpath
17
17
  import * as z4 from 'zod';
18
18
  import { execFile } from 'child_process';
19
19
  import { promisify } from 'util';
20
- import { createReadStream, readFileSync, existsSync, mkdirSync, openSync, writeSync, closeSync, chmodSync, renameSync } from 'fs';
20
+ import { createReadStream, existsSync, mkdirSync, openSync, closeSync, chmodSync, utimesSync, readFileSync, writeSync, renameSync } from 'fs';
21
21
  import { createServer } from 'http';
22
22
  import mime from 'mime';
23
23
  import { createRequire } from 'module';
@@ -1323,6 +1323,18 @@ function resolveGitConfig(args) {
1323
1323
  }
1324
1324
  var modeLog = createLogger({ kind: "connector", subkind: "git" });
1325
1325
  var execFileAsync2 = promisify(execFile);
1326
+ function redactUrlAuth(text) {
1327
+ return text.replace(/\/\/[^/@\s]*:[^/@\s]*@/g, "//***@");
1328
+ }
1329
+ function redactError(err) {
1330
+ const original = err instanceof Error ? err : new Error(String(err));
1331
+ const copy = new Error(redactUrlAuth(original.message));
1332
+ copy.name = original.name;
1333
+ copy.stack = original.stack === void 0 ? void 0 : redactUrlAuth(original.stack);
1334
+ return copy;
1335
+ }
1336
+ var BOOTSTRAP_COMMIT_NAME = "Skaile";
1337
+ var BOOTSTRAP_COMMIT_EMAIL = "skaile@local";
1326
1338
  function gitPrefix(cwd) {
1327
1339
  return ["-c", `safe.directory=${cwd}`];
1328
1340
  }
@@ -1482,53 +1494,59 @@ var GitConnector = class extends AbstractConnector {
1482
1494
  if (!isValidGitRefOrSha(branch)) {
1483
1495
  throw new Error(`Git connector: invalid branch name "${branch}"`);
1484
1496
  }
1485
- this.log.info("mount start", { url, branch, targetDir });
1486
- const initial = await this.acquireInitialToken(declaration, secrets, tokenMediator);
1487
- const { session, sync, lifecycle } = resolveGitConfig({
1488
- mode: declaration.mode,
1489
- options: opts
1490
- });
1491
- const state = {
1492
- url,
1493
- branch,
1494
- activeBranch: branch,
1495
- token: initial.token,
1496
- session,
1497
- sync,
1498
- lifecycle,
1499
- targetDir,
1500
- tokenMediator: initial.authRef?.kind === "backend" ? tokenMediator : void 0
1501
- };
1502
- const authUrl = this.authUrl(state);
1503
- await this.prepareCheckout(targetDir, url, branch, authUrl, started);
1504
- if (session.enabled && !session.isMain && session.slug) {
1505
- await this.checkoutSessionBranch(state, session, targetDir);
1506
- }
1507
- if (sync.fetchIntervalSec > 0) {
1508
- this.startPeriodicFetch(state, targetDir);
1509
- }
1510
- if (declaration.mount?.exposeAccessToken === true) {
1511
- try {
1512
- this.exposeManagedCredential(declaration, state, url, initial);
1513
- } catch (err) {
1514
- this.log.error("managed-gitconfig write failed", err, { connectorId: declaration.id });
1515
- throw err;
1497
+ try {
1498
+ this.log.info("mount start", { url, branch, targetDir });
1499
+ this.clearUnavailableCredentialHelper(declaration.id, url);
1500
+ const initial = await this.acquireInitialToken(declaration, secrets, tokenMediator);
1501
+ const { session, sync, lifecycle } = resolveGitConfig({
1502
+ mode: declaration.mode,
1503
+ options: opts
1504
+ });
1505
+ const state = {
1506
+ url,
1507
+ branch,
1508
+ activeBranch: branch,
1509
+ token: initial.token,
1510
+ session,
1511
+ sync,
1512
+ lifecycle,
1513
+ targetDir,
1514
+ tokenMediator: initial.authRef?.kind === "backend" ? tokenMediator : void 0
1515
+ };
1516
+ const authUrl = this.authUrl(state);
1517
+ await this.prepareCheckout(targetDir, url, branch, authUrl, declaration.access, started);
1518
+ if (session.enabled && !session.isMain && session.slug) {
1519
+ await this.checkoutSessionBranch(state, session, targetDir);
1516
1520
  }
1517
- }
1518
- if (state.managedGitconfig) {
1519
- await this.stripRemoteInlineToken(targetDir);
1520
- } else {
1521
- await this.rebakeRemoteAuth(targetDir, state);
1522
- if (initial.authRef?.kind === "backend" && initial.expiresAt && state.tokenMediator) {
1523
- this.scheduleRefresh(declaration.id, state, initial.expiresAt);
1521
+ if (sync.fetchIntervalSec > 0) {
1522
+ this.startPeriodicFetch(state, targetDir);
1523
+ }
1524
+ if (declaration.mount?.exposeAccessToken === true) {
1525
+ try {
1526
+ this.exposeManagedCredential(declaration, state, url, initial);
1527
+ } catch (err) {
1528
+ this.log.error("managed-gitconfig write failed", err, { connectorId: declaration.id });
1529
+ throw err;
1530
+ }
1531
+ }
1532
+ if (state.managedGitconfig) {
1533
+ await this.stripRemoteInlineToken(targetDir);
1534
+ } else {
1535
+ await this.rebakeRemoteAuth(targetDir, state);
1536
+ if (initial.authRef?.kind === "backend" && initial.expiresAt && state.tokenMediator) {
1537
+ this.scheduleRefresh(declaration.id, state, initial.expiresAt);
1538
+ }
1524
1539
  }
1540
+ this.log.info("mount ok", {
1541
+ targetDir,
1542
+ activeBranch: state.activeBranch,
1543
+ durationMs: Date.now() - started
1544
+ });
1545
+ return this.makeHandle(declaration, state, targetDir);
1546
+ } catch (err) {
1547
+ this.registerUnavailableCredentialHelper(declaration, url, err);
1548
+ throw err;
1525
1549
  }
1526
- this.log.info("mount ok", {
1527
- targetDir,
1528
- activeBranch: state.activeBranch,
1529
- durationMs: Date.now() - started
1530
- });
1531
- return this.makeHandle(declaration, state, targetDir);
1532
1550
  }
1533
1551
  /**
1534
1552
  * Bring `targetDir` into a usable checkout state for `connect()`.
@@ -1541,40 +1559,58 @@ var GitConnector = class extends AbstractConnector {
1541
1559
  *
1542
1560
  * Three cases:
1543
1561
  * - `.git` exists → use as-is. No pull, no reset.
1544
- * - dir empty → fresh clone (only network fetch on connect).
1562
+ * - dir empty → fresh clone (only network fetch on connect), or
1563
+ * `bootstrapEmptyRemote` when the remote has no refs at all.
1545
1564
  * - dir populated, no `.git` → `git init` + `remote add origin` so
1546
1565
  * lifecycle hooks (commit/push) can do their job. No fetch, no
1547
1566
  * reset, no clean. Existing files become uncommitted-on-init; the
1548
1567
  * agent or `pushAfterCommit` reconciles with remote later.
1549
1568
  */
1550
- async prepareCheckout(targetDir, url, branch, authUrl, started) {
1569
+ async prepareCheckout(targetDir, url, branch, authUrl, access, started) {
1551
1570
  try {
1552
1571
  if (await pathExists(join(targetDir, ".git"))) {
1553
1572
  if (await this.isInitialisedCheckout(targetDir)) {
1554
1573
  this.log.info("found existing checkout \u2014 using as-is", { branch, targetDir });
1555
1574
  } else {
1556
- this.log.info("found empty .git \u2014 bootstrapping from origin", { branch });
1557
- await this.bootstrapFromOrigin(targetDir, branch, authUrl);
1575
+ if (await this.countRemoteHeads(targetDir, authUrl) === 0) {
1576
+ this.log.info("empty .git against a ref-less remote \u2014 bootstrapping branch", {
1577
+ branch,
1578
+ access
1579
+ });
1580
+ await this.bootstrapEmptyRemote(targetDir, branch, authUrl, access);
1581
+ } else {
1582
+ this.log.info("found empty .git \u2014 bootstrapping from origin", { branch });
1583
+ await this.bootstrapFromOrigin(targetDir, branch, authUrl);
1584
+ }
1558
1585
  this.log.info("bootstrap ok", { branch, durationMs: Date.now() - started });
1559
1586
  }
1560
1587
  } else {
1561
1588
  await withFsDeadline(`mkdir ${targetDir}`, () => mkdir(targetDir, { recursive: true }));
1562
1589
  if (await isDirEmpty(targetDir)) {
1563
- this.log.info("clone start", { url, branch });
1564
- await runGit(
1565
- [
1566
- ...gitPrefix(targetDir),
1567
- "clone",
1568
- "--branch",
1590
+ if (await this.countRemoteHeads(targetDir, authUrl) === 0) {
1591
+ this.log.info("remote has no refs \u2014 bootstrapping branch", { url, branch, access });
1592
+ await this.bootstrapEmptyRemote(targetDir, branch, authUrl, access);
1593
+ this.log.info("empty-remote bootstrap ok", {
1569
1594
  branch,
1570
- "--single-branch",
1571
- "--recurse-submodules",
1572
- authUrl,
1573
- targetDir
1574
- ],
1575
- { timeout: 12e4 }
1576
- );
1577
- this.log.info("clone ok", { branch, durationMs: Date.now() - started });
1595
+ durationMs: Date.now() - started
1596
+ });
1597
+ } else {
1598
+ this.log.info("clone start", { url, branch });
1599
+ await runGit(
1600
+ [
1601
+ ...gitPrefix(targetDir),
1602
+ "clone",
1603
+ "--branch",
1604
+ branch,
1605
+ "--single-branch",
1606
+ "--recurse-submodules",
1607
+ authUrl,
1608
+ targetDir
1609
+ ],
1610
+ { timeout: 12e4 }
1611
+ );
1612
+ this.log.info("clone ok", { branch, durationMs: Date.now() - started });
1613
+ }
1578
1614
  } else {
1579
1615
  this.log.warn("target non-empty without .git \u2014 initialising in place", { targetDir });
1580
1616
  await this.initInPlace(targetDir, authUrl);
@@ -1582,7 +1618,7 @@ var GitConnector = class extends AbstractConnector {
1582
1618
  }
1583
1619
  }
1584
1620
  } catch (err) {
1585
- this.log.error("connect preparation failed", err, { url, branch });
1621
+ this.log.error("connect preparation failed", redactError(err), { url, branch });
1586
1622
  throw err;
1587
1623
  }
1588
1624
  }
@@ -1964,6 +2000,104 @@ var GitConnector = class extends AbstractConnector {
1964
2000
  }
1965
2001
  return { authRef, token, expiresAt: null };
1966
2002
  }
2003
+ /**
2004
+ * Register a credential helper that *reports* why there is no credential,
2005
+ * after this connector failed to connect.
2006
+ *
2007
+ * Without it git finds no helper for the host and asks the terminal for a
2008
+ * username, so the agent sees `fatal: could not read Username for
2009
+ * 'https://github.com'` — which names neither the mount nor the cause, and
2010
+ * which only ever appears on a write, since reads against a public remote
2011
+ * keep succeeding. The helper written here makes git stop with the mediator's
2012
+ * actual refusal text instead.
2013
+ *
2014
+ * Entirely best-effort: it runs on the failure path and must never mask the
2015
+ * connect error that is already on its way up.
2016
+ */
2017
+ registerUnavailableCredentialHelper(declaration, url, err) {
2018
+ try {
2019
+ if (!url.startsWith("https://") && !url.startsWith("http://")) return;
2020
+ const gitconfigPath = process.env.GIT_CONFIG_GLOBAL;
2021
+ if (!gitconfigPath) return;
2022
+ const skaileDir = dirname(gitconfigPath);
2023
+ const credentialsPath = join(skaileDir, "git-credentials");
2024
+ const urlPrefix = this.deriveUrlPrefix(url);
2025
+ const helperScriptPath = join(skaileDir, `credential-helper-${declaration.id}.sh`);
2026
+ const refreshAttemptPath = join(skaileDir, `refresh-attempt-${declaration.id}`);
2027
+ const reason = redactUrlAuth(err instanceof Error ? err.message : String(err));
2028
+ this.ensureAttemptStamp(declaration.id, refreshAttemptPath);
2029
+ writeHelperScript(
2030
+ helperScriptPath,
2031
+ renderCredentialHelperScript({
2032
+ credentialsPath,
2033
+ refreshFlagPath: join(skaileDir, "refresh-request"),
2034
+ credentialTag: `# skaile-mount: ${declaration.id}`,
2035
+ refreshAttemptPath,
2036
+ unavailableReason: reason
2037
+ })
2038
+ );
2039
+ writeUnavailableMountBlock({
2040
+ mountId: declaration.id,
2041
+ urlPrefix,
2042
+ gitconfigPath,
2043
+ credentialsPath,
2044
+ helperScriptPath
2045
+ });
2046
+ this.log.warn("registered unavailable credential helper for failed mount", {
2047
+ connectorId: declaration.id,
2048
+ urlPrefix
2049
+ });
2050
+ } catch (writeErr) {
2051
+ this.log.warn("failed to register unavailable credential helper", {
2052
+ connectorId: declaration.id,
2053
+ error: writeErr instanceof Error ? writeErr.message : String(writeErr)
2054
+ });
2055
+ }
2056
+ }
2057
+ /**
2058
+ * Best-effort pre-creation of the helper's refresh-attempt stamp. A missing
2059
+ * stamp only costs the cooldown (the helper dances on every git op instead of
2060
+ * once per window), so a failure here is never worth surfacing.
2061
+ */
2062
+ ensureAttemptStamp(connectorId, stampPath) {
2063
+ try {
2064
+ ensureRefreshAttemptStamp(stampPath);
2065
+ } catch (err) {
2066
+ this.log.warn("managed-gitconfig: could not pre-create refresh-attempt stamp", {
2067
+ connectorId,
2068
+ error: err instanceof Error ? err.message : String(err)
2069
+ });
2070
+ }
2071
+ }
2072
+ /**
2073
+ * Best-effort removal of the unavailable-mount blocks that would answer this
2074
+ * connector's git operations: its own from a previous failed session, and any
2075
+ * left under the same URL prefix by a sibling mount that failed earlier in
2076
+ * this `connectAll` (git matches helpers by URL, not by mount id).
2077
+ */
2078
+ clearUnavailableCredentialHelper(connectorId, url) {
2079
+ const gitconfigPath = process.env.GIT_CONFIG_GLOBAL;
2080
+ if (!gitconfigPath) return;
2081
+ let urlPrefix;
2082
+ if (url.startsWith("https://") || url.startsWith("http://")) {
2083
+ try {
2084
+ urlPrefix = this.deriveUrlPrefix(url);
2085
+ } catch {
2086
+ }
2087
+ }
2088
+ try {
2089
+ removeUnavailableMountBlock({
2090
+ mountId: connectorId,
2091
+ gitconfigPath,
2092
+ ...urlPrefix !== void 0 ? { urlPrefix } : {}
2093
+ });
2094
+ } catch (err) {
2095
+ this.log.warn("failed to clear unavailable credential helper", {
2096
+ connectorId,
2097
+ error: err instanceof Error ? err.message : String(err)
2098
+ });
2099
+ }
2100
+ }
1967
2101
  exposeManagedCredential(declaration, state, url, initial) {
1968
2102
  const token = initial.token;
1969
2103
  if (!token) {
@@ -1991,15 +2125,22 @@ var GitConnector = class extends AbstractConnector {
1991
2125
  const urlPrefix = this.deriveUrlPrefix(url);
1992
2126
  const refreshFlagPath = join(skaileDir, "refresh-request");
1993
2127
  const helperScriptPath = join(skaileDir, `credential-helper-${declaration.id}.sh`);
2128
+ const refreshAttemptPath = join(skaileDir, `refresh-attempt-${declaration.id}`);
1994
2129
  const credentialTag2 = `# skaile-mount: ${declaration.id}`;
1995
2130
  const scriptBody = renderCredentialHelperScript({
1996
2131
  credentialsPath,
1997
2132
  refreshFlagPath,
1998
- credentialTag: credentialTag2
2133
+ credentialTag: credentialTag2,
2134
+ // Passed explicitly on both call sites so the failure path and this one
2135
+ // never disagree about which stamp file bounds this mount's dance.
2136
+ refreshAttemptPath
1999
2137
  });
2138
+ this.ensureAttemptStamp(declaration.id, refreshAttemptPath);
2139
+ let helperWritten = true;
2000
2140
  try {
2001
2141
  writeHelperScript(helperScriptPath, scriptBody);
2002
2142
  } catch (err) {
2143
+ helperWritten = false;
2003
2144
  this.log.warn(
2004
2145
  "managed-gitconfig: failed to write credential-helper script -- falling back to static helper",
2005
2146
  {
@@ -2014,7 +2155,12 @@ var GitConnector = class extends AbstractConnector {
2014
2155
  token,
2015
2156
  gitconfigPath,
2016
2157
  credentialsPath,
2017
- helperScriptPath
2158
+ // Only point at the script when it was actually written. Pointing at an
2159
+ // unwritten path is not a fallback: the file may still hold a previous
2160
+ // session's *unavailable* body, which would refuse this live credential
2161
+ // with a stale reason. Omitting it selects the static `store --file=`
2162
+ // form, which reads the credential line this call is about to write.
2163
+ ...helperWritten ? { helperScriptPath } : {}
2018
2164
  });
2019
2165
  state.managedGitconfig = { gitconfigPath, credentialsPath, urlPrefix };
2020
2166
  const providerKind = initial.authRef?.kind ?? "pat";
@@ -2163,6 +2309,97 @@ var GitConnector = class extends AbstractConnector {
2163
2309
  }
2164
2310
  await git(["remote", "add", "origin", authUrl], targetDir, 5e3);
2165
2311
  }
2312
+ /**
2313
+ * Count `refs/heads/*` on `authUrl`, optionally narrowed to one `branch`.
2314
+ *
2315
+ * Returns `null` when the probe itself fails (network, auth, bad URL).
2316
+ * "Cannot tell" must never collapse into "no refs": the empty-remote
2317
+ * fallback WRITES to origin, and every caller therefore treats `null` as
2318
+ * "assume populated" and falls through to the path that fails loudly.
2319
+ */
2320
+ async countRemoteHeads(targetDir, authUrl, branch) {
2321
+ try {
2322
+ const out = await runGit(
2323
+ branch === void 0 ? [...gitPrefix(targetDir), "ls-remote", "--heads", authUrl] : [...gitPrefix(targetDir), "ls-remote", "--heads", authUrl, `refs/heads/${branch}`],
2324
+ { timeout: 3e4 }
2325
+ );
2326
+ return out.length === 0 ? 0 : out.split("\n").length;
2327
+ } catch (err) {
2328
+ this.log.warn("ls-remote probe failed \u2014 assuming a populated remote", {
2329
+ error: redactUrlAuth(err instanceof Error ? err.message : String(err))
2330
+ });
2331
+ return null;
2332
+ }
2333
+ }
2334
+ /**
2335
+ * Bootstrap a checkout against a remote that has no refs at all — the state
2336
+ * of a GitHub repo created without "Add a README".
2337
+ *
2338
+ * A plain `git clone` is NOT a substitute: cloning a ref-less remote leaves
2339
+ * local HEAD on git's `init.defaultBranch` (`master`), not the declared
2340
+ * branch, so the branch is created explicitly here.
2341
+ *
2342
+ * The root commit is created REGARDLESS of `access`, and only the push to
2343
+ * origin is gated on it. An unborn HEAD is not a stable resting state: the
2344
+ * next connect sees `.git` with no resolvable HEAD, routes to
2345
+ * `bootstrapFromOrigin`, and its `fetch origin <branch>` dies on the still
2346
+ * ref-less remote — turning a fixed first connect into a broken second one.
2347
+ * A local commit costs a read-only connector nothing (it is never pushed)
2348
+ * and makes reconnect take the cheap "use as-is" path.
2349
+ *
2350
+ * With the push done, `origin/<branch>` exists for everything downstream —
2351
+ * notably `checkoutSessionBranch`, whose publish-on-create push needs a
2352
+ * commit to point at.
2353
+ *
2354
+ * Invariant: this method never throws on a failed push. The session must
2355
+ * come up with a usable checkout even when origin is unreachable — the
2356
+ * lifecycle hooks (`pushAfterCommit`, hibernate/close push) publish later.
2357
+ */
2358
+ async bootstrapEmptyRemote(targetDir, branch, authUrl, access) {
2359
+ await this.initInPlace(targetDir, authUrl);
2360
+ await git(["symbolic-ref", "HEAD", `refs/heads/${branch}`], targetDir, 5e3);
2361
+ await git(
2362
+ [
2363
+ "-c",
2364
+ `user.name=${BOOTSTRAP_COMMIT_NAME}`,
2365
+ "-c",
2366
+ `user.email=${BOOTSTRAP_COMMIT_EMAIL}`,
2367
+ "commit",
2368
+ "--allow-empty",
2369
+ "-m",
2370
+ "chore: initialize repository"
2371
+ ],
2372
+ targetDir,
2373
+ 3e4
2374
+ );
2375
+ if (access === "read-only") {
2376
+ this.log.info("empty remote is read-only \u2014 branch created locally, nothing pushed", {
2377
+ branch
2378
+ });
2379
+ return;
2380
+ }
2381
+ try {
2382
+ await git(["push", "-u", "origin", branch], targetDir, 6e4);
2383
+ this.log.info("empty remote initialised on origin", { branch });
2384
+ return;
2385
+ } catch (err) {
2386
+ this.log.warn("empty-remote publish push failed; deferring to lifecycle push", {
2387
+ branch,
2388
+ error: redactUrlAuth(err instanceof Error ? err.message : String(err))
2389
+ });
2390
+ }
2391
+ const publishedElsewhere = await this.countRemoteHeads(targetDir, authUrl, branch);
2392
+ if (publishedElsewhere === null || publishedElsewhere === 0) return;
2393
+ try {
2394
+ this.log.info("origin gained the branch concurrently \u2014 adopting it", { branch });
2395
+ await this.bootstrapFromOrigin(targetDir, branch, authUrl);
2396
+ } catch (err) {
2397
+ this.log.warn("adopting the concurrently-created branch failed; keeping local branch", {
2398
+ branch,
2399
+ error: redactUrlAuth(err instanceof Error ? err.message : String(err))
2400
+ });
2401
+ }
2402
+ }
2166
2403
  /**
2167
2404
  * True when `<targetDir>/.git` contains at least one resolvable commit
2168
2405
  * (i.e. `HEAD` points at a real object). False when `.git` is an empty
@@ -5804,6 +6041,13 @@ var ConnectorManager = class {
5804
6041
  }
5805
6042
  opts;
5806
6043
  active = /* @__PURE__ */ new Map();
6044
+ /**
6045
+ * Connectors whose `connect()` threw, keyed by id. A failed connector never
6046
+ * enters `active`, so without this map its failure would exist only in the
6047
+ * log store — invisible to the agent that is about to write into a mount
6048
+ * that will never sync. See {@link listFailedConnectors}.
6049
+ */
6050
+ failedConnectors = /* @__PURE__ */ new Map();
5807
6051
  // Allocated absolute mount target → owning connector id; collisions rejected at setup, released on disconnect.
5808
6052
  mountTargets = /* @__PURE__ */ new Map();
5809
6053
  workspaceDir;
@@ -5942,6 +6186,30 @@ var ConnectorManager = class {
5942
6186
  }
5943
6187
  this.mountTargets.set(absPath, id);
5944
6188
  }
6189
+ /**
6190
+ * Remember a connect failure so the agent can be told about it. Called from
6191
+ * both the bulk (`attemptConnect`) and hot-plug (`connect`) failure paths.
6192
+ */
6193
+ recordFailure(decl, error) {
6194
+ if (this.active.has(decl.id)) return;
6195
+ let mountPath;
6196
+ if (decl.mount != null) {
6197
+ try {
6198
+ mountPath = this.resolveMountTarget(decl);
6199
+ } catch {
6200
+ }
6201
+ }
6202
+ this.failedConnectors.set(decl.id, {
6203
+ id: decl.id,
6204
+ driver: decl.driver,
6205
+ access: decl.access,
6206
+ ...decl.label !== void 0 ? { label: decl.label } : {},
6207
+ ...decl.mount?.source !== void 0 ? { source: decl.mount.source } : {},
6208
+ ...mountPath !== void 0 ? { mountPath } : {},
6209
+ required: decl.access === "read-write" && decl.mount != null,
6210
+ error
6211
+ });
6212
+ }
5945
6213
  /** Release every mount target owned by `id` (on disconnect). */
5946
6214
  releaseMountTarget(id) {
5947
6215
  for (const [path, owner] of [...this.mountTargets]) {
@@ -6094,6 +6362,7 @@ var ConnectorManager = class {
6094
6362
  handle,
6095
6363
  declaration: decl
6096
6364
  });
6365
+ this.failedConnectors.delete(decl.id);
6097
6366
  this.opts.onStatusChange?.({
6098
6367
  type: "connector_status",
6099
6368
  connectorId: decl.id,
@@ -6108,6 +6377,7 @@ var ConnectorManager = class {
6108
6377
  } catch (err) {
6109
6378
  if (!this.active.has(decl.id)) this.releaseMountTarget(decl.id);
6110
6379
  const msg = err instanceof Error ? err.message : String(err);
6380
+ this.recordFailure(decl, msg);
6111
6381
  this.mgrLog.error("connect failed", err, { id: decl.id, driver: decl.driver });
6112
6382
  this.opts.onStatusChange?.({
6113
6383
  type: "connector_status",
@@ -6152,6 +6422,7 @@ var ConnectorManager = class {
6152
6422
  }
6153
6423
  this.active.clear();
6154
6424
  this.mountTargets.clear();
6425
+ this.failedConnectors.clear();
6155
6426
  this.mgrLog.info("disconnectAll complete");
6156
6427
  }
6157
6428
  // ── Hot-plug ──────────────────────────────────────────────────────────────────
@@ -6186,6 +6457,7 @@ var ConnectorManager = class {
6186
6457
  () => connector.connect(declaration, ctx)
6187
6458
  );
6188
6459
  this.active.set(declaration.id, { connector, handle, declaration });
6460
+ this.failedConnectors.delete(declaration.id);
6189
6461
  this.opts.onStatusChange?.({
6190
6462
  type: "connector_status",
6191
6463
  connectorId: declaration.id,
@@ -6195,6 +6467,7 @@ var ConnectorManager = class {
6195
6467
  } catch (err) {
6196
6468
  this.releaseMountTarget(declaration.id);
6197
6469
  const msg = err instanceof Error ? err.message : String(err);
6470
+ this.recordFailure(declaration, msg);
6198
6471
  this.opts.onStatusChange?.({
6199
6472
  type: "connector_status",
6200
6473
  connectorId: declaration.id,
@@ -6207,9 +6480,25 @@ var ConnectorManager = class {
6207
6480
  /**
6208
6481
  * Disconnect a single connector by ID and stop its watcher.
6209
6482
  *
6210
- * @throws {Error} if the connector is not found
6483
+ * A connector that only ever *failed* to connect is not in `active`, so
6484
+ * `get(id)` would throw for it. Removing such a declaration is still a
6485
+ * legitimate host action — and it is the only way to retract its
6486
+ * `## Mount failures` row — so that case is handled as a plain forget rather
6487
+ * than an error.
6488
+ *
6489
+ * @throws {Error} if the connector is neither connected nor recorded as failed
6211
6490
  */
6212
6491
  async disconnect(id) {
6492
+ if (!this.active.has(id) && this.failedConnectors.has(id)) {
6493
+ this.failedConnectors.delete(id);
6494
+ this.releaseMountTarget(id);
6495
+ this.opts.onStatusChange?.({
6496
+ type: "connector_status",
6497
+ connectorId: id,
6498
+ status: "disconnected"
6499
+ });
6500
+ return;
6501
+ }
6213
6502
  const entry = this.get(id);
6214
6503
  if (entry.watchHandle) {
6215
6504
  try {
@@ -6229,6 +6518,7 @@ var ConnectorManager = class {
6229
6518
  await entry.connector.disconnect(entry.handle);
6230
6519
  } finally {
6231
6520
  this.active.delete(id);
6521
+ this.failedConnectors.delete(id);
6232
6522
  this.releaseMountTarget(id);
6233
6523
  this.opts.onStatusChange?.({
6234
6524
  type: "connector_status",
@@ -6344,6 +6634,22 @@ var ConnectorManager = class {
6344
6634
  status: entry.handle.status
6345
6635
  }));
6346
6636
  }
6637
+ /**
6638
+ * Return every connector whose `connect()` threw and that has not since
6639
+ * connected.
6640
+ *
6641
+ * A failed connector is deliberately **absent** from `listConnectors()` and
6642
+ * `get()` — it never entered the active map, and those surfaces describe what
6643
+ * the agent can actually use. That is exactly why this second list exists:
6644
+ * without it a failed mount is invisible to the prompt builder, and the agent
6645
+ * keeps treating a dead directory as a live, synced workspace.
6646
+ *
6647
+ * Entries are cleared by a later successful connect of the same id, by
6648
+ * `disconnect(id)`, and wholesale by `disconnectAll()`.
6649
+ */
6650
+ listFailedConnectors() {
6651
+ return [...this.failedConnectors.values()].map((f) => ({ ...f }));
6652
+ }
6347
6653
  /**
6348
6654
  * Lists git-backed connectors, surfacing the fields prompt-builders and the
6349
6655
  * runner's wake-mid-401 refresh handler need.
@@ -6558,6 +6864,14 @@ var ConnectorManager = class {
6558
6864
  }
6559
6865
  };
6560
6866
  var zStatic = z4.z ?? z4.default ?? z4;
6867
+ function readFailedConnectors(manager) {
6868
+ if (typeof manager.listFailedConnectors !== "function") return [];
6869
+ try {
6870
+ return manager.listFailedConnectors();
6871
+ } catch {
6872
+ return [];
6873
+ }
6874
+ }
6561
6875
  async function buildSdkConnectorTools(manager, options) {
6562
6876
  const shellAccess = options?.shellAccess ?? true;
6563
6877
  let sdk = options?.sdk;
@@ -6572,11 +6886,14 @@ async function buildSdkConnectorTools(manager, options) {
6572
6886
  const tools = [];
6573
6887
  tools.push({
6574
6888
  name: "connector_list",
6575
- description: "List all currently registered connectors with their id, driver, access mode, and available operations. Use this to discover connectors that may have been added after session start (e.g. shared-state stores like 'presence' or 'session' pushed by the host).",
6889
+ description: "List connectors as {connected, failed}. `connected` holds the usable connectors with their id, driver, access mode, and available operations; `failed` holds connectors that did not connect this session, with the failure reason \u2014 their mount paths are dead directories, not synced workspaces. Use this to discover connectors that may have been added after session start (e.g. shared-state stores like 'presence' or 'session' pushed by the host).",
6576
6890
  inputSchema: {},
6577
6891
  handler: async () => {
6578
6892
  try {
6579
- const entries = manager.listConnectors();
6893
+ const entries = {
6894
+ connected: manager.listConnectors(),
6895
+ failed: readFailedConnectors(manager)
6896
+ };
6580
6897
  return { content: [{ type: "text", text: JSON.stringify(entries, null, 2) }] };
6581
6898
  } catch (err) {
6582
6899
  return {
@@ -6814,7 +7131,8 @@ function buildOperationInputSchema(args, z5) {
6814
7131
  function buildConnectorPromptSection(manager, options) {
6815
7132
  const shellAccess = options?.shellAccess ?? true;
6816
7133
  const allConnectors = manager.listConnectors();
6817
- if (allConnectors.length === 0) return "";
7134
+ const failed = readFailedConnectors(manager);
7135
+ if (allConnectors.length === 0 && failed.length === 0) return "";
6818
7136
  const sections = [];
6819
7137
  const faces = allConnectors.map((c) => {
6820
7138
  try {
@@ -6832,8 +7150,46 @@ function buildConnectorPromptSection(manager, options) {
6832
7150
  if (toolConnectors.length > 0) {
6833
7151
  sections.push(buildToolSection(manager, toolConnectors, shellAccess));
6834
7152
  }
7153
+ if (failed.length > 0) {
7154
+ sections.unshift(buildFailedConnectorSection(failed));
7155
+ }
6835
7156
  return sections.join("\n\n");
6836
7157
  }
7158
+ function buildFailedConnectorSection(failed) {
7159
+ const lines = [
7160
+ "## Mount failures \u2014 read this first",
7161
+ "",
7162
+ "The following connectors **failed to connect** in this session. Their directories are ordinary local folders: **not** connected, **not** synced, and **not** backend-managed git worktrees. Any other statement in this prompt \u2014 including the host platform's own description of the workspace \u2014 that describes these paths as a managed, synced, or version-controlled workspace **is superseded by this section**.",
7163
+ "",
7164
+ "| Mount | Driver | Path | Access | Required | Failure |",
7165
+ "|---|---|---|---|---|---|"
7166
+ ];
7167
+ for (const f of failed) {
7168
+ const path = f.mountPath ?? f.source ?? "(no mount)";
7169
+ const required = f.required ? "yes" : "no";
7170
+ lines.push(
7171
+ `| ${f.id} | ${f.driver} | \`${path}\` | ${f.access} | ${required} | ${oneLine(f.error)} |`
7172
+ );
7173
+ }
7174
+ lines.push("");
7175
+ lines.push(
7176
+ "**Work written under these paths is not saved.** The mount's lifecycle automation \u2014 auto-commit, push-after-commit, push-on-hibernate, commit-on-hibernate, push-on-close \u2014 is dead for a failed connector: nothing is committed, nothing is pushed, and the contents do not survive the session. Do not tell the user their work is saved, synced, or pushed. If they ask you to write substantial work into one of these paths, say first that it will be lost. `Required: yes` marks a read-write mount the session was supposed to be built on \u2014 that session did not start degraded, it started with nowhere to save anything."
7177
+ );
7178
+ lines.push("");
7179
+ lines.push(
7180
+ "**There is nothing to fix inside this workspace.** The credential for a mount is minted by the host platform, not by anything in here and not by any project setting \u2014 there is no credential field to fill in. Report the failure text above verbatim to the user and ask them to reconnect the connector's provider link on the platform, then restart the session. Do not retry the connection yourself; a connector that failed at startup stays failed until the session restarts."
7181
+ );
7182
+ if (failed.some((f) => f.driver === "git")) {
7183
+ lines.push("");
7184
+ lines.push(
7185
+ "**Git specifically:** `git fetch` and `git ls-remote` can still succeed against a public remote with no credential at all, so a working read is **not** evidence that auth works. Only writes need the credential. `git push` will fail with `skaile: mount not connected: \u2026`. Do not retry it in a loop and do not attempt to configure git credentials by hand."
7186
+ );
7187
+ }
7188
+ return lines.join("\n");
7189
+ }
7190
+ function oneLine(text) {
7191
+ return text.replace(/\s*[\r\n]+\s*/g, " ").replace(/\|/g, "\\|").trim();
7192
+ }
6837
7193
  function buildFilesystemSection(manager, fsConnectors) {
6838
7194
  const lines = [
6839
7195
  "## Mounts",
@@ -6937,15 +7293,29 @@ function safeHost(source) {
6937
7293
  return source;
6938
7294
  }
6939
7295
  }
6940
-
6941
- // connectors/src/credential-helper-script.ts
7296
+ function shQuote(value) {
7297
+ return `'${value.replaceAll("'", "'\\''")}'`;
7298
+ }
7299
+ function singleLine(value) {
7300
+ return value.replace(/[\r\n]+/g, " ").trim();
7301
+ }
7302
+ function mountIdFromTag(tag) {
7303
+ const match = tag.match(/^#\s*skaile-mount:\s*(.+)$/);
7304
+ return (match?.[1] ?? tag).trim();
7305
+ }
7306
+ function stampSlug(tag) {
7307
+ return mountIdFromTag(tag).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "mount";
7308
+ }
6942
7309
  function renderCredentialHelperScript(opts) {
6943
7310
  const triggerAgeSec = opts.triggerAgeSec ?? 300;
6944
7311
  const waitTimeoutMs = opts.waitTimeoutMs ?? 5e3;
6945
7312
  const waitTimeoutSec = Math.max(1, Math.ceil(waitTimeoutMs / 1e3));
6946
- const credPath = opts.credentialsPath;
6947
- const flagPath = opts.refreshFlagPath;
7313
+ const cooldownSec = Math.max(0, Math.trunc(opts.refreshCooldownSec ?? 30));
6948
7314
  const tag = opts.credentialTag;
7315
+ const unavailable = opts.unavailableReason != null && opts.unavailableReason.trim().length > 0;
7316
+ const attemptPath = opts.refreshAttemptPath ?? join(dirname(opts.credentialsPath), `refresh-attempt-${stampSlug(tag)}`);
7317
+ const message = unavailable ? `skaile: mount not connected: ${singleLine(opts.unavailableReason ?? "")}` : `skaile: no credential available for git mount ${mountIdFromTag(tag)} \u2014 the workspace mount is not authenticated.`;
7318
+ const readsStillWorkNote = "skaile: git fetch/ls-remote can still succeed on a public remote; only writes need this credential.";
6949
7319
  return `#!/bin/sh
6950
7320
  # Generated by skaile-runner -- do not edit.
6951
7321
  # Tier-2 git credential helper: serves the latest cached token and triggers a
@@ -6959,15 +7329,29 @@ op="\${1:-}"
6959
7329
  # (none today) also bail silently so we don't break future git versions.
6960
7330
  [ "$op" = "get" ] || exit 0
6961
7331
 
6962
- CRED_FILE='${credPath}'
6963
- FLAG_FILE='${flagPath}'
6964
- TAG='${tag}'
7332
+ CRED_FILE=${shQuote(opts.credentialsPath)}
7333
+ FLAG_FILE=${shQuote(opts.refreshFlagPath)}
7334
+ TAG=${shQuote(tag)}
7335
+ ATTEMPT_FILE=${shQuote(attemptPath)}
7336
+ MSG=${shQuote(message)}
7337
+ UNAVAILABLE=${unavailable ? 1 : 0}
6965
7338
 
6966
7339
  # Drain stdin: git always pipes the request envelope. Leaving it unread can
6967
7340
  # wedge git's helper FD. We don't actually need any of it -- the credentials
6968
7341
  # file already keys by URL prefix via the helper's gitconfig block.
6969
7342
  cat > /dev/null 2>&1 || true
6970
7343
 
7344
+ # 'quit=1' is what actually stops git's helper chain. Exiting 0 with no output
7345
+ # sends git to the next helper and then the terminal prompt ("could not read
7346
+ # Username"); exiting non-zero does the same. Only quit=1 makes git report our
7347
+ # reason as the terminal failure.
7348
+ if [ "$UNAVAILABLE" -eq 1 ]; then
7349
+ printf '%s\\n' "$MSG" >&2
7350
+ printf '%s\\n' ${shQuote(readsStillWorkNote)} >&2
7351
+ printf 'quit=1\\n'
7352
+ exit 0
7353
+ fi
7354
+
6971
7355
  # Cross-platform mtime in epoch seconds: GNU stat -c first (Linux), then BSD
6972
7356
  # stat -f (macOS), then 0 if neither is present (in which case we skip the
6973
7357
  # refresh-flag dance entirely and just serve what we have).
@@ -6985,38 +7369,59 @@ started_empty=0
6985
7369
  [ -s "$CRED_FILE" ] || started_empty=1
6986
7370
 
6987
7371
  if [ "$started_empty" -eq 1 ] || [ "$age" -gt ${triggerAgeSec} ]; then
6988
- # Touch the refresh-flag (atomic open+close emits 'change' to fs.watch).
6989
- : > "$FLAG_FILE" 2>/dev/null || true
7372
+ # Bound the dance. A refresh that keeps failing would otherwise cost every
7373
+ # git network op a fresh timeout, forever. The stamp below is written only
7374
+ # when a dance changed nothing, so a working refresh is never throttled.
7375
+ last_attempt=$(get_mtime "$ATTEMPT_FILE")
7376
+ if [ $((now - last_attempt)) -ge ${cooldownSec} ]; then
7377
+ # Touch the refresh-flag (atomic open+close emits 'change' to fs.watch).
7378
+ # The subshell is load-bearing: ':' is a POSIX *special built-in*, so a
7379
+ # redirection error on it terminates a non-interactive dash/ash outright --
7380
+ # '|| true' never runs, and the helper would die before reaching the
7381
+ # quit=1 block below. Run the redirection in a child instead.
7382
+ ( : > "$FLAG_FILE" ) 2>/dev/null || true
6990
7383
 
6991
- # Poll for the credentials file mtime to advance. usleep gives sub-second
6992
- # resolution where available; sleep 1 is the portable fallback. Resolve which
6993
- # once, up front, rather than probing PATH on every iteration.
6994
- if command -v usleep >/dev/null 2>&1; then
6995
- have_usleep=1
6996
- else
6997
- have_usleep=0
6998
- fi
6999
- end=$((now + ${waitTimeoutSec}))
7000
- while :; do
7001
- cur=$(date +%s 2>/dev/null || echo 0)
7002
- if [ "$cur" -ge "$end" ]; then
7003
- break
7004
- fi
7005
- new_mtime=$(get_mtime "$CRED_FILE")
7006
- if [ "$new_mtime" -gt "$mtime" ]; then
7007
- break
7008
- fi
7009
- # A same-second rewrite doesn't advance the second-resolution mtime, so an
7010
- # empty file that gained content must break on content, not on mtime.
7011
- if [ "$started_empty" -eq 1 ] && [ -s "$CRED_FILE" ]; then
7012
- break
7013
- fi
7014
- if [ "$have_usleep" -eq 1 ]; then
7015
- usleep 200000
7384
+ # Poll for the credentials file mtime to advance. usleep gives sub-second
7385
+ # resolution where available; sleep 1 is the portable fallback. Resolve which
7386
+ # once, up front, rather than probing PATH on every iteration.
7387
+ if command -v usleep >/dev/null 2>&1; then
7388
+ have_usleep=1
7016
7389
  else
7017
- sleep 1
7390
+ have_usleep=0
7391
+ fi
7392
+ refreshed=0
7393
+ end=$((now + ${waitTimeoutSec}))
7394
+ while :; do
7395
+ cur=$(date +%s 2>/dev/null || echo 0)
7396
+ if [ "$cur" -ge "$end" ]; then
7397
+ break
7398
+ fi
7399
+ new_mtime=$(get_mtime "$CRED_FILE")
7400
+ if [ "$new_mtime" -gt "$mtime" ]; then
7401
+ refreshed=1
7402
+ break
7403
+ fi
7404
+ # A same-second rewrite doesn't advance the second-resolution mtime, so an
7405
+ # empty file that gained content must break on content, not on mtime.
7406
+ if [ "$started_empty" -eq 1 ] && [ -s "$CRED_FILE" ]; then
7407
+ refreshed=1
7408
+ break
7409
+ fi
7410
+ if [ "$have_usleep" -eq 1 ]; then
7411
+ usleep 200000
7412
+ else
7413
+ sleep 1
7414
+ fi
7415
+ done
7416
+ if [ "$refreshed" -eq 0 ]; then
7417
+ # Subshell for the same reason as the flag write above. Here it also
7418
+ # matters in the ordinary case: the stamp lives in a runner-owned
7419
+ # directory, so under uid separation the agent can only truncate a file
7420
+ # the runner pre-created. A missing pre-create must cost the cooldown,
7421
+ # never the quit=1 report.
7422
+ ( : > "$ATTEMPT_FILE" ) 2>/dev/null || true
7018
7423
  fi
7019
- done
7424
+ fi
7020
7425
  fi
7021
7426
 
7022
7427
  # Find the credential line for THIS mount (matched by the trailing tag) and
@@ -7025,7 +7430,16 @@ fi
7025
7430
  # more than one matching line (shouldn't happen but defence-in-depth), we
7026
7431
  # take the last one (most recently written).
7027
7432
  line=$(grep -F " $TAG" "$CRED_FILE" 2>/dev/null | tail -n 1)
7028
- [ -n "$line" ] || exit 0
7433
+ if [ -z "$line" ]; then
7434
+ # No credential to serve. Terminate the chain rather than exiting 0, which
7435
+ # would hand git back to its username prompt -- the misleading error this
7436
+ # helper exists to replace. The same fetch-succeeds/push-fails confusion
7437
+ # applies here as on the known-dead path, so say so either way.
7438
+ printf '%s\\n' "$MSG" >&2
7439
+ printf '%s\\n' ${shQuote(readsStillWorkNote)} >&2
7440
+ printf 'quit=1\\n'
7441
+ exit 0
7442
+ fi
7029
7443
 
7030
7444
  # Strip the trailing ' <TAG>' from the URL.
7031
7445
  url=\${line% *}
@@ -7170,6 +7584,7 @@ var ManagedGitconfigCollisionError = class extends Error {
7170
7584
  var blockStart = (mountId) => `# skaile-mount: ${mountId}`;
7171
7585
  var blockEnd = (mountId) => `# skaile-mount: ${mountId} (end)`;
7172
7586
  var credentialTag = (mountId) => `# skaile-mount: ${mountId}`;
7587
+ var UNAVAILABLE_MARKER = "# skaile-mount-unavailable";
7173
7588
  function readFileOrEmpty(path) {
7174
7589
  try {
7175
7590
  return readFileSync(path, "utf-8");
@@ -7274,17 +7689,22 @@ function blockUrlPrefix(block) {
7274
7689
  }
7275
7690
  return void 0;
7276
7691
  }
7692
+ function isUnavailableBlock(block) {
7693
+ return block.lines.some((l) => l.trim() === UNAVAILABLE_MARKER);
7694
+ }
7277
7695
  function writeMountBlock(opts) {
7278
7696
  const { mountId, urlPrefix, token, gitconfigPath, credentialsPath, helperScriptPath } = opts;
7279
7697
  const text = readFileOrEmpty(gitconfigPath);
7280
- const blocks = parseBlocks(text);
7281
- for (const b of blocks) {
7698
+ const parsed = parseBlocks(text);
7699
+ for (const b of parsed) {
7282
7700
  if (b.mountId === "" || b.mountId === mountId) continue;
7701
+ if (isUnavailableBlock(b)) continue;
7283
7702
  const otherPrefix = blockUrlPrefix(b);
7284
7703
  if (otherPrefix === urlPrefix) {
7285
7704
  throw new ManagedGitconfigCollisionError(urlPrefix, b.mountId, mountId);
7286
7705
  }
7287
7706
  }
7707
+ const blocks = parsed.filter((b) => !(isUnavailableBlock(b) && blockUrlPrefix(b) === urlPrefix));
7288
7708
  const newBlock = {
7289
7709
  mountId,
7290
7710
  lines: buildBlock(mountId, urlPrefix, credentialsPath, helperScriptPath)
@@ -7302,6 +7722,48 @@ function writeMountBlock(opts) {
7302
7722
  filtered.push(buildCredentialLine(urlPrefix, token, mountId));
7303
7723
  writeCredentialLines(credentialsPath, filtered);
7304
7724
  }
7725
+ function writeUnavailableMountBlock(opts) {
7726
+ const { mountId, urlPrefix, gitconfigPath, credentialsPath, helperScriptPath } = opts;
7727
+ const lines = readCredentialLines(credentialsPath);
7728
+ const tag = credentialTag(mountId);
7729
+ const filtered = lines.filter((l) => !l.endsWith(tag));
7730
+ if (filtered.length !== lines.length) {
7731
+ writeCredentialLines(credentialsPath, filtered);
7732
+ }
7733
+ const blocks = parseBlocks(readFileOrEmpty(gitconfigPath));
7734
+ for (const b of blocks) {
7735
+ if (b.mountId === "" || b.mountId === mountId) continue;
7736
+ if (isUnavailableBlock(b)) continue;
7737
+ if (blockUrlPrefix(b) === urlPrefix) return;
7738
+ }
7739
+ const newLines = [
7740
+ blockStart(mountId),
7741
+ UNAVAILABLE_MARKER,
7742
+ `[credential "${urlPrefix}"]`,
7743
+ ` helper = !sh ${helperScriptPath}`,
7744
+ blockEnd(mountId)
7745
+ ];
7746
+ const existing = findExistingBlock(blocks, mountId);
7747
+ if (existing) {
7748
+ existing.lines = newLines;
7749
+ } else {
7750
+ blocks.push({ mountId, lines: newLines });
7751
+ }
7752
+ atomicWrite(gitconfigPath, renderBlocks(blocks), 416);
7753
+ }
7754
+ function removeUnavailableMountBlock(opts) {
7755
+ const { mountId, gitconfigPath, urlPrefix } = opts;
7756
+ const text = readFileOrEmpty(gitconfigPath);
7757
+ if (text.length === 0) return;
7758
+ const blocks = parseBlocks(text);
7759
+ const doomed = new Set(
7760
+ blocks.filter(
7761
+ (b) => isUnavailableBlock(b) && (b.mountId === mountId || urlPrefix !== void 0 && blockUrlPrefix(b) === urlPrefix)
7762
+ )
7763
+ );
7764
+ if (doomed.size === 0) return;
7765
+ atomicWrite(gitconfigPath, renderBlocks(blocks.filter((b) => !doomed.has(b))), 416);
7766
+ }
7305
7767
  function removeMountBlock(opts) {
7306
7768
  const { mountId, gitconfigPath, credentialsPath } = opts;
7307
7769
  const text = readFileOrEmpty(gitconfigPath);
@@ -7319,6 +7781,17 @@ function removeMountBlock(opts) {
7319
7781
  function writeHelperScript(scriptPath, body) {
7320
7782
  atomicWrite(scriptPath, body, 493);
7321
7783
  }
7784
+ function ensureRefreshAttemptStamp(stampPath) {
7785
+ if (existsSync(stampPath)) return;
7786
+ const dir = dirname(stampPath);
7787
+ if (!existsSync(dir)) {
7788
+ mkdirSync(dir, { recursive: true, mode: 448 });
7789
+ }
7790
+ const fd = openSync(stampPath, "a", 432);
7791
+ closeSync(fd);
7792
+ chmodSync(stampPath, 432);
7793
+ utimesSync(stampPath, 0, 0);
7794
+ }
7322
7795
  function atomicReplaceCredential(opts) {
7323
7796
  const { mountId, urlPrefix, token, credentialsPath } = opts;
7324
7797
  const lines = readCredentialLines(credentialsPath);
@@ -10079,6 +10552,6 @@ function createConnector() {
10079
10552
  return new FlowAdapter();
10080
10553
  }
10081
10554
 
10082
- export { AbstractConnector, BUILTIN_CONNECTOR_CATALOG, BoxConnector, ConnectorFieldMissingError, ConnectorManager, ConnectorStartupError, DeployConnector, DevServerConnector, FlowAdapter, GitConnector, GmailConnector, GoogleDriveConnector, LocalConnector, LogBuffer, ManagedGitconfigCollisionError, MattermostConnector, MemoryConnector, MinIOConnector, PortPool, PostgresConnector, S3Connector, SQLiteConnector, SessionInlineSubFlowRunner, SessionNodeExecutor, SessionSubpromptRunner, SharePointConnector, StaticServerConnector, TunnelConnector, WebDAVConnector, XStateConnector, XStateStoreConnector, __test, atomicReplaceCredential, buildConnectorPromptSection, buildFlowPromptEvent, buildSdkConnectorTools, buildSdkFlowTools, createConnector, createConnector10, createConnector11, createConnector12, createConnector13, createConnector14, createConnector15, createConnector16, createConnector17, createConnector18, createConnector19, createConnector2, createConnector20, createConnector3, createConnector4, createConnector5, createConnector6, createConnector7, createConnector8, createConnector9, createFsWatcher, createWorktree, defaultSpawn, driveOrchestratorTurn, ensureDirMode, ensureFleetMounted, findGitRoot, findMissingPackages, getConnector, installNpmPackages, isFleetManaged, isPackageResolvable, listConnectors, listWorktrees, readGitSyncStatus, registerBuiltinConnectors, removeMountBlock, renderCredentialHelperScript, tryGetConnector, worktreeRegistry, writeHelperScript, writeMountBlock };
10083
- //# sourceMappingURL=chunk-JTBF4OB2.js.map
10084
- //# sourceMappingURL=chunk-JTBF4OB2.js.map
10555
+ export { AbstractConnector, BUILTIN_CONNECTOR_CATALOG, BoxConnector, ConnectorFieldMissingError, ConnectorManager, ConnectorStartupError, DeployConnector, DevServerConnector, FlowAdapter, GitConnector, GmailConnector, GoogleDriveConnector, LocalConnector, LogBuffer, ManagedGitconfigCollisionError, MattermostConnector, MemoryConnector, MinIOConnector, PortPool, PostgresConnector, S3Connector, SQLiteConnector, SessionInlineSubFlowRunner, SessionNodeExecutor, SessionSubpromptRunner, SharePointConnector, StaticServerConnector, TunnelConnector, WebDAVConnector, XStateConnector, XStateStoreConnector, __test, atomicReplaceCredential, buildConnectorPromptSection, buildFlowPromptEvent, buildSdkConnectorTools, buildSdkFlowTools, createConnector, createConnector10, createConnector11, createConnector12, createConnector13, createConnector14, createConnector15, createConnector16, createConnector17, createConnector18, createConnector19, createConnector2, createConnector20, createConnector3, createConnector4, createConnector5, createConnector6, createConnector7, createConnector8, createConnector9, createFsWatcher, createWorktree, defaultSpawn, driveOrchestratorTurn, ensureDirMode, ensureFleetMounted, ensureRefreshAttemptStamp, findGitRoot, findMissingPackages, getConnector, installNpmPackages, isFleetManaged, isPackageResolvable, listConnectors, listWorktrees, readGitSyncStatus, registerBuiltinConnectors, removeMountBlock, removeUnavailableMountBlock, renderCredentialHelperScript, tryGetConnector, worktreeRegistry, writeHelperScript, writeMountBlock, writeUnavailableMountBlock };
10556
+ //# sourceMappingURL=chunk-6GDVGAIZ.js.map
10557
+ //# sourceMappingURL=chunk-6GDVGAIZ.js.map