@lanes-sh/link 0.7.2 → 0.9.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 (190) hide show
  1. package/README.md +20 -10
  2. package/instructions/agents/lanes-link-scout.md +2 -2
  3. package/instructions/skills/lanes-link/SKILL.md +148 -73
  4. package/package.json +2 -1
  5. package/src/audit/index.ts +8 -1
  6. package/src/auth/index.ts +58 -2
  7. package/src/auth/lanes/assertion.ts +256 -0
  8. package/src/auth/lanes/callback.ts +135 -0
  9. package/src/auth/lanes/federation.ts +50 -0
  10. package/src/auth/lanes/login.ts +294 -0
  11. package/src/auth/lanes/members.ts +103 -0
  12. package/src/auth/lanes/session.ts +97 -0
  13. package/src/auth/oauth/grant.ts +183 -0
  14. package/src/auth/oauth/result.ts +27 -0
  15. package/src/auth/oauth/server.ts +176 -203
  16. package/src/auth/oauth/store.ts +85 -4
  17. package/src/auth/remote.ts +32 -9
  18. package/src/cli/accepts.ts +109 -0
  19. package/src/cli/argv.ts +57 -3
  20. package/src/cli/audit-change.ts +140 -0
  21. package/src/cli/callback-page.ts +36 -115
  22. package/src/cli/commands/auth-dispatch.ts +48 -0
  23. package/src/cli/commands/auth.ts +229 -0
  24. package/src/cli/commands/connect/accounts.ts +4 -4
  25. package/src/cli/commands/connect/authorise.ts +4 -4
  26. package/src/cli/commands/connect/bind-credential.ts +2 -1
  27. package/src/cli/commands/connect/custom/index.ts +1 -1
  28. package/src/cli/commands/connect/custom/write.ts +2 -2
  29. package/src/cli/commands/connect/grant.ts +29 -14
  30. package/src/cli/commands/connect/index.ts +90 -88
  31. package/src/cli/commands/connect/options.ts +83 -0
  32. package/src/cli/commands/connect/registration.ts +50 -0
  33. package/src/cli/commands/connect/requirements.ts +1 -1
  34. package/src/cli/commands/connect/settle.ts +16 -6
  35. package/src/cli/commands/connect/target-note.ts +7 -2
  36. package/src/cli/commands/connect/unknown.ts +1 -1
  37. package/src/cli/commands/connect/variables.ts +3 -2
  38. package/src/cli/commands/connection-list.ts +116 -0
  39. package/src/cli/commands/connection.ts +183 -165
  40. package/src/cli/commands/grant.ts +140 -0
  41. package/src/cli/commands/identity.ts +24 -12
  42. package/src/cli/commands/knowledge/index.ts +49 -89
  43. package/src/cli/commands/knowledge/migrate.ts +79 -13
  44. package/src/cli/commands/knowledge/show.ts +97 -0
  45. package/src/cli/commands/knowledge.ts +2 -1
  46. package/src/cli/commands/mcp/harnesses.ts +30 -8
  47. package/src/cli/commands/mcp/onboarding.ts +86 -0
  48. package/src/cli/commands/mcp/register.ts +16 -2
  49. package/src/cli/commands/mcp.ts +1 -0
  50. package/src/cli/commands/members.ts +288 -0
  51. package/src/cli/commands/operate/attach.ts +3 -3
  52. package/src/cli/commands/operate/audit.ts +11 -7
  53. package/src/cli/commands/operate/auth.ts +28 -11
  54. package/src/cli/commands/operate/findings.ts +2 -1
  55. package/src/cli/commands/operate/inspect.ts +37 -19
  56. package/src/cli/commands/operate/migrate.ts +33 -13
  57. package/src/cli/commands/operate/outputs.ts +3 -3
  58. package/src/cli/commands/operate/pair-certificate.ts +141 -0
  59. package/src/cli/commands/operate/pair.ts +324 -0
  60. package/src/cli/commands/operate/policy.ts +73 -22
  61. package/src/cli/commands/operate/serve.ts +53 -5
  62. package/src/cli/commands/operate/status.ts +18 -10
  63. package/src/cli/commands/operate/tools.ts +2 -2
  64. package/src/cli/commands/operate.ts +2 -0
  65. package/src/cli/commands/owner/assets.ts +2 -2
  66. package/src/cli/commands/owner/entities.ts +2 -2
  67. package/src/cli/commands/owner/memory.ts +2 -2
  68. package/src/cli/commands/owner/shared.ts +13 -2
  69. package/src/cli/commands/owner/skills.ts +28 -8
  70. package/src/cli/commands/owner/tasks.ts +2 -2
  71. package/src/cli/commands/owner/vault.ts +3 -3
  72. package/src/cli/commands/profile/disposition.ts +236 -0
  73. package/src/cli/commands/profile/removal.ts +154 -64
  74. package/src/cli/commands/profile/remove.ts +83 -7
  75. package/src/cli/commands/profile.ts +79 -16
  76. package/src/cli/commands/relabel.ts +112 -0
  77. package/src/cli/commands/secrets.ts +39 -17
  78. package/src/cli/commands/set-workspace.ts +96 -0
  79. package/src/cli/commands/setup.ts +2 -2
  80. package/src/cli/commands/sync.ts +8 -8
  81. package/src/cli/commands/target.ts +9 -7
  82. package/src/cli/commands/update-migration.ts +54 -0
  83. package/src/cli/commands/update.ts +78 -24
  84. package/src/cli/config-edit.ts +99 -143
  85. package/src/cli/config-migrate.ts +82 -64
  86. package/src/cli/config-repair-sweep.ts +119 -0
  87. package/src/cli/config-repair.ts +131 -125
  88. package/src/cli/config-templates.ts +200 -0
  89. package/src/cli/contract3-credentials.ts +294 -0
  90. package/src/cli/contract3-data.ts +262 -0
  91. package/src/cli/contract3-layout.ts +46 -0
  92. package/src/cli/contract3-shape.ts +212 -0
  93. package/src/cli/contract3.ts +399 -0
  94. package/src/cli/contract4-credentials.ts +207 -0
  95. package/src/cli/contract4-data.ts +399 -0
  96. package/src/cli/contract4-rename.ts +73 -0
  97. package/src/cli/contract4-yaml.ts +223 -0
  98. package/src/cli/contract4.ts +342 -0
  99. package/src/cli/endpoint-url.ts +1 -1
  100. package/src/cli/identity.ts +44 -26
  101. package/src/cli/lanes.ts +25 -1
  102. package/src/cli/main.ts +94 -14
  103. package/src/cli/migrate-move.ts +166 -0
  104. package/src/cli/migrate-plan.ts +12 -6
  105. package/src/cli/output.ts +34 -1
  106. package/src/cli/publish.ts +6 -7
  107. package/src/cli/runtime/open.ts +64 -99
  108. package/src/cli/runtime/registry.ts +6 -7
  109. package/src/cli/runtime/select.ts +2 -11
  110. package/src/cli/runtime/stores.ts +58 -0
  111. package/src/cli/runtime/types.ts +106 -0
  112. package/src/cli/runtime/vault.ts +19 -4
  113. package/src/cli/runtime/workspace.ts +60 -0
  114. package/src/cli/runtime.ts +2 -1
  115. package/src/cli/selection-require.ts +44 -13
  116. package/src/cli/selection.ts +127 -145
  117. package/src/cli/usage.ts +40 -20
  118. package/src/cli/workspace-migrate.ts +152 -22
  119. package/src/connectivity/manifest/provider.ts +34 -13
  120. package/src/connectivity/manifest/requirements.ts +1 -1
  121. package/src/connectivity/transports/imap/parser.ts +70 -9
  122. package/src/deployments/adapters/filesystem.ts +18 -3
  123. package/src/deployments/bind.ts +1 -1
  124. package/src/deployments/deploy.ts +38 -29
  125. package/src/deployments/gcp/bucket.ts +58 -11
  126. package/src/deployments/gcp/provision.ts +7 -7
  127. package/src/deployments/knowledge.ts +9 -4
  128. package/src/deployments/prepare.ts +72 -24
  129. package/src/deployments/record.ts +1 -1
  130. package/src/deployments/report.ts +2 -2
  131. package/src/deployments/serving.ts +15 -74
  132. package/src/deployments/target.ts +34 -13
  133. package/src/deployments/upload.ts +60 -27
  134. package/src/dispatch/deps.ts +88 -0
  135. package/src/dispatch/dispatch.ts +21 -62
  136. package/src/policy/index.ts +47 -15
  137. package/src/profile/connections.ts +195 -0
  138. package/src/profile/deployments.ts +86 -8
  139. package/src/profile/index.ts +35 -6
  140. package/src/profile/knowledge.ts +18 -5
  141. package/src/profile/layout.ts +163 -90
  142. package/src/profile/load.ts +133 -64
  143. package/src/profile/pairing.ts +32 -0
  144. package/src/profile/primitives.ts +35 -1
  145. package/src/profile/registry.ts +6 -6
  146. package/src/profile/schema.ts +181 -21
  147. package/src/profile/targets.ts +21 -9
  148. package/src/profile/testing.ts +104 -2
  149. package/src/profile/workspace.ts +124 -33
  150. package/src/providers/assets/provider.ts +6 -6
  151. package/src/providers/custom/index.ts +1 -1
  152. package/src/providers/custom/load.ts +2 -3
  153. package/src/providers/entities/provider.ts +6 -6
  154. package/src/providers/entities/writes.ts +1 -1
  155. package/src/providers/identity/provider.ts +2 -2
  156. package/src/providers/memory/provider.ts +26 -8
  157. package/src/providers/setup/plan.ts +1 -1
  158. package/src/providers/setup/provider.ts +3 -3
  159. package/src/providers/skills/provider.ts +2 -2
  160. package/src/providers/slack/index.ts +2 -2
  161. package/src/providers/tasks/provider.ts +6 -6
  162. package/src/providers/vault/provider.ts +1 -1
  163. package/src/registry/policy-bridge.ts +33 -11
  164. package/src/registry/reconcile.ts +4 -4
  165. package/src/registry/registry.ts +1 -1
  166. package/src/server/authorization.ts +94 -0
  167. package/src/server/edge.ts +14 -1
  168. package/src/server/endpoint.ts +89 -104
  169. package/src/server/generation.ts +10 -1
  170. package/src/server/harness.ts +71 -13
  171. package/src/server/index.ts +31 -0
  172. package/src/server/mcp/build.ts +20 -1
  173. package/src/server/mcp/client-info.ts +54 -0
  174. package/src/server/mcp/guide.ts +120 -0
  175. package/src/server/mcp/instructions.ts +22 -22
  176. package/src/server/mcp/prompts.ts +7 -3
  177. package/src/server/mcp/resources.ts +16 -8
  178. package/src/server/mcp/routing.ts +3 -3
  179. package/src/server/mcp/tools.ts +25 -6
  180. package/src/server/mcp/visibility.ts +74 -7
  181. package/src/server/oauth.ts +29 -109
  182. package/src/server/read/credential.ts +134 -0
  183. package/src/server/read/deployed.ts +56 -0
  184. package/src/server/read/listener.ts +54 -0
  185. package/src/server/read/open.ts +101 -0
  186. package/src/server/read/routes.ts +247 -0
  187. package/src/server/read/state.ts +171 -0
  188. package/src/stores/blobs/conformance.ts +19 -0
  189. package/src/stores/state/index.ts +76 -10
  190. package/src/stores/state/testing.ts +5 -1
