@lanes-sh/link 0.8.0 → 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 (87) hide show
  1. package/instructions/skills/lanes-link/SKILL.md +12 -12
  2. package/package.json +1 -1
  3. package/src/auth/oauth/store.ts +21 -5
  4. package/src/cli/accepts.ts +6 -5
  5. package/src/cli/commands/connect/index.ts +2 -1
  6. package/src/cli/commands/connect/settle.ts +12 -4
  7. package/src/cli/commands/connection.ts +2 -1
  8. package/src/cli/commands/identity.ts +4 -4
  9. package/src/cli/commands/knowledge/index.ts +9 -16
  10. package/src/cli/commands/knowledge/migrate.ts +9 -4
  11. package/src/cli/commands/knowledge/show.ts +14 -9
  12. package/src/cli/commands/operate/migrate.ts +5 -2
  13. package/src/cli/commands/operate/pair.ts +1 -1
  14. package/src/cli/commands/operate/serve.ts +1 -1
  15. package/src/cli/commands/owner/assets.ts +2 -2
  16. package/src/cli/commands/owner/entities.ts +2 -2
  17. package/src/cli/commands/owner/memory.ts +2 -2
  18. package/src/cli/commands/owner/tasks.ts +2 -2
  19. package/src/cli/commands/owner/vault.ts +3 -3
  20. package/src/cli/commands/profile/disposition.ts +236 -0
  21. package/src/cli/commands/profile/removal.ts +100 -13
  22. package/src/cli/commands/profile/remove.ts +67 -6
  23. package/src/cli/commands/profile.ts +33 -6
  24. package/src/cli/commands/secrets.ts +4 -4
  25. package/src/cli/commands/update-migration.ts +54 -0
  26. package/src/cli/commands/update.ts +36 -23
  27. package/src/cli/config-edit.ts +29 -8
  28. package/src/cli/config-repair-sweep.ts +119 -0
  29. package/src/cli/config-repair.ts +90 -141
  30. package/src/cli/config-templates.ts +26 -24
  31. package/src/cli/contract3-credentials.ts +294 -0
  32. package/src/cli/contract3-data.ts +143 -209
  33. package/src/cli/contract3-layout.ts +46 -0
  34. package/src/cli/contract3-shape.ts +34 -8
  35. package/src/cli/contract3.ts +141 -24
  36. package/src/cli/contract4-credentials.ts +207 -0
  37. package/src/cli/contract4-data.ts +399 -0
  38. package/src/cli/contract4-rename.ts +73 -0
  39. package/src/cli/contract4-yaml.ts +223 -0
  40. package/src/cli/contract4.ts +342 -0
  41. package/src/cli/identity.ts +44 -26
  42. package/src/cli/main.ts +6 -1
  43. package/src/cli/migrate-move.ts +166 -0
  44. package/src/cli/migrate-plan.ts +3 -3
  45. package/src/cli/publish.ts +1 -5
  46. package/src/cli/runtime/open.ts +5 -5
  47. package/src/cli/runtime/select.ts +2 -11
  48. package/src/cli/runtime/stores.ts +16 -11
  49. package/src/cli/runtime/vault.ts +2 -2
  50. package/src/cli/usage.ts +5 -1
  51. package/src/cli/workspace-migrate.ts +32 -11
  52. package/src/connectivity/manifest/provider.ts +31 -12
  53. package/src/connectivity/transports/imap/parser.ts +70 -9
  54. package/src/deployments/adapters/filesystem.ts +18 -3
  55. package/src/deployments/deploy.ts +5 -5
  56. package/src/deployments/gcp/bucket.ts +42 -6
  57. package/src/deployments/knowledge.ts +9 -4
  58. package/src/deployments/target.ts +28 -7
  59. package/src/deployments/upload.ts +39 -30
  60. package/src/profile/connections.ts +13 -1
  61. package/src/profile/deployments.ts +86 -8
  62. package/src/profile/index.ts +5 -1
  63. package/src/profile/knowledge.ts +18 -5
  64. package/src/profile/layout.ts +147 -71
  65. package/src/profile/load.ts +53 -17
  66. package/src/profile/schema.ts +11 -2
  67. package/src/profile/testing.ts +45 -10
  68. package/src/profile/workspace.ts +66 -30
  69. package/src/providers/assets/provider.ts +6 -6
  70. package/src/providers/entities/provider.ts +6 -6
  71. package/src/providers/entities/writes.ts +1 -1
  72. package/src/providers/identity/provider.ts +1 -1
  73. package/src/providers/memory/provider.ts +6 -6
  74. package/src/providers/setup/provider.ts +3 -3
  75. package/src/providers/skills/provider.ts +2 -2
  76. package/src/providers/tasks/provider.ts +6 -6
  77. package/src/providers/vault/provider.ts +1 -1
  78. package/src/registry/registry.ts +1 -1
  79. package/src/server/endpoint.ts +4 -0
  80. package/src/server/harness.ts +1 -1
  81. package/src/server/mcp/instructions.ts +21 -21
  82. package/src/server/mcp/routing.ts +3 -3
  83. package/src/server/mcp/tools.ts +16 -3
  84. package/src/server/mcp/visibility.ts +56 -4
  85. package/src/stores/blobs/conformance.ts +19 -0
  86. package/src/stores/state/index.ts +76 -10
  87. package/src/stores/state/testing.ts +5 -1
