@mastra/factory 0.2.2-alpha.0 → 0.2.2-alpha.2

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 (174) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/auth.d.ts +2 -2
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +401 -314
  5. package/dist/auth.js.map +1 -1
  6. package/dist/capabilities/connection.js +1 -1
  7. package/dist/capabilities/intake.js +1 -1
  8. package/dist/capabilities/version-control.js +1 -1
  9. package/dist/factory.d.ts.map +1 -1
  10. package/dist/factory.js +441 -39455
  11. package/dist/factory.js.map +1 -1
  12. package/dist/index.js +2 -39466
  13. package/dist/integrations/base.js +1 -1
  14. package/dist/integrations/github/config.js +36 -22
  15. package/dist/integrations/github/config.js.map +1 -1
  16. package/dist/integrations/github/factory-session.js +33 -37
  17. package/dist/integrations/github/factory-session.js.map +1 -1
  18. package/dist/integrations/github/integration.js +1037 -3624
  19. package/dist/integrations/github/integration.js.map +1 -1
  20. package/dist/integrations/github/issue-triage.js +71 -71
  21. package/dist/integrations/github/issue-triage.js.map +1 -1
  22. package/dist/integrations/github/pat.js +47 -36
  23. package/dist/integrations/github/pat.js.map +1 -1
  24. package/dist/integrations/github/provenance.js +72 -101
  25. package/dist/integrations/github/provenance.js.map +1 -1
  26. package/dist/integrations/github/routes.d.ts.map +1 -1
  27. package/dist/integrations/github/routes.js +1272 -1833
  28. package/dist/integrations/github/routes.js.map +1 -1
  29. package/dist/integrations/github/rules.js +203 -471
  30. package/dist/integrations/github/rules.js.map +1 -1
  31. package/dist/integrations/github/sandbox.d.ts +2 -0
  32. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  33. package/dist/integrations/github/sandbox.js +401 -326
  34. package/dist/integrations/github/sandbox.js.map +1 -1
  35. package/dist/integrations/github/session-subscriptions.js +165 -223
  36. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  37. package/dist/integrations/github/subscriptions.js +43 -51
  38. package/dist/integrations/github/subscriptions.js.map +1 -1
  39. package/dist/integrations/github/token-refresh.js +15 -18
  40. package/dist/integrations/github/token-refresh.js.map +1 -1
  41. package/dist/integrations/github/webhook.js +359 -343
  42. package/dist/integrations/github/webhook.js.map +1 -1
  43. package/dist/integrations/linear/agent-tools.js +77 -100
  44. package/dist/integrations/linear/agent-tools.js.map +1 -1
  45. package/dist/integrations/linear/integration.js +611 -1309
  46. package/dist/integrations/linear/integration.js.map +1 -1
  47. package/dist/integrations/linear/routes.js +227 -230
  48. package/dist/integrations/linear/routes.js.map +1 -1
  49. package/dist/integrations/linear/rules.js +126 -346
  50. package/dist/integrations/linear/rules.js.map +1 -1
  51. package/dist/integrations/linear/storage.js +1 -1
  52. package/dist/integrations/platform/api-client.js +131 -135
  53. package/dist/integrations/platform/api-client.js.map +1 -1
  54. package/dist/integrations/platform/github/event-worker.js +275 -573
  55. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  56. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  57. package/dist/integrations/platform/github/integration.js +845 -3942
  58. package/dist/integrations/platform/github/integration.js.map +1 -1
  59. package/dist/integrations/platform/linear/integration.js +457 -1309
  60. package/dist/integrations/platform/linear/integration.js.map +1 -1
  61. package/dist/integrations/workos/integration.js +80 -85
  62. package/dist/integrations/workos/integration.js.map +1 -1
  63. package/dist/routes/config.d.ts.map +1 -1
  64. package/dist/routes/config.js +933 -915
  65. package/dist/routes/config.js.map +1 -1
  66. package/dist/routes/custom-provider-source.js +110 -93
  67. package/dist/routes/custom-provider-source.js.map +1 -1
  68. package/dist/routes/fs.js +401 -349
  69. package/dist/routes/fs.js.map +1 -1
  70. package/dist/routes/intake.js +165 -165
  71. package/dist/routes/intake.js.map +1 -1
  72. package/dist/routes/oauth.js +447 -25166
  73. package/dist/routes/oauth.js.map +1 -1
  74. package/dist/routes/projects.js +437 -376
  75. package/dist/routes/projects.js.map +1 -1
  76. package/dist/routes/provider-credentials.js +76 -58
  77. package/dist/routes/provider-credentials.js.map +1 -1
  78. package/dist/routes/route.js +12 -8
  79. package/dist/routes/route.js.map +1 -1
  80. package/dist/routes/skills.js +160 -173
  81. package/dist/routes/skills.js.map +1 -1
  82. package/dist/routes/surface.js +250 -28618
  83. package/dist/routes/surface.js.map +1 -1
  84. package/dist/routes/tenant-credentials.js +141 -166
  85. package/dist/routes/tenant-credentials.js.map +1 -1
  86. package/dist/routes/work-items.js +654 -862
  87. package/dist/routes/work-items.js.map +1 -1
  88. package/dist/rules/binding-context.js +23 -35
  89. package/dist/rules/binding-context.js.map +1 -1
  90. package/dist/rules/defaults.js +200 -309
  91. package/dist/rules/defaults.js.map +1 -1
  92. package/dist/rules/dispatcher.d.ts.map +1 -1
  93. package/dist/rules/dispatcher.js +438 -677
  94. package/dist/rules/dispatcher.js.map +1 -1
  95. package/dist/rules/index.js +5 -599
  96. package/dist/rules/processor.js +344 -596
  97. package/dist/rules/processor.js.map +1 -1
  98. package/dist/rules/resolve.js +21 -18
  99. package/dist/rules/resolve.js.map +1 -1
  100. package/dist/rules/start-coordinator.js +168 -157
  101. package/dist/rules/start-coordinator.js.map +1 -1
  102. package/dist/rules/tools.js +50 -85
  103. package/dist/rules/tools.js.map +1 -1
  104. package/dist/rules/transition-service.js +183 -454
  105. package/dist/rules/transition-service.js.map +1 -1
  106. package/dist/rules/types.js +33 -29
  107. package/dist/rules/types.js.map +1 -1
  108. package/dist/rules/validation.js +266 -281
  109. package/dist/rules/validation.js.map +1 -1
  110. package/dist/sandbox/fleet.js +272 -250
  111. package/dist/sandbox/fleet.js.map +1 -1
  112. package/dist/sandbox/reattach.js +13 -6
  113. package/dist/sandbox/reattach.js.map +1 -1
  114. package/dist/server-error.js +12 -16
  115. package/dist/server-error.js.map +1 -1
  116. package/dist/skills/service.js +30 -35
  117. package/dist/skills/service.js.map +1 -1
  118. package/dist/spa-static.d.ts.map +1 -1
  119. package/dist/spa-static.js +91 -64
  120. package/dist/spa-static.js.map +1 -1
  121. package/dist/state-signing.js +73 -44
  122. package/dist/state-signing.js.map +1 -1
  123. package/dist/storage/domains/audit/agent-audit.js +60 -59
  124. package/dist/storage/domains/audit/agent-audit.js.map +1 -1
  125. package/dist/storage/domains/audit/base.js +169 -112
  126. package/dist/storage/domains/audit/base.js.map +1 -1
  127. package/dist/storage/domains/audit/domain.js +155 -153
  128. package/dist/storage/domains/audit/domain.js.map +1 -1
  129. package/dist/storage/domains/credentials/base.js +259 -243
  130. package/dist/storage/domains/credentials/base.js.map +1 -1
  131. package/dist/storage/domains/custom-providers/base.js +138 -143
  132. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  133. package/dist/storage/domains/intake/base.js +75 -57
  134. package/dist/storage/domains/intake/base.js.map +1 -1
  135. package/dist/storage/domains/integrations/base.js +310 -245
  136. package/dist/storage/domains/integrations/base.js.map +1 -1
  137. package/dist/storage/domains/memory-settings/base.js +117 -102
  138. package/dist/storage/domains/memory-settings/base.js.map +1 -1
  139. package/dist/storage/domains/model-packs/base.js +89 -88
  140. package/dist/storage/domains/model-packs/base.js.map +1 -1
  141. package/dist/storage/domains/projects/base.js +97 -92
  142. package/dist/storage/domains/projects/base.js.map +1 -1
  143. package/dist/storage/domains/queue-health/base.js +111 -76
  144. package/dist/storage/domains/queue-health/base.js.map +1 -1
  145. package/dist/storage/domains/source-control/base.d.ts +9 -0
  146. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  147. package/dist/storage/domains/source-control/base.js +777 -702
  148. package/dist/storage/domains/source-control/base.js.map +1 -1
  149. package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
  150. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  151. package/dist/storage/domains/source-control/inmemory.js +322 -344
  152. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  153. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  154. package/dist/storage/domains/work-items/base.js +1266 -1132
  155. package/dist/storage/domains/work-items/base.js.map +1 -1
  156. package/dist/storage/domains/work-items/metrics.js +202 -184
  157. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  158. package/dist/timing.d.ts +15 -0
  159. package/dist/timing.d.ts.map +1 -0
  160. package/dist/timing.js +34 -0
  161. package/dist/timing.js.map +1 -0
  162. package/dist/workspace.d.ts +2 -3
  163. package/dist/workspace.d.ts.map +1 -1
  164. package/dist/workspace.js +236 -438
  165. package/dist/workspace.js.map +1 -1
  166. package/factory-skills/factory-review/SKILL.md +9 -2
  167. package/package.json +9 -9
  168. package/dist/capabilities/connection.js.map +0 -1
  169. package/dist/capabilities/intake.js.map +0 -1
  170. package/dist/capabilities/version-control.js.map +0 -1
  171. package/dist/index.js.map +0 -1
  172. package/dist/integrations/base.js.map +0 -1
  173. package/dist/integrations/linear/storage.js.map +0 -1
  174. package/dist/rules/index.js.map +0 -1