@@ -1,22 +1,27 @@
1
1
  import { existsSync } from 'node:fs';
2
+ import { layout, legacyProfileConfig, LEGACY_WORKSPACE_FILE, PROFILE_FILE } from './layout.ts';
2
3
  import { isRemoteWorkspace, readWorkspaceFile, workspaceFiles } from './files.ts';
3
4
  import { parseConfig, type LoadedConfig } from './load.ts';
4
5
  import { dirname, isAbsolute, join, resolve } from 'node:path';
5
6
  import { homedir } from 'node:os';
6
7
  import { parse as parseYaml } from 'yaml';
7
8
  import { ConfigError } from './load.ts';
8
- import { workspaceSchema, type Config, type WorkspaceConfig } from './schema.ts';
9
+ import {
10
+ SUPPORTED_CONTRACT,
11
+ connectionsFileSchema,
12
+ workspaceSchema,
13
+ type Config,
14
+ type ConnectionsFile,
15
+ type WorkspaceConfig,
16
+ } from './schema.ts';
17
+ import { assertConnectionsUnique, assertNoRenamedProviders } from './connections.ts';
18
+ import { findSecrets, formatSecretFindings } from './secret-detection.ts';
9
19
 
10
20
  /**
11
21
  * Workspace and profile resolution.
12
22
  *
13
- * A workspace is a directory holding one or more profiles:
14
- *
15
- * lanes-link.yaml workspace settings: contract, default_profile
16
- * profiles/
17
- * personal.yaml
18
- * work.yaml
19
- * data/ local state per profile, gitignored
23
+ * A workspace holds its registry, its accounts, and one directory per profile.
24
+ * `layout.ts` has the tree.
20
25
  *
21
26
  * **A command says which profile it means, or it does not run** (ADR-037).
22
27
  * `--profile` is the only thing that selects one. `LANES_LINK_PROFILE` and
@@ -24,23 +29,23 @@ import { workspaceSchema, type Config, type WorkspaceConfig } from './schema.ts'
24
29
  *
25
30
  * The argument this replaces was that persisted selection is how operators act
26
31
  * on the wrong thing, and that a *visible* fallback — an exported variable, a
27
- * key in a file the operator reads, and a line printed before every command —
28
- * was therefore safe. The first half stands and is why this rule exists at all.
32
+ * key in a file the operator reads, a line printed before every command — was
33
+ * therefore safe. The first half stands and is why this rule exists at all.
29
34
  * What did not survive is the conclusion: the printed line is a dim grey one,
30
35
  * and a fallback made an ignored flag survivable, so `profile add --target
31
36
  * cloud` dropping its flag surfaced on the *next* command, from a different
32
- * source, detached from its cause. A resolver with nothing to fall back to
33
- * cannot do that.
37
+ * source, detached from its cause. A resolver with nothing to fall back to cannot.
34
38
  *
35
39
  * The workspace root is deliberately not part of this and keeps its chain —
36
- * `LANES_LINK_HOME`, then an ancestor holding `lanes-link.yaml`, then
37
- * `~/.lanes-link`. Getting it wrong yields "no profiles here" rather than an
40
+ * `LANES_LINK_HOME`, then an ancestor holding `workspaces.yaml` (or the
41
+ * `lanes-link.yaml` it was called before contract 4), then `~/.lanes-link`.
42
+ * Getting it wrong yields "no profiles here" rather than an
38
43
  * action against the wrong account, it is the only channel a container has for
39
44
  * its bucket (ADR-023), and the ancestor walk is what makes a per-repository
40
45
  * workspace work at all.
41
46
  */