@@ -107,7 +107,7 @@ when Y", that is a skill they should write, not a memory entry describing it.
107
107
 
108
108
  ## Reach for memory before answering from nothing
109
109
 
110
- `memory.search` before concluding you do not know something about this person or
110
+ `lanes_memory.search` before concluding you do not know something about this person or
111
111
  their work. It is a substring search over their own notes, not a ranked index —
112
112
  try more than one wording before deciding it is not there.
113
113
 
@@ -118,7 +118,7 @@ entries with `lanes link memory list --profile <name> --workspace <name>` and a
118
118
 
119
119
  ## Tasks have a status, so finish them rather than deleting them
120
120
 
121
- `tasks.list` answers what is outstanding. It shows `in_progress`, `open` and
121
+ `lanes_tasks.list` answers what is outstanding. It shows `in_progress`, `open` and
122
122
  `blocked` and hides the rest, so a listing is what is left to do rather than
123
123
  everything that ever was — name a status to see more.
124
124
 
@@ -133,7 +133,7 @@ Six of them, and the two that are easy to confuse are worth learning:
133
133
  | `done` | finished |
134
134
  | `dropped` | decided against, which is not the same as finished |
135
135
 
136
- **Closing a task is `tasks.update` with a status, never `tasks.remove`.** The
136
+ **Closing a task is `lanes_tasks.update` with a status, never `lanes_tasks.remove`.** The
137
137
  record of having done it is the useful half, and it is what stops the same thing
138
138
  being suggested again next week. Remove is for something recorded by mistake.
139
139
 
@@ -180,7 +180,7 @@ value has been printed by accident, say so.
180
180
  ## Who you are writing as is declared, not inferred
181
181
 
182
182
  A profile may declare the names, addresses and handles its owner wants used when
183
- something is written as them. Call `identity_list` for the profile in play
183
+ something is written as them. Call `lanes_identity_list` for the profile in play
184
184
  before signing a message, choosing an address to send from, or attributing work
185
185
  to a handle — **do not** read a name off the conversation, off a previous
186
186
  message's signature, or off the account label on a connection. That label is the
@@ -200,7 +200,7 @@ Nothing you can call writes here, deliberately.
200
200
 
201
201
  ## Who you are writing *to* is declared as well
202
202
 
203
- `entities_find` holds the people, companies and projects this owner deals with,
203
+ `lanes_entities_find` holds the people, companies and projects this owner deals with,
204
204
  with the addresses and handles to reach each of them. Call it before using
205
205
  anyone's address — do not recall one from earlier in the conversation, and do
206
206
  not lift one off a message you happen to have read.
@@ -218,9 +218,9 @@ Where an entity holds two of a kind — a work address and a personal one — th
218
218
  first is the default and the notes say when to prefer the other, exactly as
219
219
  identity works.
220
220
 
221
- Writing is a separate grant. Where you have it, `entities_write` declares one
222
- and `entities_link` relates two; a field you do not send is left as it is.
223
- `entities_forget` does not clean up edges pointing at what it removed, and says
221
+ Writing is a separate grant. Where you have it, `lanes_entities_write` declares one
222
+ and `lanes_entities_link` relates two; a field you do not send is left as it is.
223
+ `lanes_entities_forget` does not clean up edges pointing at what it removed, and says
224
224
  which ones will dangle.
225
225
 
226
226
  ## Attachments are named, not carried
@@ -279,9 +279,9 @@ shows what was attempted, refusals included.
279
279
 
280
280
  ## Setting something up
281
281
 
282
- Call `setup_overview` before answering any question about what you can reach, and
282
+ Call `lanes_setup_overview` before answering any question about what you can reach, and
283
283
  before suggesting that anything be connected. It names the accounts reachable in a
284
- profile and the providers that are not connected yet. `setup_provider` then gives
284
+ profile and the providers that are not connected yet. `lanes_setup_provider` then gives
285
285
  one provider's console steps, the values it will ask for, and the exact command.
286
286
 
287
287
  **Memory, tasks, assets, skills and the vault need no setup at all.** They hold
@@ -306,7 +306,7 @@ lanes link policy allow gmail.users.messages.list --connection gmail.<id> --prof
306
306
  profiles that grant it, which is the answer when an owner says an account they
307
307
  connected is not showing up somewhere.
308
308
 
309
- **Take the command from `setup_provider`; never compose one yourself.** It carries
309
+ **Take the command from `lanes_setup_provider`; never compose one yourself.** It carries
310
310
  the right profile and, where the provider stores a credential per account, the
311
311
  `--id` it needs. A command you assembled is one the owner pastes and watches fail.
312
312
 
@@ -326,7 +326,7 @@ it needs, print the scopes and let the owner add the flag. Deciding that is thei
326
326
 
327
327
  **A new connection is served at once; the tools you were handed are not.**
328
328
  Connecting publishes the config to wherever that workspace's endpoint reads it and
329
- asks the endpoint to re-read it, so a `setup_overview` straight after connecting
329
+ asks the endpoint to re-read it, so a `lanes_setup_overview` straight after connecting
330
330
  *does* show the account. What has not changed is the set of tools this session
331
331
  was given when it connected — the endpoint does not announce that its tools
332
332
  changed, so a capability for a freshly connected account is not callable until
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lanes-sh/link",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "A self-hostable MCP gateway for all your connections, memory, tasks, files, and secrets",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://lanes.sh/link",
@@ -1,4 +1,4 @@
1
- import type { KeyValueStore } from '#stores/state';
1
+ import { OAUTH_NAMESPACE, type KeyValueStore } from '#stores/state';
2
2
 
3
3
  /**
4
4
  * What an authorization server has to remember.
@@ -20,10 +20,26 @@ import type { KeyValueStore } from '#stores/state';
20
20
  * living about a minute.
21
21
  */
22
22
 
23
- const CLIENTS = 'oauth/clients';
24
- const CODES = 'oauth/codes';
25
- const TOKENS = 'oauth/tokens';
26
- const PENDING = 'oauth/pending';
23
+ // Derived from `OAUTH_NAMESPACE`, not spelled. These were `oauth/clients` and
24
+ // the rest while the namespace constant said `oauth.v1`, so the contract-4
25
+ // migration moved every record to a key nothing read — every signed-in client
26
+ // logged out, which is the outcome the migration exists to prevent. Worse, an
27
+ // undotted namespace is not a workspace one, so `isWorkspaceNamespace` sent
28
+ // live records into one profile's store: `profile remove <primary>` would take
29
+ // the endpoint's registrations with it.
30
+ const CLIENTS = `${OAUTH_NAMESPACE}/clients`;
31
+ const CODES = `${OAUTH_NAMESPACE}/codes`;
32
+ const TOKENS = `${OAUTH_NAMESPACE}/tokens`;
33
+ const PENDING = `${OAUTH_NAMESPACE}/pending`;
34
+
35
+ /**
36
+ * The four, for the test that holds them to `isWorkspaceNamespace`.
37
+ *
38
+ * Exported rather than retyped there: the pairing between what this module
39
+ * writes and where state routing sends it is the thing that broke, so the test
40
+ * reads the names off the module that owns them.
41
+ */
42
+ export const OAUTH_SUBSPACES: readonly string[] = [CLIENTS, CODES, TOKENS, PENDING];
27
43
 
28
44
  /** Far above any real number of connectors, and far below a problem. */
29
45
  const MAX_CLIENTS = 200;
@@ -39,11 +39,12 @@ export const ACCEPTS: Record<string, readonly string[]> = {
39
39
  ],
40
40
  setup: ['id'],
41
41
  'profile add': ['workspace', 'non-interactive'],
42
- // `--target` decommissions one target's stores and leaves the profile file in
43
- // place (`removal.ts`). It is documented in `usage.ts` and read by
44
- // `removalPlan`, and was refused here the flag existed everywhere except in
45
- // the list that decides whether it may be typed.
46
- 'profile remove': ['dry-run', 'yes', 'workspace'],
42
+ // `--delete-data` and `--migrate-to` say what becomes of the bytes the
43
+ // profile owns (ADR-066). Neither is optional in effect: `--yes` skips
44
+ // confirmations of things the command was told to do, and this is a question
45
+ // it has not been asked, so a run with no terminal is refused rather than
46
+ // guessing.
47
+ 'profile remove': ['dry-run', 'yes', 'workspace', 'delete-data', 'migrate-to'],
47
48
  'profile members': ['me', 'role'],
48
49
  disconnect: ['yes', 'keep-credential'],
49
50
  // The one repair `doctor` can apply rather than only name. Narrow on purpose:
@@ -35,6 +35,7 @@ import { settleIdentity } from './settle.ts';
35
35
  import { runStrategySetup } from './strategy.ts';
36
36
  import { announceConnectTarget } from './target-note.ts';
37
37
  import { unknownProvider } from './unknown.ts';
38
+ import { DISCOVERY_NAMESPACE } from '#stores/state';
38
39
 
39
40
  const PROVISIONAL_ID = 'pending';
40
41
 
@@ -265,7 +266,7 @@ export async function runConnect(
265
266
  credentials: runtime.credentials,
266
267
  connectorFor: address.connectorFor,
267
268
  remember: async (found) => {
268
- await runtime.state.kv.set('discovery', providerId, JSON.stringify(found));
269
+ await runtime.state.kv.set(DISCOVERY_NAMESPACE, providerId, JSON.stringify(found));
269
270
  registry.setDiscovered(providerId, found);
270
271
  },
271
272
  });
@@ -3,7 +3,7 @@ import { bearerTokenAsStored } from '#connectivity/auth/index.ts';
3
3
  import type { SecretStore } from '#secrets';
4
4
  import type { ConnectionConfig, Config } from '#profile';
5
5
  import type { AnyConnector, ProviderManifest } from '#connectivity';
6
- import { idFromAccount, resolveAccount } from '../../identity.ts';
6
+ import { nextConnectionId, resolveAccount } from '../../identity.ts';
7
7
  import { style } from '../../output.ts';
8
8
  import { PromptCancelled, terminalPrompter, type Prompter } from '../../prompt.ts';
9
9
  import { accountSiblings } from './accounts.ts';
@@ -151,15 +151,23 @@ export async function settleIdentity(input: {
151
151
  typed = true;
152
152
  }
153
153
 
154
- const taken = siblings.map((candidate) => candidate.id);
154
+ // Every id in the workspace, not this provider's alone. An id is opaque now,
155
+ // so a reader has nothing but the number to go on and two rows sharing one
156
+ // across providers would be a needless second thing to hold in mind — and
157
+ // `con3` naming exactly one row is what makes it usable in a refusal.
158
+ const taken = runtime.workspaceConnections.map((candidate) => candidate.id);
155
159
 
160
+ // The reconnect match is on the *account*, which is what tells a repair from
161
+ // a second row. That is the whole reason `resolveAccount` runs before this:
162
+ // without it a failed attempt appends rather than repairs, which is how
163
+ // `Gmail main3` came to exist.
156
164
  const connectionId =
157
165
  explicitId ??
158
166
  (unaccounted
159
- ? idFromAccount('main', taken)
167
+ ? nextConnectionId(taken, true)
160
168
  : (siblings.find(
161
169
  (candidate) => candidate.account.toLowerCase() === account!.toLowerCase(),
162
- )?.id ?? idFromAccount(account, taken)));
170
+ )?.id ?? nextConnectionId(taken, false)));
163
171
 
164
172
  return {
165
173
  connectionId,
@@ -7,6 +7,7 @@ import {
7
7
  readConnections,
8
8
  type ConnectionConfig,
9
9
  type Resolution,
10
+ layout,
10
11
  } from '#profile';
11
12
  import { ConfigDocument } from '../config-edit.ts';
12
13
  import { announce, announceWorkspace, emit, ok, print, style, warn } from '../output.ts';
@@ -230,7 +231,7 @@ export async function removeConnection(
230
231
  // refused at load, while a connection nothing grants is merely unused. So if
231
232
  // the process dies between the two writes, the workspace still opens.
232
233
  for (const profile of affected) {
233
- const document = await ConfigDocument.openKey(root, `profiles/${profile}.yaml`);
234
+ const document = await ConfigDocument.openKey(root, layout.profileConfig(profile));
234
235
  if (dropGrantRow(document, key)) await document.save();
235
236
  }
236
237
 
@@ -37,9 +37,9 @@ export interface IdentityListing {
37
37
  readonly reachable: boolean;
38
38
  }
39
39
 
40
- /** Whether a rule list puts `identity.*` in force, in either spelling. */
40
+ /** Whether a rule list puts `lanes_identity.*` in force, in either spelling. */
41
41
  function covers(rules: ReadonlyArray<{ capability: string }>): boolean {
42
- return rules.some((rule) => rule.capability === '*' || rule.capability === 'identity.*');
42
+ return rules.some((rule) => rule.capability === '*' || rule.capability === 'lanes_identity.*');
43
43
  }
44
44
 
45
45
  /**
@@ -61,7 +61,7 @@ function readable(config: {
61
61
  // (ADR-058). The old shape could say "declared but not granted" and
62
62
  // "granted but not declared", and both served nothing while reading like
63
63
  // configuration that worked; neither is expressible here.
64
- const grant = config.grants.find((row) => row.connection.startsWith('identity.'));
64
+ const grant = config.grants.find((row) => row.connection.startsWith('lanes_identity.'));
65
65
  if (grant === undefined) return false;
66
66
 
67
67
  return covers(grant.allow) && !covers(grant.deny);
@@ -244,7 +244,7 @@ export async function identityList(options: { json?: boolean } & GlobalFlags): P
244
244
  print(warn('declared, but no agent can read it'));
245
245
  print(
246
246
  style.dim(
247
- ' the identity surface needs a connection row and an identity.* allow rule;',
247
+ ' the identity surface needs a connection row and a lanes_identity.* allow rule;',
248
248
  ),
249
249
  );
250
250
  print(style.dim(' adding an entry with this command writes both.'));
@@ -115,11 +115,7 @@ async function useGithub(flags: KnowledgeFlags): Promise<void> {
115
115
 
116
116
  // Nothing is written until this returns: the token is asked for, the
117
117
  // repository is probed, and either can refuse while the profile is intact.
118
- const secrets = await openSecretStoreFor(
119
- runtime.config,
120
- runtime.resolution.workspaceRoot,
121
- runtime.target,
122
- );
118
+ const secrets = await openSecretStoreFor(runtime.resolution.workspaceRoot, runtime.target);
123
119
  const token = await resolveToken(secrets, knowledge.token_ref, knowledge.repo, selection, {
124
120
  replace: flags.replace,
125
121
  });
@@ -205,11 +201,7 @@ async function useLocal(flags: KnowledgeFlags): Promise<void> {
205
201
 
206
202
  // Built here rather than taken from `runtime.knowledge`, so both directions
207
203
  // of this command reach the repository through one constructor.
208
- const secrets = await openSecretStoreFor(
209
- runtime.config,
210
- runtime.resolution.workspaceRoot,
211
- runtime.target,
212
- );
204
+ const secrets = await openSecretStoreFor(runtime.resolution.workspaceRoot, runtime.target);
213
205
  const token = await resolveToken(secrets, knowledge.token_ref, knowledge.repo, selection);
214
206
  const repository = repositoryFor(knowledge, token, flags.fetch);
215
207
 
@@ -270,12 +262,13 @@ async function openLocalStores(
270
262
  },
271
263
  runtime.credentials,
272
264
  );
273
- // The *granted* connection, not the profile name. `layout.skills` changed
274
- // meaning without changing arity at ADR-059, so the compiler was silent.
275
- const skillsConnection = soleGrantFor(runtime.config, 'skills');
265
+ // The *granted* connection, beside the profile. `layout.skills` changed
266
+ // meaning without changing arity at ADR-059, so the compiler was silent;
267
+ // ADR-066 put the profile back in front of it, and it is an argument now.
268
+ const skillsConnection = soleGrantFor(runtime.config, 'lanes_skills');
276
269
  return {
277
270
  storage: factory(),
278
- skills: skillsConnection === undefined ? null : factory(layout.skills(skillsConnection)),
271
+ skills: skillsConnection === undefined ? null : factory(layout.skills(runtime.config.instance.profile, skillsConnection)),
279
272
  };
280
273
  }
281
274
 
@@ -349,7 +342,7 @@ function grantedInstances(config: Parameters<typeof soleGrantFor>[0]): {
349
342
  entities: string;
350
343
  } {
351
344
  return {
352
- memory: soleGrantFor(config, 'memory') ?? 'main',
353
- entities: soleGrantFor(config, 'entities') ?? 'main',
345
+ memory: soleGrantFor(config, 'lanes_memory') ?? 'main',
346
+ entities: soleGrantFor(config, 'lanes_entities') ?? 'main',
354
347
  };
355
348
  }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ConfigError,
3
3
  KNOWLEDGE_LAYOUT,
4
+ KNOWLEDGE_PREFIX,
4
5
  knowledgeRoot,
5
6
  type KnowledgeArea,
6
7
  type KnowledgeConfig,
@@ -81,10 +82,14 @@ function localAreas(
81
82
  // personal's repository and then deleted them locally, leaving `work`
82
83
  // reading an empty store with no knowledge block of its own. `skills` was
83
84
  // already connection-scoped, which is why it alone was correct.
85
+ // `scope` reads the *local* store and `prefix` names the repository
86
+ // directory, and since contract 4 those are different strings:
87
+ // `lanes_memory/lan1/` on disk becomes `memory/lan1/` in the repository.
88
+ // Reading one where the other belongs finds nothing and reports success.
84
89
  memory: {
85
90
  store: storage,
86
- scope: `${KNOWLEDGE_LAYOUT.memory}/${instances.memory}/`,
87
- prefix: `${KNOWLEDGE_LAYOUT.memory}/`,
91
+ scope: `${KNOWLEDGE_PREFIX.memory}/${instances.memory}/`,
92
+ prefix: `${KNOWLEDGE_PREFIX.memory}/`,
88
93
  },
89
94
  // Null becomes an empty area rather than a refusal: a profile granting no
90
95
  // skills connection has none to move, and `knowledge use` should still move
@@ -93,8 +98,8 @@ function localAreas(
93
98
  skills: { store: skills ?? EMPTY_AREA, scope: '', prefix: '' },
94
99
  entities: {
95
100
  store: storage,
96
- scope: `${KNOWLEDGE_LAYOUT.entities}/${instances.entities}/`,
97
- prefix: `${KNOWLEDGE_LAYOUT.entities}/`,
101
+ scope: `${KNOWLEDGE_PREFIX.entities}/${instances.entities}/`,
102
+ prefix: `${KNOWLEDGE_PREFIX.entities}/`,
98
103
  },
99
104
  };
100
105
  }
@@ -1,4 +1,4 @@
1
- import { KNOWLEDGE_LAYOUT, layout, soleGrantFor } from '#profile';
1
+ import { KNOWLEDGE_LAYOUT, KNOWLEDGE_PREFIX, layout, soleGrantFor } from '#profile';
2
2
  import { announce, emit, heading, print, style, table } from '../../output.ts';
3
3
  import { openRuntime } from '../../runtime.ts';
4
4
  import type { KnowledgeFlags } from './index.ts';
@@ -20,12 +20,16 @@ export async function knowledgeShow(flags: KnowledgeFlags): Promise<void> {
20
20
  const profile = runtime.config.instance.profile;
21
21
  const selection = ` --profile ${runtime.resolution.profile} --target ${runtime.target}`;
22
22
  const skills = runtime.skills ? (await runtime.skills.list()).length : 0;
23
- const memory = (await runtime.storage.list(`${KNOWLEDGE_LAYOUT.memory}/`)).length;
24
- const entities = (await runtime.storage.list(`${KNOWLEDGE_LAYOUT.entities}/`)).length;
23
+ // Counted through the *provider's* prefix, which is what the local store is
24
+ // scoped into; `KNOWLEDGE_LAYOUT` is the repository's directory name and
25
+ // since contract 4 the two are different strings. Counting by the wrong one
26
+ // reports zero for a profile whose memory is perfectly well populated.
27
+ const memory = (await runtime.storage.list(`${KNOWLEDGE_PREFIX.memory}/`)).length;
28
+ const entities = (await runtime.storage.list(`${KNOWLEDGE_PREFIX.entities}/`)).length;
25
29
  const where = runtime.knowledge?.describe;
26
30
  // Null when no skills connection is granted (ADR-059). The row still prints,
27
31
  // saying so: a missing row reads as "there are none", not "not granted".
28
- const skillsConnection = soleGrantFor(runtime.config, 'skills');
32
+ const skillsConnection = soleGrantFor(runtime.config, 'lanes_skills');
29
33
 
30
34
  if (flags.json) {
31
35
  print(
@@ -41,11 +45,12 @@ export async function knowledgeShow(flags: KnowledgeFlags): Promise<void> {
41
45
  heading('Knowledge');
42
46
  table([
43
47
  // The memory *directory*, not the blob root it sits in. `layout.blobs`
44
- // is `data/`, which is where every provider's namespace lives —
45
- // printing it here would name a directory that is mostly not memory.
48
+ // is the profile's own directory, which is where every provider's
49
+ // namespace lives — printing it here would name a directory that is
50
+ // mostly not memory.
46
51
  [
47
52
  ' memory',
48
- where ? `${where}/${KNOWLEDGE_LAYOUT.memory}` : `${layout.blobs()}/${KNOWLEDGE_LAYOUT.memory}`,
53
+ where ? `${where}/${KNOWLEDGE_LAYOUT.memory}` : `${layout.blobs(profile)}/${KNOWLEDGE_PREFIX.memory}`,
49
54
  style.dim(`${memory} file${memory === 1 ? '' : 's'}`),
50
55
  ],
51
56
  [
@@ -53,7 +58,7 @@ export async function knowledgeShow(flags: KnowledgeFlags): Promise<void> {
53
58
  where
54
59
  ? `${where}/${KNOWLEDGE_LAYOUT.skills}`
55
60
  : skillsConnection
56
- ? layout.skills(skillsConnection)
61
+ ? layout.skills(profile, skillsConnection)
57
62
  : style.dim('not granted'),
58
63
  style.dim(`${skills} file${skills === 1 ? '' : 's'}`),
59
64
  ],
@@ -64,7 +69,7 @@ export async function knowledgeShow(flags: KnowledgeFlags): Promise<void> {
64
69
  ' entities',
65
70
  where
66
71
  ? `${where}/${KNOWLEDGE_LAYOUT.entities}`
67
- : `${layout.blobs()}/${KNOWLEDGE_LAYOUT.entities}`,
72
+ : `${layout.blobs(profile)}/${KNOWLEDGE_PREFIX.entities}`,
68
73
  style.dim(`${entities} file${entities === 1 ? '' : 's'}`),
69
74
  ],
70
75
  ]);
@@ -71,7 +71,7 @@ export async function migratedRenamedProviders(
71
71
  // Throws when it is malformed beyond a rename, which is a better sentence
72
72
  // than the referential one it would otherwise be reported under.
73
73
  const config = shapeOf(await ConfigDocument.open(root, selection.profile));
74
- const credentials = await openSecretStoreFor(config, root, target);
74
+ const credentials = await openSecretStoreFor(root, target);
75
75
 
76
76
  const migration = await migrateRenamedProviders(document, profiles, credentials, {
77
77
  apply: flags.fix === true,
@@ -158,7 +158,10 @@ export async function migratedContract(flags: RenameFlags, refusal: unknown): Pr
158
158
  // only about contract 1 — so a contract-2 bucket, the exact thing this
159
159
  // function exists to rescue, returned false here and fell through to the
160
160
  // refusal it was supposed to fix. With `--fix` absent this writes nothing.
161
- const migration = await migrateToCurrentContract(where, { apply: flags.fix === true });
161
+ const migration = await migrateToCurrentContract(where, {
162
+ apply: flags.fix === true,
163
+ target,
164
+ });
162
165
  if (migration.alreadyCurrent) return false;
163
166
 
164
167
  const applied = flags.fix === true;
@@ -121,7 +121,7 @@ export async function pair(flags: PairFlags, deps: PairDeps = {}): Promise<void>
121
121
 
122
122
  const chosen = named ?? profiles[0]!;
123
123
  const host = chosen.config.instance.host;
124
- const credentials = await openSecretStoreFor(chosen.config, root, target);
124
+ const credentials = await openSecretStoreFor(root, target);
125
125
 
126
126
  // A workspace that declares a deployment is paired over the address the
127
127
  // platform gave it, not over loopback — which is what `declared.deploy`
@@ -2,7 +2,7 @@ import { readSession } from '#auth/lanes/session.ts';
2
2
  import { ConfigError } from '#profile';
3
3
  import { startEndpoint } from '#server/endpoint.ts';
4
4
  import { streamLogger } from '#server/logging.ts';
5
- import { repairOwnerLayer } from '../../config-repair.ts';
5
+ import { repairOwnerLayer } from '../../config-repair-sweep.ts';
6
6
  import { announce, ok, print, style, warn } from '../../output.ts';
7
7
  import { staleNudge } from '../../release.ts';
8
8
  import { primaryProfile, resolveProfile, type GlobalFlags } from '../../runtime.ts';
@@ -127,6 +127,6 @@ export async function assetsRemove(name: string | undefined, flags: OwnerFlags):
127
127
  * it is built from the same two functions rather than spelled as a path.
128
128
  */
129
129
  export function assetsStore(runtime: Runtime, flags: OwnerFlags): BlobStore {
130
- const connection = ownerConnection(runtime.config, 'assets', flags);
131
- return scopeBlobStore(runtime.storage, scopeNamespace('assets', connection));
130
+ const connection = ownerConnection(runtime.config, 'lanes_assets', flags);
131
+ return scopeBlobStore(runtime.storage, scopeNamespace('lanes_assets', connection));
132
132
  }
@@ -239,6 +239,6 @@ export async function entitiesReindex(flags: OwnerFlags): Promise<void> {
239
239
  * `memoryStore` gives: a path spelled out again is a path that can differ.
240
240
  */
241
241
  export function entitiesStore(runtime: Runtime, flags: OwnerFlags): BlobStore {
242
- const connection = ownerConnection(runtime.config, 'entities', flags);
243
- return scopeBlobStore(runtime.storage, scopeNamespace('entities', connection));
242
+ const connection = ownerConnection(runtime.config, 'lanes_entities', flags);
243
+ return scopeBlobStore(runtime.storage, scopeNamespace('lanes_entities', connection));
244
244
  }
@@ -105,6 +105,6 @@ export async function memoryForget(id: string | undefined, flags: OwnerFlags): P
105
105
  * again, so the CLI cannot address a different directory from the provider.
106
106
  */
107
107
  export function memoryStore(runtime: Runtime, flags: OwnerFlags): BlobStore {
108
- const connection = ownerConnection(runtime.config, 'memory', flags);
109
- return scopeBlobStore(runtime.storage, scopeNamespace('memory', connection));
108
+ const connection = ownerConnection(runtime.config, 'lanes_memory', flags);
109
+ return scopeBlobStore(runtime.storage, scopeNamespace('lanes_memory', connection));
110
110
  }
@@ -189,6 +189,6 @@ function assertStatus(raw: string | undefined): TaskStatus | undefined {
189
189
  * CLI cannot address a different directory from the provider.
190
190
  */
191
191
  export function tasksStore(runtime: Runtime, flags: OwnerFlags): BlobStore {
192
- const connection = ownerConnection(runtime.config, 'tasks', flags);
193
- return scopeBlobStore(runtime.storage, scopeNamespace('tasks', connection));
192
+ const connection = ownerConnection(runtime.config, 'lanes_tasks', flags);
193
+ return scopeBlobStore(runtime.storage, scopeNamespace('lanes_tasks', connection));
194
194
  }
@@ -50,7 +50,7 @@ export async function vaultGet(id: string | undefined, flags: OwnerFlags): Promi
50
50
  const itemId = required(id, 'lanes link vault get <id> [--show]');
51
51
 
52
52
  await withRuntime(flags, async (runtime) => {
53
- const connection = ownerConnection(runtime.config, 'vault', flags);
53
+ const connection = ownerConnection(runtime.config, 'lanes_vault', flags);
54
54
  const item = await runtime.vault.get(connection, itemId);
55
55
  if (!item) throw new ConfigError(`No vault item "${itemId}" in this profile.`);
56
56
 
@@ -77,7 +77,7 @@ export async function vaultSet(id: string | undefined, flags: OwnerFlags): Promi
77
77
  const value = await readStdin(`lanes link vault set ${itemId}`, 'the secret');
78
78
 
79
79
  await withRuntime(flags, async (runtime) => {
80
- const connection = ownerConnection(runtime.config, 'vault', flags);
80
+ const connection = ownerConnection(runtime.config, 'lanes_vault', flags);
81
81
  const replacing = (await runtime.vault.get(connection, itemId)) !== null;
82
82
 
83
83
  await runtime.vault.put(connection, {
@@ -104,7 +104,7 @@ export async function vaultRemove(id: string | undefined, flags: OwnerFlags): Pr
104
104
  const itemId = required(id, 'lanes link vault remove <id>');
105
105
 
106
106
  await withRuntime(flags, async (runtime) => {
107
- const connection = ownerConnection(runtime.config, 'vault', flags);
107
+ const connection = ownerConnection(runtime.config, 'lanes_vault', flags);
108
108
  const item = await runtime.vault.get(connection, itemId);
109
109
  if (!item) throw new ConfigError(`No vault item "${itemId}" in this profile.`);
110
110