package/dist/routes/fs.js CHANGED
@@ -1,394 +1,446 @@
1
- // src/routes/fs.ts
1
+ import { registerApiRoute } from "@mastra/core/server";
2
+ import { isAbsolute, join, posix, resolve, sep } from "path";
2
3
  import { lstat, open, readdir, realpath, stat } from "fs/promises";
3
4
  import { homedir } from "os";
4
- import { isAbsolute, join, posix as posixPath, resolve, sep } from "path";
5
5
  import { SandboxFilesystem } from "@mastra/code-sdk/agents/sandbox-filesystem";
6
6
  import { detectProject, getResourceIdOverride } from "@mastra/code-sdk/utils/project";
7
- import { registerApiRoute } from "@mastra/core/server";
8
- var MAX_TEXT_FILE_BYTES = 512 * 1024;
9
- var TEXT_DECODER = new TextDecoder("utf-8", { fatal: true });
10
- var APPROVED_RENDERED_ROOTS = /* @__PURE__ */ new Set([".artifacts"]);
7
+ //#region src/routes/fs.ts
8
+ const MAX_TEXT_FILE_BYTES = 512 * 1024;
9
+ const TEXT_DECODER = new TextDecoder("utf-8", { fatal: true });
10
+ const APPROVED_RENDERED_ROOTS = /* @__PURE__ */ new Set([".artifacts"]);
11
+ /** Erase a route handler's path-parameterized context to a plain `Context`. */
11
12
  function loose(c) {
12
- return c;
13
+ return c;
13
14
  }
15
+ /** Resolve the browsable root, defaulting to the user's home directory. */
14
16
  function resolveFsRoot(root) {
15
- return resolve(root && root.trim() ? root : homedir());
17
+ return resolve(root && root.trim() ? root : homedir());
16
18
  }
19
+ /** True when `candidate` is `root` or nested under it. */
17
20
  function isWithinRoot(candidate, root) {
18
- if (candidate === root) return true;
19
- const rootWithSep = root.endsWith(sep) ? root : root + sep;
20
- return candidate.startsWith(rootWithSep);
21
+ if (candidate === root) return true;
22
+ const rootWithSep = root.endsWith(sep) ? root : root + sep;
23
+ return candidate.startsWith(rootWithSep);
21
24
  }
22
25
  async function realOrResolved(path) {
23
- try {
24
- return await realpath(path);
25
- } catch {
26
- return path;
27
- }
26
+ try {
27
+ return await realpath(path);
28
+ } catch {
29
+ return path;
30
+ }
28
31
  }
32
+ /**
33
+ * Resolve a path's real location (following symlinks) and confirm it stays
34
+ * within `root`. Returns the real path when confined, or `null` when it escapes
35
+ * the root or does not exist. Used so a symlink inside the root that points
36
+ * outside it cannot be browsed or selected.
37
+ */
29
38
  async function realPathWithinRoot(candidate, root) {
30
- try {
31
- const real = await realpath(candidate);
32
- return isWithinRoot(real, root) ? real : null;
33
- } catch {
34
- return null;
35
- }
39
+ try {
40
+ const real = await realpath(candidate);
41
+ return isWithinRoot(real, root) ? real : null;
42
+ } catch {
43
+ return null;
44
+ }
36
45
  }
37
46
  function assertRelativePath(path, label) {
38
- const trimmed = path.trim();
39
- if (!trimmed) throw new Error(`Missing required query param: ${label}`);
40
- if (isAbsolute(trimmed)) throw new Error(`${label} must be relative`);
41
- if (trimmed.split(/[\\/]+/).includes("..")) throw new Error(`${label} escapes workspace`);
42
- const normalized = resolve("/", trimmed).slice(1);
43
- if (!normalized || normalized === ".." || normalized.startsWith(`..${sep}`))
44
- throw new Error(`${label} escapes workspace`);
45
- return normalized;
47
+ const trimmed = path.trim();
48
+ if (!trimmed) throw new Error(`Missing required query param: ${label}`);
49
+ if (isAbsolute(trimmed)) throw new Error(`${label} must be relative`);
50
+ if (trimmed.split(/[\\/]+/).includes("..")) throw new Error(`${label} escapes workspace`);
51
+ const normalized = resolve("/", trimmed).slice(1);
52
+ if (!normalized || normalized === ".." || normalized.startsWith(`..${sep}`)) throw new Error(`${label} escapes workspace`);
53
+ return normalized;
46
54
  }
47
55
  function assertApprovedRenderedRoot(renderedRoot) {
48
- const safeRoot = assertRelativePath(renderedRoot, "root");
49
- if (!APPROVED_RENDERED_ROOTS.has(safeRoot)) throw new Error("Root is not approved for rendered workspace access");
50
- return safeRoot;
56
+ const safeRoot = assertRelativePath(renderedRoot, "root");
57
+ if (!APPROVED_RENDERED_ROOTS.has(safeRoot)) throw new Error("Root is not approved for rendered workspace access");
58
+ return safeRoot;
51
59
  }
52
60
  async function confinedWorkspacePath(root, workspacePath) {
53
- const resolvedRoot = await realOrResolved(resolveFsRoot(root));
54
- const candidate = isAbsolute(workspacePath) ? resolve(workspacePath) : resolve(resolvedRoot, workspacePath);
55
- const workspace = await realPathWithinRoot(candidate, resolvedRoot);
56
- if (!workspace) throw new Error("Path is outside the browsable root");
57
- return { resolvedRoot, workspace };
61
+ const resolvedRoot = await realOrResolved(resolveFsRoot(root));
62
+ const workspace = await realPathWithinRoot(isAbsolute(workspacePath) ? resolve(workspacePath) : resolve(resolvedRoot, workspacePath), resolvedRoot);
63
+ if (!workspace) throw new Error("Path is outside the browsable root");
64
+ return {
65
+ resolvedRoot,
66
+ workspace
67
+ };
58
68
  }
59
69
  async function confinedWorkspaceRelativePath(root, workspacePath, relativePath) {
60
- const safeRelativePath = assertRelativePath(relativePath, "path");
61
- const { workspace } = await confinedWorkspacePath(root, workspacePath);
62
- const candidate = resolve(workspace, safeRelativePath);
63
- if (!isWithinRoot(candidate, workspace)) throw new Error("Path escapes workspace");
64
- const confinedPath = await realPathWithinRoot(candidate, workspace);
65
- if (!confinedPath) throw new Error("Path is outside the workspace");
66
- return { workspace, path: confinedPath, relativePath: safeRelativePath };
70
+ const safeRelativePath = assertRelativePath(relativePath, "path");
71
+ const { workspace } = await confinedWorkspacePath(root, workspacePath);
72
+ const candidate = resolve(workspace, safeRelativePath);
73
+ if (!isWithinRoot(candidate, workspace)) throw new Error("Path escapes workspace");
74
+ const confinedPath = await realPathWithinRoot(candidate, workspace);
75
+ if (!confinedPath) throw new Error("Path is outside the workspace");
76
+ return {
77
+ workspace,
78
+ path: confinedPath,
79
+ relativePath: safeRelativePath
80
+ };
67
81
  }
82
+ /**
83
+ * List the directories inside `requestedPath`, confined to `root`. An absent or
84
+ * out-of-root path is clamped to the root, so the worst a malicious client can
85
+ * do is browse within the allowed root.
86
+ */
68
87
  async function listDirectory(root, requestedPath) {
69
- const resolvedRoot = await realOrResolved(resolveFsRoot(root));
70
- let target = resolvedRoot;
71
- if (requestedPath && requestedPath.trim()) {
72
- const candidate = isAbsolute(requestedPath) ? resolve(requestedPath) : resolve(resolvedRoot, requestedPath);
73
- target = await realPathWithinRoot(candidate, resolvedRoot) ?? resolvedRoot;
74
- }
75
- try {
76
- const info = await stat(target);
77
- if (!info.isDirectory()) target = resolvedRoot;
78
- } catch {
79
- target = resolvedRoot;
80
- }
81
- const dirents = await readdir(target, { withFileTypes: true });
82
- const entries = [];
83
- for (const dirent of dirents) {
84
- if (dirent.name.startsWith(".")) continue;
85
- const entryPath = join(target, dirent.name);
86
- let isDir = dirent.isDirectory();
87
- if (dirent.isSymbolicLink()) {
88
- const real = await realPathWithinRoot(entryPath, resolvedRoot);
89
- isDir = real ? (await stat(real).catch(() => null))?.isDirectory() === true : false;
90
- }
91
- if (isDir) entries.push({ name: dirent.name, path: entryPath });
92
- }
93
- entries.sort((a, b) => a.name.localeCompare(b.name));
94
- const parent = target === resolvedRoot ? null : resolve(target, "..");
95
- return { root: resolvedRoot, path: target, parent, entries };
88
+ const resolvedRoot = await realOrResolved(resolveFsRoot(root));
89
+ let target = resolvedRoot;
90
+ if (requestedPath && requestedPath.trim()) target = await realPathWithinRoot(isAbsolute(requestedPath) ? resolve(requestedPath) : resolve(resolvedRoot, requestedPath), resolvedRoot) ?? resolvedRoot;
91
+ try {
92
+ if (!(await stat(target)).isDirectory()) target = resolvedRoot;
93
+ } catch {
94
+ target = resolvedRoot;
95
+ }
96
+ const dirents = await readdir(target, { withFileTypes: true });
97
+ const entries = [];
98
+ for (const dirent of dirents) {
99
+ if (dirent.name.startsWith(".")) continue;
100
+ const entryPath = join(target, dirent.name);
101
+ let isDir = dirent.isDirectory();
102
+ if (dirent.isSymbolicLink()) {
103
+ const real = await realPathWithinRoot(entryPath, resolvedRoot);
104
+ isDir = real ? (await stat(real).catch(() => null))?.isDirectory() === true : false;
105
+ }
106
+ if (isDir) entries.push({
107
+ name: dirent.name,
108
+ path: entryPath
109
+ });
110
+ }
111
+ entries.sort((a, b) => a.name.localeCompare(b.name));
112
+ const parent = target === resolvedRoot ? null : resolve(target, "..");
113
+ return {
114
+ root: resolvedRoot,
115
+ path: target,
116
+ parent,
117
+ entries
118
+ };
96
119
  }
97
120
  async function listRenderedEntries(rootPath, currentPath = rootPath) {
98
- const dirents = await readdir(currentPath, { withFileTypes: true });
99
- const entries = [];
100
- for (const dirent of dirents) {
101
- const entryPath = join(currentPath, dirent.name);
102
- const info = await lstat(entryPath);
103
- const relativePath = entryPath.slice(rootPath.length + 1);
104
- if (info.isDirectory()) {
105
- entries.push({
106
- name: dirent.name,
107
- path: relativePath,
108
- type: "directory",
109
- size: info.size,
110
- updatedAt: info.mtime.toISOString()
111
- });
112
- entries.push(...await listRenderedEntries(rootPath, entryPath));
113
- continue;
114
- }
115
- if (info.isFile()) {
116
- entries.push({
117
- name: dirent.name,
118
- path: relativePath,
119
- type: "file",
120
- size: info.size,
121
- updatedAt: info.mtime.toISOString()
122
- });
123
- }
124
- }
125
- return entries.sort((a, b) => a.path.localeCompare(b.path));
121
+ const dirents = await readdir(currentPath, { withFileTypes: true });
122
+ const entries = [];
123
+ for (const dirent of dirents) {
124
+ const entryPath = join(currentPath, dirent.name);
125
+ const info = await lstat(entryPath);
126
+ const relativePath = entryPath.slice(rootPath.length + 1);
127
+ if (info.isDirectory()) {
128
+ entries.push({
129
+ name: dirent.name,
130
+ path: relativePath,
131
+ type: "directory",
132
+ size: info.size,
133
+ updatedAt: info.mtime.toISOString()
134
+ });
135
+ entries.push(...await listRenderedEntries(rootPath, entryPath));
136
+ continue;
137
+ }
138
+ if (info.isFile()) entries.push({
139
+ name: dirent.name,
140
+ path: relativePath,
141
+ type: "file",
142
+ size: info.size,
143
+ updatedAt: info.mtime.toISOString()
144
+ });
145
+ }
146
+ return entries.sort((a, b) => a.path.localeCompare(b.path));
126
147
  }
127
148
  async function listWorkspaceRenderedPath(root, workspacePath, renderedRoot) {
128
- const safeRoot = assertApprovedRenderedRoot(renderedRoot);
129
- const { workspace } = await confinedWorkspacePath(root, workspacePath);
130
- const renderedPath = resolve(workspace, safeRoot);
131
- if (!isWithinRoot(renderedPath, workspace)) throw new Error("Root escapes workspace");
132
- const confinedRootPath = await realPathWithinRoot(renderedPath, workspace);
133
- if (!confinedRootPath) return { workspacePath: workspace, root: safeRoot, rootPath: renderedPath, entries: [] };
134
- const info = await stat(confinedRootPath);
135
- if (!info.isDirectory()) return { workspacePath: workspace, root: safeRoot, rootPath: confinedRootPath, entries: [] };
136
- return {
137
- workspacePath: workspace,
138
- root: safeRoot,
139
- rootPath: confinedRootPath,
140
- entries: await listRenderedEntries(confinedRootPath)
141
- };
149
+ const safeRoot = assertApprovedRenderedRoot(renderedRoot);
150
+ const { workspace } = await confinedWorkspacePath(root, workspacePath);
151
+ const renderedPath = resolve(workspace, safeRoot);
152
+ if (!isWithinRoot(renderedPath, workspace)) throw new Error("Root escapes workspace");
153
+ const confinedRootPath = await realPathWithinRoot(renderedPath, workspace);
154
+ if (!confinedRootPath) return {
155
+ workspacePath: workspace,
156
+ root: safeRoot,
157
+ rootPath: renderedPath,
158
+ entries: []
159
+ };
160
+ if (!(await stat(confinedRootPath)).isDirectory()) return {
161
+ workspacePath: workspace,
162
+ root: safeRoot,
163
+ rootPath: confinedRootPath,
164
+ entries: []
165
+ };
166
+ return {
167
+ workspacePath: workspace,
168
+ root: safeRoot,
169
+ rootPath: confinedRootPath,
170
+ entries: await listRenderedEntries(confinedRootPath)
171
+ };
142
172
  }
143
173
  async function readWorkspaceFile(root, workspacePath, path) {
144
- const safePath = assertRelativePath(path, "path");
145
- const relativeRoot = safePath.split("/")[0] ?? "";
146
- assertApprovedRenderedRoot(relativeRoot);
147
- const {
148
- workspace,
149
- path: confinedPath,
150
- relativePath
151
- } = await confinedWorkspaceRelativePath(root, workspacePath, path);
152
- const info = await lstat(confinedPath);
153
- if (info.isDirectory()) throw new Error("Path is a directory");
154
- if (!info.isFile()) throw new Error("Unsupported file type");
155
- const bytesToRead = Math.min(info.size, MAX_TEXT_FILE_BYTES);
156
- const contentBuffer = Buffer.alloc(bytesToRead);
157
- const handle = await open(confinedPath, "r");
158
- try {
159
- await handle.read(contentBuffer, 0, bytesToRead, 0);
160
- } finally {
161
- await handle.close();
162
- }
163
- try {
164
- const content = TEXT_DECODER.decode(contentBuffer);
165
- return {
166
- workspacePath: workspace,
167
- path: relativePath,
168
- name: relativePath.split("/").pop() ?? relativePath,
169
- size: info.size,
170
- updatedAt: info.mtime.toISOString(),
171
- contentType: "text",
172
- content,
173
- truncated: info.size > MAX_TEXT_FILE_BYTES
174
- };
175
- } catch {
176
- return {
177
- workspacePath: workspace,
178
- path: relativePath,
179
- name: relativePath.split("/").pop() ?? relativePath,
180
- size: info.size,
181
- updatedAt: info.mtime.toISOString(),
182
- contentType: "unsupported"
183
- };
184
- }
174
+ assertApprovedRenderedRoot(assertRelativePath(path, "path").split("/")[0] ?? "");
175
+ const { workspace, path: confinedPath, relativePath } = await confinedWorkspaceRelativePath(root, workspacePath, path);
176
+ const info = await lstat(confinedPath);
177
+ if (info.isDirectory()) throw new Error("Path is a directory");
178
+ if (!info.isFile()) throw new Error("Unsupported file type");
179
+ const bytesToRead = Math.min(info.size, MAX_TEXT_FILE_BYTES);
180
+ const contentBuffer = Buffer.alloc(bytesToRead);
181
+ const handle = await open(confinedPath, "r");
182
+ try {
183
+ await handle.read(contentBuffer, 0, bytesToRead, 0);
184
+ } finally {
185
+ await handle.close();
186
+ }
187
+ try {
188
+ const content = TEXT_DECODER.decode(contentBuffer);
189
+ return {
190
+ workspacePath: workspace,
191
+ path: relativePath,
192
+ name: relativePath.split("/").pop() ?? relativePath,
193
+ size: info.size,
194
+ updatedAt: info.mtime.toISOString(),
195
+ contentType: "text",
196
+ content,
197
+ truncated: info.size > MAX_TEXT_FILE_BYTES
198
+ };
199
+ } catch {
200
+ return {
201
+ workspacePath: workspace,
202
+ path: relativePath,
203
+ name: relativePath.split("/").pop() ?? relativePath,
204
+ size: info.size,
205
+ updatedAt: info.mtime.toISOString(),
206
+ contentType: "unsupported"
207
+ };
208
+ }
185
209
  }
186
210
  async function listArtifacts(root, workspacePath) {
187
- const listing = await listWorkspaceRenderedPath(root, workspacePath, ".artifacts");
188
- return {
189
- rootPath: listing.workspacePath,
190
- artifactsPath: listing.rootPath,
191
- entries: listing.entries
192
- };
211
+ const listing = await listWorkspaceRenderedPath(root, workspacePath, ".artifacts");
212
+ return {
213
+ rootPath: listing.workspacePath,
214
+ artifactsPath: listing.rootPath,
215
+ entries: listing.entries
216
+ };
193
217
  }
218
+ /**
219
+ * Resolve a `workspacePath` query param as a Factory session id. Returns the
220
+ * session when one exists and the caller owns it, `null` when no session
221
+ * matches (the caller should fall back to local-path handling), and throws
222
+ * when a session exists but belongs to another tenant.
223
+ */
194
224
  async function resolveAuthorizedSession(c, deps, workspacePath) {
195
- if (!deps) return null;
196
- const session = await deps.sessions.getBySessionId(workspacePath);
197
- if (!session) return null;
198
- if (deps.auth.enabled()) {
199
- await deps.auth.ensureUser(c);
200
- const tenant = deps.auth.tenant(c);
201
- if (!tenant || tenant.orgId !== session.orgId || tenant.userId !== session.userId) {
202
- throw new Error("Session is not available to the current user");
203
- }
204
- }
205
- return session;
225
+ if (!deps) return null;
226
+ const session = await deps.sessions.getBySessionId(workspacePath);
227
+ if (!session) return null;
228
+ if (deps.auth.enabled()) {
229
+ await deps.auth.ensureUser(c);
230
+ const tenant = deps.auth.tenant(c);
231
+ if (!tenant || tenant.orgId !== session.orgId || tenant.userId !== session.userId) throw new Error("Session is not available to the current user");
232
+ }
233
+ return session;
206
234
  }
235
+ /**
236
+ * Reattach to the session's sandbox and wrap its workdir in a
237
+ * `SandboxFilesystem`. Returns `null` when the session has no provisioned
238
+ * sandbox yet (nothing materialized → nothing to list), or when the sandbox
239
+ * can no longer be reattached (e.g. torn down by the provider's idle GC).
240
+ * This is a passive read path, so it never re-provisions: the session's
241
+ * filesystem is preserved in its provider checkpoint and comes back the next
242
+ * time the workspace is actually opened (e.g. by sending a message).
243
+ */
207
244
  async function sessionSandbox(fleet, session) {
208
- if (!fleet.enabled || !session.sandboxId || !session.sandboxWorkdir) return null;
209
- let sandbox;
210
- try {
211
- sandbox = await fleet.reattachSandbox(session.sandboxId, { workingDirectory: session.sandboxWorkdir });
212
- } catch {
213
- return null;
214
- }
215
- return {
216
- sandbox,
217
- filesystem: new SandboxFilesystem({ sandbox, workdir: session.sandboxWorkdir }),
218
- workdir: session.sandboxWorkdir
219
- };
245
+ if (!fleet.enabled || !session.sandboxId || !session.sandboxWorkdir) return null;
246
+ let sandbox;
247
+ try {
248
+ sandbox = await fleet.reattachSandbox(session.sandboxId, { workingDirectory: session.sandboxWorkdir });
249
+ } catch {
250
+ return null;
251
+ }
252
+ return {
253
+ sandbox,
254
+ filesystem: new SandboxFilesystem({
255
+ sandbox,
256
+ workdir: session.sandboxWorkdir
257
+ }),
258
+ workdir: session.sandboxWorkdir
259
+ };
220
260
  }
261
+ /** List an approved rendered root inside a Factory session's sandbox workdir. */
221
262
  async function listSessionRenderedPath(fleet, session, renderedRoot) {
222
- const safeRoot = assertApprovedRenderedRoot(renderedRoot);
223
- const rootPath = posixPath.join(session.sandboxWorkdir ?? "", safeRoot);
224
- const empty = { workspacePath: session.sessionId, root: safeRoot, rootPath, entries: [] };
225
- const handle = await sessionSandbox(fleet, session);
226
- if (!handle) return empty;
227
- const quotedRoot = `'${rootPath.replace(/'/g, `'\\''`)}'`;
228
- const result = await handle.sandbox.executeCommand(
229
- "sh",
230
- [
231
- "-c",
232
- `test -d ${quotedRoot} && find ${quotedRoot} -mindepth 1 -printf '%y\\t%s\\t%T@\\t%p\\n' 2>/dev/null || true`
233
- ],
234
- { timeout: 3e4 }
235
- );
236
- if (result.exitCode !== 0) return empty;
237
- const entries = [];
238
- for (const line of result.stdout.split("\n")) {
239
- if (!line) continue;
240
- const [type, sizeStr, mtimeStr, ...pathParts] = line.split(" ");
241
- const fullPath = pathParts.join(" ");
242
- if (!fullPath || !fullPath.startsWith(`${rootPath}/`)) continue;
243
- const relativePath = fullPath.slice(rootPath.length + 1);
244
- entries.push({
245
- name: posixPath.basename(relativePath),
246
- path: relativePath,
247
- type: type === "d" ? "directory" : "file",
248
- size: type === "d" ? 0 : Number(sizeStr) || 0,
249
- updatedAt: new Date((Number(mtimeStr) || 0) * 1e3).toISOString()
250
- });
251
- }
252
- entries.sort((a, b) => a.path.localeCompare(b.path));
253
- return { workspacePath: session.sessionId, root: safeRoot, rootPath, entries };
263
+ const safeRoot = assertApprovedRenderedRoot(renderedRoot);
264
+ const rootPath = posix.join(session.sandboxWorkdir ?? "", safeRoot);
265
+ const empty = {
266
+ workspacePath: session.sessionId,
267
+ root: safeRoot,
268
+ rootPath,
269
+ entries: []
270
+ };
271
+ const handle = await sessionSandbox(fleet, session);
272
+ if (!handle) return empty;
273
+ const quotedRoot = `'${rootPath.replace(/'/g, `'\\''`)}'`;
274
+ const result = await handle.sandbox.executeCommand("sh", ["-c", `test -d ${quotedRoot} && find ${quotedRoot} -mindepth 1 -printf '%y\\t%s\\t%T@\\t%p\\n' 2>/dev/null || true`], { timeout: 3e4 });
275
+ if (result.exitCode !== 0) return empty;
276
+ const entries = [];
277
+ for (const line of result.stdout.split("\n")) {
278
+ if (!line) continue;
279
+ const [type, sizeStr, mtimeStr, ...pathParts] = line.split(" ");
280
+ const fullPath = pathParts.join(" ");
281
+ if (!fullPath || !fullPath.startsWith(`${rootPath}/`)) continue;
282
+ const relativePath = fullPath.slice(rootPath.length + 1);
283
+ entries.push({
284
+ name: posix.basename(relativePath),
285
+ path: relativePath,
286
+ type: type === "d" ? "directory" : "file",
287
+ size: type === "d" ? 0 : Number(sizeStr) || 0,
288
+ updatedAt: (/* @__PURE__ */ new Date((Number(mtimeStr) || 0) * 1e3)).toISOString()
289
+ });
290
+ }
291
+ entries.sort((a, b) => a.path.localeCompare(b.path));
292
+ return {
293
+ workspacePath: session.sessionId,
294
+ root: safeRoot,
295
+ rootPath,
296
+ entries
297
+ };
254
298
  }