42
47
 
43
- export const WORKSPACE_FILE = 'lanes-link.yaml';
48
+ export const WORKSPACE_FILE = 'workspaces.yaml';
44
49
 
45
50
  /** A profile, found. Everything a command needs before it has read the config. */
46
51
  export interface ProfileSelection {
@@ -95,7 +100,9 @@ export function resolveWorkspaceRoot(options: ResolveOptions = {}): string {
95
100
  // `existsSync`, not `Bun.file(path).size`: a missing file reports size 0,
96
101
  // so a `>= 0` check would call every candidate a workspace and stop at the
97
102
  // first directory it looked at.
98
- if (existsSync(join(directory, WORKSPACE_FILE))) return directory;
103
+ // Either name: a root that cannot be found cannot be migrated.
104
+ const marker = (name: string): boolean => existsSync(join(directory, name));
105
+ if (marker(WORKSPACE_FILE) || marker(LEGACY_WORKSPACE_FILE)) return directory;
99
106
  const parent = dirname(directory);
100
107
  if (parent === directory) break;
101
108
  directory = parent;
@@ -105,7 +112,7 @@ export function resolveWorkspaceRoot(options: ResolveOptions = {}): string {
105
112
  }
106
113
 
107
114
  /**
108
- * Where Lanes Link itself is installed — the directory holding `package.json`,
115
+ * Where Lanes Link itself is installed — the directory with `package.json`,
109
116
  * and with it `skills/` and `docs/`.
110
117
  *
111
118
  * Not the workspace: this is the code, not the operator's data. Found by
@@ -135,7 +142,7 @@ export function installRoot(from: string): string {
135
142
  * workspace stopped being a directory.
136
143
  */
137
144
  export function profilePath(workspaceRoot: string, profile: string): string {
138
- const key = `profiles/${profile}.yaml`;
145
+ const key = layout.profileConfig(profile);
139
146
  return isRemoteWorkspace(workspaceRoot) ? `${workspaceRoot}/${key}` : join(workspaceRoot, key);
140
147
  }
141
148
 
@@ -144,7 +151,7 @@ export async function loadProfileConfig(
144
151
  workspaceRoot: string,
145
152
  profile: string,
146
153
  ): Promise<LoadedConfig> {
147
- const key = `profiles/${profile}.yaml`;
154
+ const key = layout.profileConfig(profile);
148
155
  const text = await readWorkspaceFile(workspaceFiles(workspaceRoot), key);
149
156
  const shown = profilePath(workspaceRoot, profile);
150
157
 
@@ -152,10 +159,60 @@ export async function loadProfileConfig(
152
159
  return parseConfig(text, shown);
153
160
  }
154
161
 
162
+ /** `connections.yaml` sits beside `lanes-link.yaml`, at the workspace root. */
163
+ export const CONNECTIONS_FILE = 'connections.yaml';
164
+
165
+ /**
166
+ * Every account authorised in this workspace (ADR-057).
167
+ *
168
+ * Absent is not an error. A workspace that has never connected anything has no
169
+ * file, and returning an empty set rather than throwing is what lets `profile
170
+ * add`, `status` and `doctor` answer on a fresh install — the same reasoning
171
+ * `readWorkspace` uses for returning null.
172
+ *
173
+ * Read through the workspace store rather than the filesystem, so a deployed
174
+ * revision whose root is a bucket URL loads it (ADR-049). That was the exact
175
+ * bug manifests hit: a filesystem read against a `gs://` root silently found
176
+ * nothing and every custom provider vanished.
177
+ */
178
+ export async function readConnections(workspaceRoot: string): Promise<ConnectionsFile> {
179
+ const path = join(workspaceRoot, CONNECTIONS_FILE);
180
+ const text = await readWorkspaceFile(workspaceFiles(workspaceRoot), CONNECTIONS_FILE);
181
+ if (text === null) return { contract: SUPPORTED_CONTRACT, connections: [], oauth_apps: {} };
182
+
183
+ const raw = parseYaml(text);
184
+
185
+ const secrets = findSecrets(raw);
186
+ if (secrets.length > 0) {
187
+ throw new ConfigError(
188
+ `${path}: ${formatSecretFindings(secrets)}`,
189
+ secrets.map((finding) => finding.path),
190
+ );
191
+ }
192
+
193
+ const parsed = connectionsFileSchema.safeParse(raw);
194
+ if (!parsed.success) {
195
+ throw new ConfigError(
196
+ `${path}:\n${parsed.error.issues.map((i) => ` ${i.path.join('.')}: ${i.message}`).join('\n')}`,
197
+ );
198
+ }
199
+
200
+ assertConnectionsUnique(parsed.data.connections);
201
+ assertNoRenamedProviders(
202
+ parsed.data.connections,
203
+ 'lanes link doctor --fix --profile <name> --workspace <name>',
204
+ );
205
+ return parsed.data;
206
+ }
207
+
155
208
  export async function readWorkspace(workspaceRoot: string): Promise<WorkspaceConfig | null> {
156
- const path = join(workspaceRoot, WORKSPACE_FILE);
157
- const text = await readWorkspaceFile(workspaceFiles(workspaceRoot), WORKSPACE_FILE);
209
+ // New name first: a workspace mid-migration still has the old file, and
210
+ // preferring it would undo the rename on the next write.
211
+ const files = workspaceFiles(workspaceRoot);
212
+ const current = await readWorkspaceFile(files, WORKSPACE_FILE);
213
+ const text = current ?? (await readWorkspaceFile(files, LEGACY_WORKSPACE_FILE));
158
214
  if (text === null) return null;
215
+ const path = join(workspaceRoot, current === null ? LEGACY_WORKSPACE_FILE : WORKSPACE_FILE);
159
216
 
160
217
  const parsed = workspaceSchema.safeParse(parseYaml(text));
161
218
  if (!parsed.success) {
@@ -168,15 +225,34 @@ export async function readWorkspace(workspaceRoot: string): Promise<WorkspaceCon
168
225
 
169
226
  export async function listProfiles(workspaceRoot: string): Promise<string[]> {
170
227
  try {
171
- const entries = await workspaceFiles(workspaceRoot).list('profiles/');
172
- return entries
173
- .map((entry) => entry.key.slice('profiles/'.length))
174
- .filter((name) => name.endsWith('.yaml') && !name.endsWith('.example.yaml'))
175
- // Direct children only: a nested directory under `profiles/` is not a
176
- // profile, and a bucket listing is flat so it would otherwise look like one.
177
- .filter((name) => !name.includes('/'))
178
- .map((name) => name.slice(0, -'.yaml'.length))
179
- .sort();
228
+ const root = `${layout.profilesRoot()}/`;
229
+ const entries = await workspaceFiles(workspaceRoot).list(root);
230
+
231
+ // A profile is a *directory* holding a `profile.yaml` (ADR-067), so the
232
+ // shape matched is `<name>/profile.yaml`. The listing walks each profile's
233
+ // memory and assets on the way past, which is the price of the declaration
234
+ // sitting beside the bytes; `audit.log/` is the workspace's, so the largest
235
+ // collection is not in here.
236
+ //
237
+ // **Both shapes**, because every migration enumerates through here: listing
238
+ // only the new one leaves an unmigrated workspace holding no profiles as
239
+ // far as its own migration is concerned. Same rule as the marker file.
240
+ const names = new Set<string>();
241
+ for (const entry of entries) {
242
+ const rest = entry.key.slice(root.length);
243
+ const parts = rest.split('/');
244
+
245
+ if (parts.length === 2 && parts[1] === PROFILE_FILE) {
246
+ names.add(parts[0]!);
247
+ continue;
248
+ }
249
+
250
+ // Contract 3 and earlier. `.example.yaml` was never a profile.
251
+ if (parts.length === 1 && rest.endsWith('.yaml') && !rest.endsWith('.example.yaml')) {
252
+ names.add(rest.slice(0, -'.yaml'.length));
253
+ }
254
+ }
255
+ return [...names].sort();
180
256
  } catch {
181
257
  return [];
182
258
  }
@@ -199,8 +275,23 @@ export async function resolveSelection(options: ResolveOptions = {}): Promise<Pr
199
275
  if (!profile) throw noProfileNamed(workspaceRoot, await listProfiles(workspaceRoot), env);
200
276
 
201
277
  const path = profilePath(workspaceRoot, profile);
202
- if (!(await workspaceFiles(workspaceRoot).has(`profiles/${profile}.yaml`))) {
278
+ const files = workspaceFiles(workspaceRoot);
279
+
280
+ if (!(await files.has(layout.profileConfig(profile)))) {
203
281
  const available = await listProfiles(workspaceRoot);
282
+
283
+ // **It exists, at the path contract 3 kept it.** Without this the refusal
284
+ // reads "profile personal does not exist. Available: personal" — what a
285
+ // listing that understands both layouts and a lookup that understands one
286
+ // produce together — and names no way forward (ADR-051).
287
+ if (await files.has(legacyProfileConfig(profile))) {
288
+ throw new ConfigError(
289
+ `Profile "${profile}" is still laid out the way contract 3 kept it, and nothing reads ` +
290
+ `that any more.\n Migrate it with: lanes link doctor --fix --profile ${profile} ` +
291
+ '--workspace <name>',
292
+ );
293
+ }
294
+
204
295
  throw new ConfigError(
205
296
  `Profile "${profile}" does not exist (looked for ${path}).\n` +
206
297
  (available.length > 0 ? `Available: ${available.join(', ')}` : 'No profiles exist yet.'),
@@ -219,7 +310,7 @@ export function noProfileNamed(
219
310
  if (available.length === 0) {
220
311
  return new ConfigError(
221
312
  `--profile is required, and ${workspaceRoot} holds no profiles yet.\n` +
222
- ' Create one with: lanes link profile add <name> --target local',
313
+ ' Create one with: lanes link profile add <name> --workspace local',
223
314
  );
224
315
  }
225
316
 
@@ -230,7 +321,7 @@ export function noProfileNamed(
230
321
  'nothing else selects one.\n\n' +
231
322
  ` Profiles in ${workspaceRoot}\n` +
232
323
  available.map((name) => ` ${name}`).join('\n') +
233
- `\n\n e.g. lanes link status --profile ${available[0]} --target <target>` +
324
+ `\n\n e.g. lanes link status --profile ${available[0]} --workspace <name>` +
234
325
  (stale
235
326
  ? `\n\n LANES_LINK_PROFILE=${stale} is set in this shell and is no longer read.\n` +
236
327
  ' Unset it, or pass --profile.'
@@ -72,7 +72,7 @@ const MAX_ASSET_BYTES = 25 * 1024 * 1024;
72
72
  const MAX_TEXT_BYTES = 256 * 1024;
73
73
 
74
74
  export const assetsProvider: ProviderDefinition = defineLocalProvider({
75
- id: 'assets',
75
+ id: 'lanes_assets',
76
76
  name: 'Assets',
77
77
  version: '1.0.0',
78
78
  description:
@@ -104,12 +104,12 @@ export const assetsProvider: ProviderDefinition = defineLocalProvider({
104
104
  title: 'Stored file',
105
105
  description:
106
106
  'One stored file, addressed by its name. Text comes back as text; anything else is described rather than encoded.',
107
- uriTemplate: 'assets://file/{name}',
107
+ uriTemplate: 'lanes-assets://file/{name}',
108
108
  redact: keepKeys('uri'),
109
109
 
110
110
  async list(context) {
111
111
  return (await allAssets(context.storage)).map((asset) => ({
112
- uri: `assets://file/${encodeURIComponent(asset.name)}`,
112
+ uri: `lanes-assets://file/${encodeURIComponent(asset.name)}`,
113
113
  name: asset.name,
114
114
  }));
115
115
  },
@@ -171,7 +171,7 @@ export const assetsProvider: ProviderDefinition = defineLocalProvider({
171
171
  ...shown.flatMap((asset) => [
172
172
  {
173
173
  type: 'resource_link' as const,
174
- uri: `assets://file/${encodeURIComponent(asset.name)}`,
174
+ uri: `lanes-assets://file/${encodeURIComponent(asset.name)}`,
175
175
  name: asset.name,
176
176
  },
177
177
  { type: 'text' as const, text: describeAsset(asset) },
@@ -194,7 +194,7 @@ export const assetsProvider: ProviderDefinition = defineLocalProvider({
194
194
  name: 'get',
195
195
  title: 'Read a stored file',
196
196
  description:
197
- 'Return a text file\'s contents. A binary file is described instead — name, type, size, digest — because encoding it here is the cost this provider exists to avoid. The resource assets://file/{name} is the same content.',
197
+ 'Return a text file\'s contents. A binary file is described instead — name, type, size, digest — because encoding it here is the cost this provider exists to avoid. The resource lanes-assets://file/{name} is the same content.',
198
198
  inputSchema: z.object({ name: z.string().min(1).describe('The file name') }),
199
199
  redact: keepKeys('name'),
200
200
  async handler({ name }, context) {
@@ -271,7 +271,7 @@ export const assetsProvider: ProviderDefinition = defineLocalProvider({
271
271
  },
272
272
  {
273
273
  type: 'resource_link',
274
- uri: `assets://file/${encodeURIComponent(assetName)}`,
274
+ uri: `lanes-assets://file/${encodeURIComponent(assetName)}`,
275
275
  name: assetName,
276
276
  },
277
277
  ],
@@ -12,5 +12,5 @@
12
12
  * connectivity type; `load.ts` reads and validates them.
13
13
  */
14
14
 
15
- export { loadProfileProviders, parseManifest, type LoadedManifest } from './load.ts';
15
+ export { loadWorkspaceProviders, parseManifest, type LoadedManifest } from './load.ts';
16
16
  export { manifestTemplate } from './template.ts';
@@ -38,14 +38,13 @@ export interface LoadedManifest {
38
38
  readonly path: string;
39
39
  }
40
40
 
41
- export async function loadProfileProviders(
41
+ export async function loadWorkspaceProviders(
42
42
  workspaceRoot: string,
43
- profile: string,
44
43
  /** Injected for tests. A bucket is the case this exists to cover. */
45
44
  store?: BlobStore,
46
45
  ): Promise<LoadedManifest[]> {
47
46
  const files = store ?? workspaceFiles(workspaceRoot);
48
- const directory = layout.providers(profile);
47
+ const directory = layout.providers();
49
48
 
50
49
  let keys: string[];
51
50
  try {
@@ -58,7 +58,7 @@ const DESCRIPTION =
58
58
  'so more than one means ask rather than take the first.';
59
59
 
60
60
  export const entitiesProvider: ProviderDefinition = defineLocalProvider({
61
- id: 'entities',
61
+ id: 'lanes_entities',
62
62
  name: 'Entities',
63
63
  version: '1.0.0',
64
64
  description: DESCRIPTION,
@@ -99,14 +99,14 @@ export const entitiesProvider: ProviderDefinition = defineLocalProvider({
99
99
  name: 'entity',
100
100
  title: 'Entity',
101
101
  description: 'One declared entity, addressed by its id.',
102
- uriTemplate: 'entities://entity/{id}',
102
+ uriTemplate: 'lanes-entities://entity/{id}',
103
103
  mimeType: 'text/markdown',
104
104
  redact: keepKeys('uri'),
105
105
 
106
106
  async list(context) {
107
107
  const catalogue = await openCatalogue(context.storage);
108
108
  return catalogue.entities.map((entity) => ({
109
- uri: `entities://entity/${encodeURIComponent(entity.id)}`,
109
+ uri: `lanes-entities://entity/${encodeURIComponent(entity.id)}`,
110
110
  name: entity.name,
111
111
  }));
112
112
  },
@@ -230,7 +230,7 @@ export const entitiesProvider: ProviderDefinition = defineLocalProvider({
230
230
  { type: 'text', text: renderCandidates(matches, criteria, context.connection.key) },
231
231
  ...matches.candidates.map((one) => ({
232
232
  type: 'resource_link' as const,
233
- uri: `entities://entity/${encodeURIComponent(one.entity.id)}`,
233
+ uri: `lanes-entities://entity/${encodeURIComponent(one.entity.id)}`,
234
234
  name: one.entity.name,
235
235
  })),
236
236
  ],
@@ -244,7 +244,7 @@ export const entitiesProvider: ProviderDefinition = defineLocalProvider({
244
244
  content: [
245
245
  {
246
246
  type: 'resource_link' as const,
247
- uri: `entities://entity/${encodeURIComponent(found.id)}`,
247
+ uri: `lanes-entities://entity/${encodeURIComponent(found.id)}`,
248
248
  name: found.name,
249
249
  },
250
250
  { type: 'text', text: renderEntity(found, catalogue, file?.body ?? '') },
@@ -259,7 +259,7 @@ export const entitiesProvider: ProviderDefinition = defineLocalProvider({
259
259
  title: 'Read one entity',
260
260
  description:
261
261
  'Return one entity by id, with its relationships and everything pointing at it. The resource ' +
262
- 'entities://entity/{id} is the same content; this exists for clients that do not read resources.',
262
+ 'lanes-entities://entity/{id} is the same content; this exists for clients that do not read resources.',
263
263
  inputSchema: z.object({ id: z.string().min(1).describe('Entity id') }),
264
264
  redact: keepKeys('id'),
265
265
  async handler({ id }, context) {
@@ -120,7 +120,7 @@ export const writeCapabilities: readonly Capability[] = [
120
120
  type: 'text',
121
121
  text: `${existing === null ? 'Declared' : 'Updated'} "${id}" on ${context.connection.key}.`,
122
122
  },
123
- { type: 'resource_link', uri: `entities://entity/${id}`, name },
123
+ { type: 'resource_link', uri: `lanes-entities://entity/${id}`, name },
124
124
  ],
125
125
  };
126
126
  },
@@ -65,7 +65,7 @@ export function createIdentityProvider(options: IdentityProviderOptions): Provid
65
65
  const entries = options.entries ?? [];
66
66
 
67
67
  return defineLocalProvider({
68
- id: 'identity',
68
+ id: 'lanes_identity',
69
69
  name: 'Identity',
70
70
  version: '1.0.0',
71
71
  description:
@@ -128,7 +128,7 @@ function render(
128
128
  // Both flags, spelled out. They are required (ADR-037), so a command
129
129
  // missing either is one the owner pastes and watches refuse — and this is
130
130
  // handed to an agent, which relays it verbatim.
131
- const where = `--profile ${profile}${target ? ` --target ${target}` : ''}`;
131
+ const where = `--profile ${profile}${target ? ` --workspace ${target}` : ''}`;
132
132
  return (
133
133
  `Profile "${profile}" declares no identity.\n\n` +
134
134
  'Nothing here says what name or address to use, so do not invent one — ask. ' +
@@ -172,7 +172,7 @@ function slugify(title: string): string {
172
172
  }
173
173
 
174
174
  export const memoryProvider: ProviderDefinition = defineLocalProvider({
175
- id: 'memory',
175
+ id: 'lanes_memory',
176
176
  name: 'Memory',
177
177
  version: '1.0.0',
178
178
  description:
@@ -212,7 +212,7 @@ export const memoryProvider: ProviderDefinition = defineLocalProvider({
212
212
  name: 'entry',
213
213
  title: 'Memory entry',
214
214
  description: 'One stored memory entry, addressed by its id.',
215
- uriTemplate: 'memory://entry/{id}',
215
+ uriTemplate: 'lanes-memory://entry/{id}',
216
216
  mimeType: 'text/markdown',
217
217
  // The address is worth recording and the content is not — the same trade
218
218
  // `gmail.get_message` makes with a message id.
@@ -220,7 +220,7 @@ export const memoryProvider: ProviderDefinition = defineLocalProvider({
220
220
 
221
221
  async list(context) {
222
222
  return (await allEntries(context.storage)).map((entry) => ({
223
- uri: `memory://entry/${encodeURIComponent(entry.id)}`,
223
+ uri: `lanes-memory://entry/${encodeURIComponent(entry.id)}`,
224
224
  name: entry.title,
225
225
  }));
226
226
  },
@@ -242,7 +242,7 @@ export const memoryProvider: ProviderDefinition = defineLocalProvider({
242
242
  name: 'get',
243
243
  title: 'Read a memory entry',
244
244
  description:
245
- 'Return one entry by id. The resource memory://entry/{id} is the same content; this exists for clients that do not read resources.',
245
+ 'Return one entry by id. The resource lanes-memory://entry/{id} is the same content; this exists for clients that do not read resources.',
246
246
  inputSchema: z.object({
247
247
  id: z.string().min(1).describe('Entry id'),
248
248
  }),
@@ -272,8 +272,26 @@ export const memoryProvider: ProviderDefinition = defineLocalProvider({
272
272
  tag: z.string().optional().describe('Restrict to entries carrying this tag'),
273
273
  limit: z.number().int().min(1).max(50).optional().describe(`Maximum results (default ${DEFAULT_LIMIT})`),
274
274
  }),
275
- // Nothing kept: a memory query is as revealing as a Gmail search query,
276
- // and frequently more so — it is the owner's own material being asked for.
275
+ // Kept, and this reverses the rule the rest of this file follows.
276
+ //
277
+ // The old comment here read "a memory query is as revealing as a Gmail
278
+ // search query, and frequently more so", which is true and is an argument
279
+ // about the wrong thing. A search term is not the owner's material: it is
280
+ // what an *agent* went looking for in that material, which is precisely
281
+ // the question this log exists to answer. Withholding it leaves a record
282
+ // saying memory was searched, twice, and matched nothing, with no way to
283
+ // tell a calendar lookup from a rummage through someone's medical notes.
284
+ //
285
+ // The body stays withheld either way — `entry` and `get` keep only `uri`
286
+ // and `id`, so what was *found* is still not in here. This records the
287
+ // question, not the answer.
288
+ //
289
+ // The reason for the old rule was `audit/fanout.ts`: a workspace may ship
290
+ // copies to stdout or an OTLP collector, and those leave the machine. That
291
+ // is a real exposure and it is the operator's to weigh, which is what a
292
+ // declared sink is. It is not a reason to withhold from the durable log on
293
+ // the operator's own disk.
294
+ redact: keepKeys('query', 'tag', 'limit'),
277
295
  async handler({ query, tag, limit }, context) {
278
296
  const needle = query.toLowerCase();
279
297
  const entries = await allEntries(context.storage);
@@ -308,7 +326,7 @@ export const memoryProvider: ProviderDefinition = defineLocalProvider({
308
326
  content: matches.flatMap(({ entry, snippet }) => [
309
327
  {
310
328
  type: 'resource_link' as const,
311
- uri: `memory://entry/${encodeURIComponent(entry.id)}`,
329
+ uri: `lanes-memory://entry/${encodeURIComponent(entry.id)}`,
312
330
  name: entry.title,
313
331
  },
314
332
  {
@@ -352,7 +370,7 @@ export const memoryProvider: ProviderDefinition = defineLocalProvider({
352
370
  return {
353
371
  content: [
354
372
  { type: 'text', text: `Stored memory entry "${entryId}" on ${context.connection.key}.` },
355
- { type: 'resource_link', uri: `memory://entry/${entryId}`, name: title },
373
+ { type: 'resource_link', uri: `lanes-memory://entry/${entryId}`, name: title },
356
374
  ],
357
375
  };
358
376
  },
@@ -137,7 +137,7 @@ export function planFor(
137
137
  // missing either is one that refuses, or worse, writes a credential into a
138
138
  // store the endpoint that asked for it does not read.
139
139
  const command =
140
- `lanes link connect ${manifest.id} --profile ${context.profile} --target ${context.target}` +
140
+ `lanes link connect ${manifest.id} --profile ${context.profile} --workspace ${context.target}` +
141
141
  (needsId ? ' --id <name>' : connectionId ? ` --id ${connectionId}` : '');
142
142
 
143
143
  return {
@@ -92,7 +92,7 @@ export function createSetupProvider(options: SetupProviderOptions): ProviderDefi
92
92
  });
93
93
 
94
94
  return defineLocalProvider({
95
- id: 'setup',
95
+ id: 'lanes_setup',
96
96
  name: 'Setup',
97
97
  version: '1.0.0',
98
98
  description:
@@ -147,7 +147,7 @@ export function createSetupProvider(options: SetupProviderOptions): ProviderDefi
147
147
  'The console steps, the values needed, and the exact command that connects it. ' +
148
148
  'Use this to tell the owner what to run — do not compose the command yourself.',
149
149
  inputSchema: z.object({
150
- id: z.string().min(1).describe('Provider id, as listed by setup_overview — e.g. "notion"'),
150
+ id: z.string().min(1).describe('Provider id, as listed by lanes_setup_overview — e.g. "notion"'),
151
151
  connection: z
152
152
  .string()
153
153
  .optional()
@@ -167,7 +167,7 @@ export function createSetupProvider(options: SetupProviderOptions): ProviderDefi
167
167
  {
168
168
  type: 'text',
169
169
  text:
170
- `No provider "${id}". Call setup_overview for the ids this endpoint knows.`,
170
+ `No provider "${id}". Call lanes_setup_overview for the ids this endpoint knows.`,
171
171
  },
172
172
  ],
173
173
  isError: true,
@@ -110,7 +110,7 @@ export function createSkillsProvider(options: SkillsProviderOptions): ProviderDe
110
110
  const manage = options.store ? managementCapabilities(options.store, options.onChange) : [];
111
111
 
112
112
  return defineLocalProvider({
113
- id: 'skills',
113
+ id: 'lanes_skills',
114
114
  name: 'Skills',
115
115
  version: '1.0.0',
116
116
  description:
@@ -246,7 +246,7 @@ function managementCapabilities(
246
246
  content: [
247
247
  {
248
248
  type: 'text',
249
- text: `Stored skill "${skill.name}" on ${context.connection.key}. It is available as the prompt "skills_${skill.name}" where policy allows it.`,
249
+ text: `Stored skill "${skill.name}" on ${context.connection.key}. It is available as the prompt "lanes_skills_${skill.name}" where policy allows it.`,
250
250
  },
251
251
  ],
252
252
  };
@@ -105,12 +105,12 @@ export const slack = defineProvider({
105
105
  'Open "OAuth & Permissions" and add the scopes you need under USER TOKEN SCOPES — not Bot Token Scopes; the MCP server reads the user token. The full set this provider asks for in the browser is listed in https://lanes.sh/docs/link/slack.',
106
106
  'Choose "Install to Workspace" and approve. A Slack admin may have to approve it for you.',
107
107
  'Copy the "User OAuth Token". It starts with xoxp- — not the bot token, which starts with xoxb- and will not work here.',
108
- 'The token does not expire unless you enable token rotation on the app. If you rotate or reinstall, run: lanes link connect slack --profile personal --target local --auth pasted_token --replace.',
108
+ 'The token does not expire unless you enable token rotation on the app. If you rotate or reinstall, run: lanes link connect slack --profile personal --workspace local --auth pasted_token --replace.',
109
109
  ],
110
110
  troubleshooting:
111
111
  'Slack refused the token. The usual causes are a bot token (xoxb-) pasted where the user token (xoxp-) belongs, ' +
112
112
  'a scope missing from USER TOKEN SCOPES, or an app that was reinstalled since — reinstalling mints a new token. ' +
113
- 'Copy the User OAuth Token from https://api.slack.com/apps and re-run: lanes link connect slack --profile personal --target local --auth pasted_token --replace.',
113
+ 'Copy the User OAuth Token from https://api.slack.com/apps and re-run: lanes link connect slack --profile personal --workspace local --auth pasted_token --replace.',
114
114
  prompts: [
115
115
  {
116
116
  key: 'token',
@@ -57,7 +57,7 @@ function matches(task: Task, needle: string): boolean {
57
57
  }
58
58
 
59
59
  export const tasksProvider: ProviderDefinition = defineLocalProvider({
60
- id: 'tasks',
60
+ id: 'lanes_tasks',
61
61
  name: 'Tasks',
62
62
  version: '1.0.0',
63
63
  description:
@@ -92,13 +92,13 @@ export const tasksProvider: ProviderDefinition = defineLocalProvider({
92
92
  name: 'task',
93
93
  title: 'Task',
94
94
  description: 'One task, addressed by its id.',
95
- uriTemplate: 'tasks://task/{id}',
95
+ uriTemplate: 'lanes-tasks://task/{id}',
96
96
  mimeType: 'text/markdown',
97
97
  redact: keepKeys('uri'),
98
98
 
99
99
  async list(context) {
100
100
  return (await allTasks(context.storage)).map((task) => ({
101
- uri: `tasks://task/${encodeURIComponent(task.id)}`,
101
+ uri: `lanes-tasks://task/${encodeURIComponent(task.id)}`,
102
102
  name: task.title,
103
103
  }));
104
104
  },
@@ -170,7 +170,7 @@ export const tasksProvider: ProviderDefinition = defineLocalProvider({
170
170
  ...shown.flatMap((task) => [
171
171
  {
172
172
  type: 'resource_link' as const,
173
- uri: `tasks://task/${encodeURIComponent(task.id)}`,
173
+ uri: `lanes-tasks://task/${encodeURIComponent(task.id)}`,
174
174
  name: task.title,
175
175
  },
176
176
  { type: 'text' as const, text: `${task.id} ${line(task)}` },
@@ -193,7 +193,7 @@ export const tasksProvider: ProviderDefinition = defineLocalProvider({
193
193
  name: 'get',
194
194
  title: 'Read a task',
195
195
  description:
196
- 'Return one task by id, notes included. The resource tasks://task/{id} is the same content; this exists for clients that do not read resources.',
196
+ 'Return one task by id, notes included. The resource lanes-tasks://task/{id} is the same content; this exists for clients that do not read resources.',
197
197
  inputSchema: z.object({ id: z.string().min(1).describe('Task id') }),
198
198
  redact: keepKeys('id'),
199
199
  async handler({ id }, context) {
@@ -261,7 +261,7 @@ export const tasksProvider: ProviderDefinition = defineLocalProvider({
261
261
  type: 'text',
262
262
  text: `${existing ? 'Replaced' : 'Added'} task "${taskId}" on ${context.connection.key}.`,
263
263
  },
264
- { type: 'resource_link', uri: `tasks://task/${taskId}`, name: title },
264
+ { type: 'resource_link', uri: `lanes-tasks://task/${taskId}`, name: title },
265
265
  ],
266
266
  };
267
267
  },
@@ -92,7 +92,7 @@ export function createVaultProvider(options: VaultProviderOptions): ProviderDefi
92
92
  }));
93
93
 
94
94
  return defineLocalProvider({
95
- id: 'vault',
95
+ id: 'lanes_vault',
96
96
  name: 'Vault',
97
97
  version: '1.0.0',
98
98
  description: