@almadar/workspace 0.11.4 → 0.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -10,6 +10,6 @@ export { openWorkspace } from './open-workspace.js';
10
10
  export { listWorkspaces } from './list-workspaces.js';
11
11
  export { deleteWorkspace } from './delete-workspace.js';
12
12
  export { openAccount } from './account.js';
13
- export type { WorkspaceService, InstantiateWorkspaceSeed, WorkspaceObserver, WorkspaceWriteEvent, WorkspaceWatchEvent, OpenWorkspaceOptions, ListWorkspacesOptions, WorkspaceSummary, RestoreBackend, GitHubConfig, GitStatusInfo, SnapshotMeta, WorkspaceArchiveBackend, FileTreeNode, AccountService, AccountConfig, ProviderCredential, AuthToken, AccountIdentity, OpenAccountOptions, } from './types.js';
13
+ export type { WorkspaceService, InstantiateWorkspaceSeed, WorkspaceObserver, WorkspaceWriteEvent, WorkspaceWatchEvent, OpenWorkspaceOptions, MountEntry, ListWorkspacesOptions, WorkspaceSummary, RestoreBackend, GitHubConfig, GitStatusInfo, SnapshotMeta, WorkspaceArchiveBackend, FileTreeNode, AccountService, AccountConfig, ProviderCredential, AuthToken, AccountIdentity, OpenAccountOptions, } from './types.js';
14
14
  export type { WorkspaceIndex, EmbedderPort, ResolveResult, ResolveOptions, TraitRefEmit, WorkspaceIndexStats, OrbitalIndexEntry, ExtraTraitIdentity, RetrievalResult, RetrievalOptions, RecentlyEditedOptions, EventEdge, EntityBinding, RuleBinding, RecencyEntry, IntentMaps, ComposedMaps, BM25Document, BM25Table, BM25Options, WorkspaceIndexManifest, } from './workspace-index/types.js';
15
15
  export { DEFAULT_COERCION_THRESHOLD, DEFAULT_RETRIEVAL_TOP_K, RRF_K, WORKSPACE_INDEX_SCHEMA_VERSION, } from './workspace-index/types.js';
package/dist/index.js CHANGED
@@ -293,7 +293,7 @@ function appMarkerFile(workDir) {
293
293
  function workspaceIndexManifestFile(workDir) {
294
294
  return path2.join(workDir, WORKSPACE_LAYOUT.ALMADAR_DIR, "index.json");
295
295
  }
296
- function sandboxedPath(workDir, relPath) {
296
+ function sandboxedPath(workDir, relPath, mounts) {
297
297
  if (typeof relPath !== "string" || relPath.length === 0) {
298
298
  throw new Error("sandbox: empty relative path");
299
299
  }
@@ -303,6 +303,20 @@ function sandboxedPath(workDir, relPath) {
303
303
  if (/^[a-zA-Z]:[\\/]/.test(relPath)) {
304
304
  throw new Error(`sandbox: drive-letter paths are not allowed (${relPath})`);
305
305
  }
306
+ if (mounts) {
307
+ for (const [prefix, mountRoot] of mounts) {
308
+ const prefixSep = prefix + "/";
309
+ if (relPath === prefix || relPath.startsWith(prefixSep)) {
310
+ const mountAbs = path2.resolve(mountRoot);
311
+ const rest = relPath === prefix ? "." : relPath.slice(prefixSep.length);
312
+ const target2 = path2.resolve(mountAbs, rest);
313
+ if (target2 !== mountAbs && !target2.startsWith(mountAbs + path2.sep)) {
314
+ throw new Error(`sandbox: mount path escapes mount root (${relPath})`);
315
+ }
316
+ return target2;
317
+ }
318
+ }
319
+ }
306
320
  const rootAbs = path2.resolve(workDir);
307
321
  const target = path2.resolve(rootAbs, relPath);
308
322
  if (target !== rootAbs && !target.startsWith(rootAbs + path2.sep)) {
@@ -1770,6 +1784,8 @@ var WorkspaceServiceImpl = class {
1770
1784
  constructor(args) {
1771
1785
  /** Per-absolute-path serial queue. */
1772
1786
  this.writeQueue = /* @__PURE__ */ new Map();
1787
+ /** Mount prefix → resolved absolute directory. Built from constructor `mounts` + runtime `mountReadonly`. */
1788
+ this.mountMap = /* @__PURE__ */ new Map();
1773
1789
  this.workDir = args.workDir;
1774
1790
  this.backend = args.backend;
1775
1791
  this.sinks = args.sinks;
@@ -1787,6 +1803,9 @@ var WorkspaceServiceImpl = class {
1787
1803
  readSchema: () => this.readSchema(),
1788
1804
  readHistory: (orbital) => this.readHistory(orbital)
1789
1805
  });
1806
+ if (args.mounts) {
1807
+ for (const m of args.mounts) this.mountMap.set(m.prefix, path2.resolve(m.absPath));
1808
+ }
1790
1809
  }
1791
1810
  // === Identity ===
1792
1811
  get appId() {
@@ -2109,7 +2128,7 @@ var WorkspaceServiceImpl = class {
2109
2128
  }
2110
2129
  // === Sandboxed generic file I/O ===
2111
2130
  async readFile(relPath) {
2112
- const abs = sandboxedPath(this.workDir, relPath);
2131
+ const abs = this.sandbox(relPath);
2113
2132
  if (!this.backend.exists(abs)) return null;
2114
2133
  try {
2115
2134
  return await this.backend.readFile(abs);
@@ -2118,7 +2137,8 @@ var WorkspaceServiceImpl = class {
2118
2137
  }
2119
2138
  }
2120
2139
  async writeFile(relPath, content) {
2121
- const abs = sandboxedPath(this.workDir, relPath);
2140
+ this.assertNotMounted(relPath);
2141
+ const abs = this.sandbox(relPath);
2122
2142
  await this.withLock(abs, async () => {
2123
2143
  await this.ensureParent(abs);
2124
2144
  await this.backend.writeFile(abs, content);
@@ -2126,7 +2146,8 @@ var WorkspaceServiceImpl = class {
2126
2146
  await this.emit({ kind: "file", relPath, content });
2127
2147
  }
2128
2148
  async removeFile(relPath) {
2129
- const abs = sandboxedPath(this.workDir, relPath);
2149
+ this.assertNotMounted(relPath);
2150
+ const abs = this.sandbox(relPath);
2130
2151
  let removed = false;
2131
2152
  await this.withLock(abs, async () => {
2132
2153
  if (!this.backend.exists(abs)) return;
@@ -2136,7 +2157,7 @@ var WorkspaceServiceImpl = class {
2136
2157
  if (removed) await this.emit({ kind: "file-removed", relPath });
2137
2158
  }
2138
2159
  async listTree(relPath) {
2139
- const base = relPath === void 0 ? this.workDir : sandboxedPath(this.workDir, relPath);
2160
+ const base = relPath === void 0 ? this.workDir : this.sandbox(relPath);
2140
2161
  if (!this.backend.exists(base)) return [];
2141
2162
  const out = [];
2142
2163
  await walk(this.backend, base, this.workDir, out);
@@ -2145,12 +2166,39 @@ var WorkspaceServiceImpl = class {
2145
2166
  async exists(relPath) {
2146
2167
  let abs;
2147
2168
  try {
2148
- abs = sandboxedPath(this.workDir, relPath);
2169
+ abs = this.sandbox(relPath);
2149
2170
  } catch {
2150
2171
  return false;
2151
2172
  }
2152
2173
  return this.backend.exists(abs);
2153
2174
  }
2175
+ // === Read-only mounts ===
2176
+ mountReadonly(prefix, absPath) {
2177
+ if (this.mountMap.has(prefix)) {
2178
+ throw new Error(`mount: prefix "${prefix}" already registered`);
2179
+ }
2180
+ this.mountMap.set(prefix, path2.resolve(absPath));
2181
+ }
2182
+ isMounted(relPath) {
2183
+ try {
2184
+ for (const [prefix] of this.mountMap) {
2185
+ if (relPath === prefix || relPath.startsWith(prefix + "/")) return true;
2186
+ }
2187
+ return false;
2188
+ } catch {
2189
+ return false;
2190
+ }
2191
+ }
2192
+ /** Resolve a sandboxed path through mounts. */
2193
+ sandbox(relPath) {
2194
+ return sandboxedPath(this.workDir, relPath, this.mountMap.size > 0 ? this.mountMap : void 0);
2195
+ }
2196
+ /** Throw if relPath resolves to a mount (write/remove are blocked). */
2197
+ assertNotMounted(relPath) {
2198
+ if (this.isMounted(relPath)) {
2199
+ throw new Error(`sandbox: cannot write to read-only mount (${relPath})`);
2200
+ }
2201
+ }
2154
2202
  // === Git ===
2155
2203
  async commitAndPush(opts) {
2156
2204
  if (!this.git) return null;
@@ -2405,7 +2453,8 @@ async function openWorkspaceInternal(opts) {
2405
2453
  appId: resolved.appId,
2406
2454
  git,
2407
2455
  github: opts.github,
2408
- archiveBackend: opts.archiveBackend
2456
+ archiveBackend: opts.archiveBackend,
2457
+ mounts: opts.mounts
2409
2458
  });
2410
2459
  if (!bare) {
2411
2460
  await service.index.warm();