299
+ /** Read a file under an approved rendered root inside a session's sandbox. */
255
300
  async function readSessionWorkspaceFile(fleet, session, path) {
256
- const safePath = assertRelativePath(path, "path");
257
- assertApprovedRenderedRoot(safePath.split("/")[0] ?? "");
258
- const handle = await sessionSandbox(fleet, session);
259
- if (!handle) throw new Error("Session workspace is not available");
260
- const { filesystem } = handle;
261
- const info = await filesystem.stat(safePath);
262
- if (info.type === "directory") throw new Error("Path is a directory");
263
- const buffer = await filesystem.readFile(safePath);
264
- const truncated = buffer.length > MAX_TEXT_FILE_BYTES;
265
- const base = {
266
- workspacePath: session.sessionId,
267
- path: safePath,
268
- name: posixPath.basename(safePath),
269
- size: buffer.length,
270
- updatedAt: info.modifiedAt.toISOString()
271
- };
272
- try {
273
- const content = TEXT_DECODER.decode(truncated ? buffer.subarray(0, MAX_TEXT_FILE_BYTES) : buffer);
274
- return { ...base, contentType: "text", content, truncated };
275
- } catch {
276
- return { ...base, contentType: "unsupported" };
277
- }
301
+ const safePath = assertRelativePath(path, "path");
302
+ assertApprovedRenderedRoot(safePath.split("/")[0] ?? "");
303
+ const handle = await sessionSandbox(fleet, session);
304
+ if (!handle) throw new Error("Session workspace is not available");
305
+ const { filesystem } = handle;
306
+ const info = await filesystem.stat(safePath);
307
+ if (info.type === "directory") throw new Error("Path is a directory");
308
+ const buffer = await filesystem.readFile(safePath);
309
+ const truncated = buffer.length > MAX_TEXT_FILE_BYTES;
310
+ const base = {
311
+ workspacePath: session.sessionId,
312
+ path: safePath,
313
+ name: posix.basename(safePath),
314
+ size: buffer.length,
315
+ updatedAt: info.modifiedAt.toISOString()
316
+ };
317
+ try {
318
+ const content = TEXT_DECODER.decode(truncated ? buffer.subarray(0, MAX_TEXT_FILE_BYTES) : buffer);
319
+ return {
320
+ ...base,
321
+ contentType: "text",
322
+ content,
323
+ truncated
324
+ };
325
+ } catch {
326
+ return {
327
+ ...base,
328
+ contentType: "unsupported"
329
+ };
330
+ }
278
331
  }
332
+ /**
333
+ * Resolve a project path to the same resourceId the TUI uses. Mirrors
334
+ * `createMastraCode`: detect the project, then apply any resourceId override
335
+ * (MASTRA_RESOURCE_ID env var or `.mastracode/database.json`). This is the
336
+ * shared continuity point — start in the TUI, continue on the web, same path
337
+ * → same resourceId → same session.
338
+ */
279
339
  function resolveCodebase(projectPath) {
280
- const info = detectProject(projectPath);
281
- const override = getResourceIdOverride(info.rootPath);
282
- return {
283
- resourceId: override ?? info.resourceId,
284
- name: info.name,
285
- rootPath: info.rootPath,
286
- gitUrl: info.gitUrl,
287
- gitBranch: info.gitBranch
288
- };
340
+ const info = detectProject(projectPath);
341
+ return {
342
+ resourceId: getResourceIdOverride(info.rootPath) ?? info.resourceId,
343
+ name: info.name,
344
+ rootPath: info.rootPath,
345
+ gitUrl: info.gitUrl,
346
+ gitBranch: info.gitBranch
347
+ };
289
348
  }
349
+ /**
350
+ * Build the web filesystem routes as Mastra `apiRoutes`:
351
+ * - `GET /web/fs/list?path=...` — browse directories (confined to root)
352
+ * - `GET /web/codebase/resolve?path=...` — TUI-compatible codebase resourceId
353
+ */
290
354
  function buildFsRoutes(options = {}) {
291
- const root = resolveFsRoot(options.root);
292
- const sessionFs = options.sessionFs;
293
- return [
294
- registerApiRoute("/web/fs/list", {
295
- method: "GET",
296
- requiresAuth: false,
297
- handler: async (c) => {
298
- const path = c.req.query("path");
299
- try {
300
- const listing = await listDirectory(root, path);
301
- return c.json(listing);
302
- } catch (error) {
303
- const message = error instanceof Error ? error.message : String(error);
304
- return c.json({ error: message }, 500);
305
- }
306
- }
307
- }),
308
- registerApiRoute("/web/artifacts/list", {
309
- method: "GET",
310
- requiresAuth: false,
311
- handler: async (c) => {
312
- const path = c.req.query("path");
313
- if (!path) return c.json({ error: "Missing required query param: path" }, 400);
314
- try {
315
- return c.json(await listArtifacts(root, path));
316
- } catch (error) {
317
- const message = error instanceof Error ? error.message : String(error);
318
- const status = message === "Path is outside the browsable root" ? 403 : 500;
319
- return c.json({ error: message }, status);
320
- }
321
- }
322
- }),
323
- registerApiRoute("/web/workspace/rendered/list", {
324
- method: "GET",
325
- requiresAuth: false,
326
- handler: async (c) => {
327
- const workspacePath = c.req.query("workspacePath");
328
- const renderedRoot = c.req.query("root");
329
- if (!workspacePath) return c.json({ error: "Missing required query param: workspacePath" }, 400);
330
- if (!renderedRoot) return c.json({ error: "Missing required query param: root" }, 400);
331
- try {
332
- const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);
333
- if (session && sessionFs) {
334
- return c.json(await listSessionRenderedPath(sessionFs.fleet, session, renderedRoot));
335
- }
336
- return c.json(await listWorkspaceRenderedPath(root, workspacePath, renderedRoot));
337
- } catch (error) {
338
- const message = error instanceof Error ? error.message : String(error);
339
- const status = message.includes("outside") || message.includes("relative") || message.includes("escapes") || message.includes("not approved") || message.includes("not available") ? 403 : 500;
340
- return c.json({ error: message }, status);
341
- }
342
- }
343
- }),
344
- registerApiRoute("/web/workspace/file", {
345
- method: "GET",
346
- requiresAuth: false,
347
- handler: async (c) => {
348
- const workspacePath = c.req.query("workspacePath");
349
- const path = c.req.query("path");
350
- if (!workspacePath) return c.json({ error: "Missing required query param: workspacePath" }, 400);
351
- if (!path) return c.json({ error: "Missing required query param: path" }, 400);
352
- try {
353
- const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);
354
- if (session && sessionFs) {
355
- return c.json(await readSessionWorkspaceFile(sessionFs.fleet, session, path));
356
- }
357
- return c.json(await readWorkspaceFile(root, workspacePath, path));
358
- } catch (error) {
359
- const message = error instanceof Error ? error.message : String(error);
360
- const status = message.includes("outside") || message.includes("relative") || message.includes("escapes") || message.includes("not approved") || message.includes("not available") ? 403 : message.includes("directory") ? 400 : message.includes("not found") ? 404 : 500;
361
- return c.json({ error: message }, status);
362
- }
363
- }
364
- }),
365
- registerApiRoute("/web/codebase/resolve", {
366
- method: "GET",
367
- requiresAuth: false,
368
- handler: async (c) => {
369
- const path = c.req.query("path");
370
- if (!path) return c.json({ error: "Missing required query param: path" }, 400);
371
- const confined = await realPathWithinRoot(isAbsolute(path) ? resolve(path) : resolve(root, path), root);
372
- if (!confined) return c.json({ error: "Path is outside the browsable root" }, 403);
373
- try {
374
- return c.json(resolveCodebase(confined));
375
- } catch (error) {
376
- const message = error instanceof Error ? error.message : String(error);
377
- return c.json({ error: message }, 500);
378
- }
379
- }
380
- })
381
- ];
355
+ const root = resolveFsRoot(options.root);
356
+ const sessionFs = options.sessionFs;
357
+ return [
358
+ registerApiRoute("/web/fs/list", {
359
+ method: "GET",
360
+ requiresAuth: false,
361
+ handler: async (c) => {
362
+ const path = c.req.query("path");
363
+ try {
364
+ const listing = await listDirectory(root, path);
365
+ return c.json(listing);
366
+ } catch (error) {
367
+ const message = error instanceof Error ? error.message : String(error);
368
+ return c.json({ error: message }, 500);
369
+ }
370
+ }
371
+ }),
372
+ registerApiRoute("/web/artifacts/list", {
373
+ method: "GET",
374
+ requiresAuth: false,
375
+ handler: async (c) => {
376
+ const path = c.req.query("path");
377
+ if (!path) return c.json({ error: "Missing required query param: path" }, 400);
378
+ try {
379
+ return c.json(await listArtifacts(root, path));
380
+ } catch (error) {
381
+ const message = error instanceof Error ? error.message : String(error);
382
+ const status = message === "Path is outside the browsable root" ? 403 : 500;
383
+ return c.json({ error: message }, status);
384
+ }
385
+ }
386
+ }),
387
+ registerApiRoute("/web/workspace/rendered/list", {
388
+ method: "GET",
389
+ requiresAuth: false,
390
+ handler: async (c) => {
391
+ const workspacePath = c.req.query("workspacePath");
392
+ const renderedRoot = c.req.query("root");
393
+ if (!workspacePath) return c.json({ error: "Missing required query param: workspacePath" }, 400);
394
+ if (!renderedRoot) return c.json({ error: "Missing required query param: root" }, 400);
395
+ try {
396
+ const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);
397
+ if (session && sessionFs) return c.json(await listSessionRenderedPath(sessionFs.fleet, session, renderedRoot));
398
+ return c.json(await listWorkspaceRenderedPath(root, workspacePath, renderedRoot));
399
+ } catch (error) {
400
+ const message = error instanceof Error ? error.message : String(error);
401
+ const status = message.includes("outside") || message.includes("relative") || message.includes("escapes") || message.includes("not approved") || message.includes("not available") ? 403 : 500;
402
+ return c.json({ error: message }, status);
403
+ }
404
+ }
405
+ }),
406
+ registerApiRoute("/web/workspace/file", {
407
+ method: "GET",
408
+ requiresAuth: false,
409
+ handler: async (c) => {
410
+ const workspacePath = c.req.query("workspacePath");
411
+ const path = c.req.query("path");
412
+ if (!workspacePath) return c.json({ error: "Missing required query param: workspacePath" }, 400);
413
+ if (!path) return c.json({ error: "Missing required query param: path" }, 400);
414
+ try {
415
+ const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);
416
+ if (session && sessionFs) return c.json(await readSessionWorkspaceFile(sessionFs.fleet, session, path));
417
+ return c.json(await readWorkspaceFile(root, workspacePath, path));
418
+ } catch (error) {
419
+ const message = error instanceof Error ? error.message : String(error);
420
+ const status = message.includes("outside") || message.includes("relative") || message.includes("escapes") || message.includes("not approved") || message.includes("not available") ? 403 : message.includes("directory") ? 400 : message.includes("not found") ? 404 : 500;
421
+ return c.json({ error: message }, status);
422
+ }
423
+ }
424
+ }),
425
+ registerApiRoute("/web/codebase/resolve", {
426
+ method: "GET",
427
+ requiresAuth: false,
428
+ handler: async (c) => {
429
+ const path = c.req.query("path");
430
+ if (!path) return c.json({ error: "Missing required query param: path" }, 400);
431
+ const confined = await realPathWithinRoot(isAbsolute(path) ? resolve(path) : resolve(root, path), root);
432
+ if (!confined) return c.json({ error: "Path is outside the browsable root" }, 403);
433
+ try {
434
+ return c.json(resolveCodebase(confined));
435
+ } catch (error) {
436
+ const message = error instanceof Error ? error.message : String(error);
437
+ return c.json({ error: message }, 500);
438
+ }
439
+ }
440
+ })
441
+ ];
382
442
  }
383
- export {
384
- buildFsRoutes,
385
- listArtifacts,
386
- listDirectory,
387
- listSessionRenderedPath,
388
- listWorkspaceRenderedPath,
389
- readSessionWorkspaceFile,
390
- readWorkspaceFile,
391
- resolveCodebase,
392
- resolveFsRoot
393
- };
443
+ //#endregion
444
+ export { buildFsRoutes, listArtifacts, listDirectory, listSessionRenderedPath, listWorkspaceRenderedPath, readSessionWorkspaceFile, readWorkspaceFile, resolveCodebase, resolveFsRoot };
445
+
394
446
  //# sourceMappingURL=fs.js.map