@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,4 +1,4 @@
1
- import type { IdentityEntry, ProfileSelection, Resolution } from '#profile';
1
+ import { CONNECTIONS_FILE, type IdentityEntry, type ProfileSelection, type Resolution } from '#profile';
2
2
  import { ConfigDocument } from '../config-edit.ts';
3
3
  import { ensureIdentityConnection, repairLines, repaired } from '../config-repair.ts';
4
4
  import { announce, announceProfile, emit, ok, print, style, table, warn } from '../output.ts';
@@ -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
  /**
@@ -51,14 +51,20 @@ function covers(rules: ReadonlyArray<{ capability: string }>): boolean {
51
51
  * misleading thing this command could print.
52
52
  */
53
53
  function readable(config: {
54
- connections: ReadonlyArray<{ provider: string }>;
55
- policy: { allow: ReadonlyArray<{ capability: string }>; deny: ReadonlyArray<{ capability: string }> };
54
+ grants: ReadonlyArray<{
55
+ connection: string;
56
+ allow: ReadonlyArray<{ capability: string }>;
57
+ deny: ReadonlyArray<{ capability: string }>;
58
+ }>;
56
59
  }): boolean {
57
- return (
58
- config.connections.some((connection) => connection.provider === 'identity') &&
59
- covers(config.policy.allow) &&
60
- !covers(config.policy.deny)
61
- );
60
+ // One row now rather than a connection and a rule that had to agree
61
+ // (ADR-058). The old shape could say "declared but not granted" and
62
+ // "granted but not declared", and both served nothing while reading like
63
+ // configuration that worked; neither is expressible here.
64
+ const grant = config.grants.find((row) => row.connection.startsWith('lanes_identity.'));
65
+ if (grant === undefined) return false;
66
+
67
+ return covers(grant.allow) && !covers(grant.deny);
62
68
  }
63
69
 
64
70
  /**
@@ -96,7 +102,13 @@ export async function addIdentity(
96
102
  const entry: IdentityEntry = { kind, value, ...(options.note ? { note: options.note } : {}) };
97
103
  document.addTo(['identity'], entry, { inline: true });
98
104
 
99
- const repair = ensureIdentityConnection(document);
105
+ // The identity *connection* is the workspace's, like every other (ADR-057),
106
+ // so provisioning the surface on first use touches two files. Both saves, or
107
+ // neither: a grant naming a connection that does not exist is refused at load,
108
+ // so writing the profile alone would leave one that no longer opens.
109
+ const connections = await ConfigDocument.openKey(resolution.workspaceRoot, CONNECTIONS_FILE);
110
+ const repair = ensureIdentityConnection(connections, document);
111
+ await connections.save();
100
112
  await document.save();
101
113
 
102
114
  return {
@@ -232,7 +244,7 @@ export async function identityList(options: { json?: boolean } & GlobalFlags): P
232
244
  print(warn('declared, but no agent can read it'));
233
245
  print(
234
246
  style.dim(
235
- ' 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;',
236
248
  ),
237
249
  );
238
250
  print(style.dim(' adding an entry with this command writes both.'));
@@ -1,4 +1,5 @@
1
1
  import {
2
+ soleGrantFor,
2
3
  ConfigError,
3
4
  KNOWLEDGE_LAYOUT,
4
5
  knowledgeTargetSchema,
@@ -23,7 +24,6 @@ import {
23
24
  removeLocal,
24
25
  summarise,
25
26
  } from './migrate.ts';
26
-
27
27
  /**
28
28
  * `lanes link knowledge` — where this profile's memory and skills are kept.
29
29
  *
@@ -65,78 +65,6 @@ export interface KnowledgeFlags extends GlobalFlags {
65
65
  readonly fetch?: FetchLike | undefined;
66
66
  }
67
67
 
68
- export async function knowledgeShow(flags: KnowledgeFlags): Promise<void> {
69
- const runtime = await openRuntime(flags, { fetch: flags.fetch });
70
- try {
71
- if (!flags.json) announce(runtime.resolution);
72
-
73
- const profile = runtime.config.instance.profile;
74
- const selection = ` --profile ${runtime.resolution.profile} --target ${runtime.target}`;
75
- const skills = (await runtime.skills.list()).length;
76
- const memory = (await runtime.storage.list(`${KNOWLEDGE_LAYOUT.memory}/`)).length;
77
- const entities = (await runtime.storage.list(`${KNOWLEDGE_LAYOUT.entities}/`)).length;
78
- const where = runtime.knowledge?.describe;
79
-
80
- if (flags.json) {
81
- print(
82
- JSON.stringify(
83
- { target: runtime.target, where: where ?? 'local', memory, skills, entities },
84
- null,
85
- 2,
86
- ),
87
- );
88
- return;
89
- }
90
-
91
- heading('Knowledge');
92
- table([
93
- // The memory *directory*, not the blob root it sits in. `layout.blobs`
94
- // is `data/<profile>`, which is where every provider's namespace lives —
95
- // printing it here would name a directory that is mostly not memory.
96
- [
97
- ' memory',
98
- where ? `${where}/${KNOWLEDGE_LAYOUT.memory}` : `${layout.blobs(profile)}/${KNOWLEDGE_LAYOUT.memory}`,
99
- style.dim(`${memory} file${memory === 1 ? '' : 's'}`),
100
- ],
101
- [
102
- ' skills',
103
- where ? `${where}/${KNOWLEDGE_LAYOUT.skills}` : layout.skills(profile),
104
- style.dim(`${skills} file${skills === 1 ? '' : 's'}`),
105
- ],
106
- // The count includes the derived `_index.json`, deliberately: it is a
107
- // file in that directory and it is committed with the rest, so a number
108
- // that quietly excluded it would not match what a person sees there.
109
- [
110
- ' entities',
111
- where
112
- ? `${where}/${KNOWLEDGE_LAYOUT.entities}`
113
- : `${layout.blobs(profile)}/${KNOWLEDGE_LAYOUT.entities}`,
114
- style.dim(`${entities} file${entities === 1 ? '' : 's'}`),
115
- ],
116
- ]);
117
-
118
- print('');
119
- print(
120
- style.dim(
121
- ` The vault, the credential store, runtime state and the audit log stay in target "${runtime.target}".`,
122
- ),
123
- );
124
- // Complete commands, not shapes. Every one of these is pasted, and with
125
- // nothing left to fall back on (ADR-037) a line missing either flag is a
126
- // line that refuses — `emitted.test.ts` states the rule for the templates
127
- // reachable without a runtime, and this is the same rule where there is one.
128
- print(
129
- style.dim(
130
- where
131
- ? ` Bring them back with: lanes link knowledge use local --migrate${selection}`
132
- : ` Keep them in a repository with: lanes link knowledge use github --repo <owner/name>${selection}`,
133
- ),
134
- );
135
- } finally {
136
- await runtime.close();
137
- }
138
- }
139
-
140
68
  export async function knowledgeUse(where: string | undefined, flags: KnowledgeFlags): Promise<void> {
141
69
  if (where === 'local') return useLocal(flags);
142
70
  if (where === 'github') return useGithub(flags);
@@ -172,7 +100,7 @@ async function useGithub(flags: KnowledgeFlags): Promise<void> {
172
100
  const runtime = await openRuntime(flags, { fetch: flags.fetch });
173
101
  try {
174
102
  announce(runtime.resolution);
175
- const selection = ` --profile ${runtime.resolution.profile} --target ${runtime.target}`;
103
+ const selection = ` --profile ${runtime.resolution.profile} --workspace ${runtime.target}`;
176
104
 
177
105
  if (runtime.knowledge) {
178
106
  print(style.dim(` This profile already reads ${runtime.knowledge.describe}.`));
@@ -187,11 +115,7 @@ async function useGithub(flags: KnowledgeFlags): Promise<void> {
187
115
 
188
116
  // Nothing is written until this returns: the token is asked for, the
189
117
  // repository is probed, and either can refuse while the profile is intact.
190
- const secrets = await openSecretStoreFor(
191
- runtime.config,
192
- runtime.resolution.workspaceRoot,
193
- runtime.target,
194
- );
118
+ const secrets = await openSecretStoreFor(runtime.resolution.workspaceRoot, runtime.target);
195
119
  const token = await resolveToken(secrets, knowledge.token_ref, knowledge.repo, selection, {
196
120
  replace: flags.replace,
197
121
  });
@@ -206,7 +130,16 @@ async function useGithub(flags: KnowledgeFlags): Promise<void> {
206
130
  [' token', viewer, style.dim('can write')],
207
131
  ]);
208
132
 
209
- const movable = await localContents(runtime.storage, runtime.skills, knowledge);
133
+ // Which instances this profile grants, so `memory/` and `entities/` reach
134
+ // only its own and not every profile's (ADR-059).
135
+ const instances = grantedInstances(runtime.config);
136
+
137
+ const movable = await localContents(
138
+ runtime.storage,
139
+ runtime.skills ?? null,
140
+ knowledge,
141
+ instances,
142
+ );
210
143
  const moving = await decideMigration(movable.length > 0, flags);
211
144
 
212
145
  if (moving) {
@@ -225,7 +158,7 @@ async function useGithub(flags: KnowledgeFlags): Promise<void> {
225
158
  if (flags.keep) {
226
159
  print(style.dim(' --keep: the local copies are still there, and are no longer read.'));
227
160
  } else {
228
- await removeLocal(runtime.storage, runtime.skills, movable);
161
+ await removeLocal(runtime.storage, runtime.skills ?? null, movable, instances);
229
162
  print(ok('removed the local copies'));
230
163
  }
231
164
  } else if (movable.length > 0) {
@@ -268,11 +201,7 @@ async function useLocal(flags: KnowledgeFlags): Promise<void> {
268
201
 
269
202
  // Built here rather than taken from `runtime.knowledge`, so both directions
270
203
  // of this command reach the repository through one constructor.
271
- const secrets = await openSecretStoreFor(
272
- runtime.config,
273
- runtime.resolution.workspaceRoot,
274
- runtime.target,
275
- );
204
+ const secrets = await openSecretStoreFor(runtime.resolution.workspaceRoot, runtime.target);
276
205
  const token = await resolveToken(secrets, knowledge.token_ref, knowledge.repo, selection);
277
206
  const repository = repositoryFor(knowledge, token, flags.fetch);
278
207
 
@@ -280,7 +209,13 @@ async function useLocal(flags: KnowledgeFlags): Promise<void> {
280
209
  flags.migrate ??
281
210
  (await agreedTo(flags, `Copy everything in ${knowledge.repo} back onto this target's storage?`))
282
211
  ) {
283
- const moved = await moveOut(repository, knowledge, local.storage, local.skills);
212
+ const moved = await moveOut(
213
+ repository,
214
+ knowledge,
215
+ local.storage,
216
+ local.skills,
217
+ grantedInstances(runtime.config),
218
+ );
284
219
  print(
285
220
  ok(
286
221
  `wrote back ${moved.memory} memory entr${moved.memory === 1 ? 'y' : 'ies'}, ` +
@@ -314,7 +249,7 @@ async function useLocal(flags: KnowledgeFlags): Promise<void> {
314
249
  */
315
250
  async function openLocalStores(
316
251
  runtime: Runtime,
317
- ): Promise<{ storage: BlobStore; skills: BlobStore }> {
252
+ ): Promise<{ storage: BlobStore; skills: BlobStore | null }> {
318
253
  const { openStorage } = await import('#deployments/target.ts');
319
254
  const declared = runtime.declared;
320
255
 
@@ -327,7 +262,14 @@ async function openLocalStores(
327
262
  },
328
263
  runtime.credentials,
329
264
  );
330
- return { storage: factory(), skills: factory(layout.skills(runtime.config.instance.profile)) };
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');
269
+ return {
270
+ storage: factory(),
271
+ skills: skillsConnection === undefined ? null : factory(layout.skills(runtime.config.instance.profile, skillsConnection)),
272
+ };
331
273
  }
332
274
 
333
275
  /**
@@ -386,3 +328,21 @@ async function agreedTo(flags: KnowledgeFlags, question: string): Promise<boolea
386
328
  if (!yes) print(style.dim(' cancelled'));
387
329
  return yes;
388
330
  }
331
+
332
+ /**
333
+ * The memory and entities instances one profile grants.
334
+ *
335
+ * `main` where a profile grants none, which is what a profile that predates the
336
+ * owner layer looks like and is also where a fresh one puts them. The point is
337
+ * that this is never the *surface* — `memory/` alone matches every profile's
338
+ * notes now that the blob root is the workspace's.
339
+ */
340
+ function grantedInstances(config: Parameters<typeof soleGrantFor>[0]): {
341
+ memory: string;
342
+ entities: string;
343
+ } {
344
+ return {
345
+ memory: soleGrantFor(config, 'lanes_memory') ?? 'main',
346
+ entities: soleGrantFor(config, 'lanes_entities') ?? 'main',
347
+ };
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,
@@ -43,16 +44,75 @@ export interface Movable {
43
44
  * it once means `localContents`, `removeLocal` and `moveOut` each became a loop
44
45
  * over this rather than a branch on a literal.
45
46
  */
46
- type LocalArea = { readonly store: BlobStore; readonly prefix: string };
47
+ /**
48
+ * One area's local home: where to look, and what of that key is the repository's.
49
+ *
50
+ * Two prefixes rather than one, and the difference is the whole fix. `scope` is
51
+ * what is listed — `memory/<instance>/`, so a profile sees only the instance it
52
+ * grants. `prefix` is what is stripped to get the repository path, which stays
53
+ * `memory/<instance>/<file>` because that is the layout ADR-041 documents and
54
+ * what makes two profiles' notes distinguishable in one repository.
55
+ *
56
+ * Collapsing them listed every profile's memory and then wrote it flat.
57
+ */
58
+ type LocalArea = {
59
+ readonly store: BlobStore;
60
+ readonly scope: string;
61
+ readonly prefix: string;
62
+ };
47
63
 
48
- function localAreas(storage: BlobStore, skills: BlobStore): Record<KnowledgeArea, LocalArea> {
64
+ /** The connection ids this profile's memory and entities live under. */
65
+ export interface Instances {
66
+ readonly memory: string;
67
+ readonly entities: string;
68
+ }
69
+
70
+ function localAreas(
71
+ storage: BlobStore,
72
+ skills: BlobStore | null,
73
+ /** Which instance of each surface this profile grants (ADR-059). */
74
+ instances: Instances,
75
+ ): Record<KnowledgeArea, LocalArea> {
49
76
  return {
50
- memory: { store: storage, prefix: `${KNOWLEDGE_LAYOUT.memory}/` },
51
- skills: { store: skills, prefix: '' },
52
- entities: { store: storage, prefix: `${KNOWLEDGE_LAYOUT.entities}/` },
77
+ // Scoped to the instance this profile grants, not to the surface.
78
+ //
79
+ // `layout.blobs()` is the whole workspace since contract 3, so a prefix of
80
+ // `memory/` matched every profile's memory — and `knowledge use github
81
+ // --migrate --profile personal` therefore committed `work`'s notes to
82
+ // personal's repository and then deleted them locally, leaving `work`
83
+ // reading an empty store with no knowledge block of its own. `skills` was
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.
89
+ memory: {
90
+ store: storage,
91
+ scope: `${KNOWLEDGE_PREFIX.memory}/${instances.memory}/`,
92
+ prefix: `${KNOWLEDGE_PREFIX.memory}/`,
93
+ },
94
+ // Null becomes an empty area rather than a refusal: a profile granting no
95
+ // skills connection has none to move, and `knowledge use` should still move
96
+ // its memory and entities rather than failing on the one area that is empty
97
+ // by construction (ADR-059).
98
+ skills: { store: skills ?? EMPTY_AREA, scope: '', prefix: '' },
99
+ entities: {
100
+ store: storage,
101
+ scope: `${KNOWLEDGE_PREFIX.entities}/${instances.entities}/`,
102
+ prefix: `${KNOWLEDGE_PREFIX.entities}/`,
103
+ },
53
104
  };
54
105
  }
55
106
 
107
+ /** Nothing to list, nothing to delete. See `localAreas`. */
108
+ const EMPTY_AREA: BlobStore = {
109
+ get: async () => null,
110
+ put: async () => {},
111
+ has: async () => false,
112
+ delete: async () => {},
113
+ list: async () => [],
114
+ };
115
+
56
116
  const AREAS = ['memory', 'skills', 'entities'] as const;
57
117
 
58
118
  /**
@@ -66,16 +126,18 @@ const AREAS = ['memory', 'skills', 'entities'] as const;
66
126
  */
67
127
  export async function localContents(
68
128
  storage: BlobStore,
69
- skills: BlobStore,
129
+ /** Null when the profile grants no skills connection (ADR-059). */
130
+ skills: BlobStore | null,
70
131
  knowledge: KnowledgeConfig,
132
+ instances: Instances,
71
133
  ): Promise<Movable[]> {
72
- const areas = localAreas(storage, skills);
134
+ const areas = localAreas(storage, skills, instances);
73
135
  const found: Movable[] = [];
74
136
 
75
137
  for (const area of AREAS) {
76
- const { store, prefix } = areas[area];
138
+ const { store, scope, prefix } = areas[area];
77
139
 
78
- for (const entry of await store.list(prefix)) {
140
+ for (const entry of await store.list(scope)) {
79
141
  const data = await store.get(entry.key);
80
142
  if (data === null) continue; // Listed then deleted; not worth failing over.
81
143
 
@@ -128,10 +190,12 @@ export async function moveIn(
128
190
  /** Remove what has been verified elsewhere. Never called before `moveIn`. */
129
191
  export async function removeLocal(
130
192
  storage: BlobStore,
131
- skills: BlobStore,
193
+ /** Null when the profile grants no skills connection (ADR-059). */
194
+ skills: BlobStore | null,
132
195
  movable: readonly Movable[],
196
+ instances: Instances,
133
197
  ): Promise<void> {
134
- const areas = localAreas(storage, skills);
198
+ const areas = localAreas(storage, skills, instances);
135
199
 
136
200
  for (const item of movable) {
137
201
  const { store, prefix } = areas[item.area];
@@ -152,10 +216,12 @@ export async function moveOut(
152
216
  repository: GithubRepository,
153
217
  knowledge: KnowledgeConfig,
154
218
  storage: BlobStore,
155
- skills: BlobStore,
219
+ /** Null when the profile grants no skills connection (ADR-059). */
220
+ skills: BlobStore | null,
221
+ instances: Instances,
156
222
  ): Promise<Record<KnowledgeArea, number>> {
157
223
  const { entries } = await repository.entries();
158
- const areas = localAreas(storage, skills);
224
+ const areas = localAreas(storage, skills, instances);
159
225
  const counts: Record<KnowledgeArea, number> = { memory: 0, skills: 0, entities: 0 };
160
226
 
161
227
  for (const entry of entries.values()) {
@@ -0,0 +1,97 @@
1
+ import { KNOWLEDGE_LAYOUT, KNOWLEDGE_PREFIX, layout, soleGrantFor } from '#profile';
2
+ import { announce, emit, heading, print, style, table } from '../../output.ts';
3
+ import { openRuntime } from '../../runtime.ts';
4
+ import type { KnowledgeFlags } from './index.ts';
5
+
6
+ /**
7
+ * `lanes link knowledge show` — where memory, skills and entities actually are.
8
+ *
9
+ * Split from `knowledge use` so `index.ts` stays inside the size budget, on the
10
+ * seam the two commands already have: this one opens stores and counts, and
11
+ * never writes. That difference is why `show` can answer for a repository `use`
12
+ * would refuse to touch.
13
+ */
14
+
15
+ export async function knowledgeShow(flags: KnowledgeFlags): Promise<void> {
16
+ const runtime = await openRuntime(flags, { fetch: flags.fetch });
17
+ try {
18
+ if (!flags.json) announce(runtime.resolution);
19
+
20
+ const profile = runtime.config.instance.profile;
21
+ const selection = ` --profile ${runtime.resolution.profile} --target ${runtime.target}`;
22
+ const skills = runtime.skills ? (await runtime.skills.list()).length : 0;
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;
29
+ const where = runtime.knowledge?.describe;
30
+ // Null when no skills connection is granted (ADR-059). The row still prints,
31
+ // saying so: a missing row reads as "there are none", not "not granted".
32
+ const skillsConnection = soleGrantFor(runtime.config, 'lanes_skills');
33
+
34
+ if (flags.json) {
35
+ print(
36
+ JSON.stringify(
37
+ { target: runtime.target, where: where ?? 'local', memory, skills, entities },
38
+ null,
39
+ 2,
40
+ ),
41
+ );
42
+ return;
43
+ }
44
+
45
+ heading('Knowledge');
46
+ table([
47
+ // The memory *directory*, not the blob root it sits in. `layout.blobs`
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.
51
+ [
52
+ ' memory',
53
+ where ? `${where}/${KNOWLEDGE_LAYOUT.memory}` : `${layout.blobs(profile)}/${KNOWLEDGE_PREFIX.memory}`,
54
+ style.dim(`${memory} file${memory === 1 ? '' : 's'}`),
55
+ ],
56
+ [
57
+ ' skills',
58
+ where
59
+ ? `${where}/${KNOWLEDGE_LAYOUT.skills}`
60
+ : skillsConnection
61
+ ? layout.skills(profile, skillsConnection)
62
+ : style.dim('not granted'),
63
+ style.dim(`${skills} file${skills === 1 ? '' : 's'}`),
64
+ ],
65
+ // The count includes the derived `_index.json`, deliberately: it is a
66
+ // file in that directory and it is committed with the rest, so a number
67
+ // that quietly excluded it would not match what a person sees there.
68
+ [
69
+ ' entities',
70
+ where
71
+ ? `${where}/${KNOWLEDGE_LAYOUT.entities}`
72
+ : `${layout.blobs(profile)}/${KNOWLEDGE_PREFIX.entities}`,
73
+ style.dim(`${entities} file${entities === 1 ? '' : 's'}`),
74
+ ],
75
+ ]);
76
+
77
+ print('');
78
+ print(
79
+ style.dim(
80
+ ` The vault, the credential store, runtime state and the audit log stay in workspace "${runtime.target}".`,
81
+ ),
82
+ );
83
+ // Complete commands, not shapes. Every one of these is pasted, and with
84
+ // nothing left to fall back on (ADR-037) a line missing either flag is a
85
+ // line that refuses — `emitted.test.ts` states the rule for the templates
86
+ // reachable without a runtime, and this is the same rule where there is one.
87
+ print(
88
+ style.dim(
89
+ where
90
+ ? ` Bring them back with: lanes link knowledge use local --migrate${selection}`
91
+ : ` Keep them in a repository with: lanes link knowledge use github --repo <owner/name>${selection}`,
92
+ ),
93
+ );
94
+ } finally {
95
+ await runtime.close();
96
+ }
97
+ }
@@ -7,4 +7,5 @@
7
7
  * the grammar that puts them in order.
8
8
  */
9
9
 
10
- export { knowledgeShow, knowledgeUse, type KnowledgeFlags } from './knowledge/index.ts';
10
+ export { knowledgeUse, type KnowledgeFlags } from './knowledge/index.ts';
11
+ export { knowledgeShow } from './knowledge/show.ts';
@@ -17,12 +17,31 @@ import { join } from 'node:path';
17
17
  * command, write the file where it does not. A skill is content in a documented
18
18
  * location, not a config format we would be guessing at, and the directory
19
19
  * written is named after this project.
20
+ *
21
+ * **No registration carries a token any more** (ADR-062). Every endpoint runs
22
+ * the authorization flow, discovery is served ahead of the auth gate on
23
+ * loopback as well as deployed, and a client pointed at a bare URL finds
24
+ * `/.well-known/oauth-protected-resource`, signs its owner in at lanes.sh, and
25
+ * comes back with a token of its own. What that removes is worth stating: the
26
+ * registration no longer contains a credential, so a config file synced to a
27
+ * dotfiles repository is no longer a leak, and a rotation does not invalidate
28
+ * every harness at once.
29
+ *
30
+ * The exception is `tokenEnv`, which survives for the one caller that has no
31
+ * browser — see `token show`, which is now documented as a CI command.
20
32
  */
21
33
 
22
34
  export interface AddInput {
23
35
  readonly name: string;
24
36
  readonly url: string;
25
- readonly token: string;
37
+ /**
38
+ * The endpoint's static token, for a harness that cannot run a browser.
39
+ *
40
+ * Nothing passes this in the ordinary path. It is here because
41
+ * `--headless` exists for CI, where there is no browser to complete an
42
+ * authorization in and a pasted credential is the only thing that works.
43
+ */
44
+ readonly token?: string | undefined;
26
45
  readonly tokenEnv: string;
27
46
  readonly scope: string;
28
47
  /**
@@ -44,11 +63,12 @@ export interface Harness {
44
63
  /** Whether `--scope` means anything here. Codex config is global. */
45
64
  readonly scoped: boolean;
46
65
  /**
47
- * Whether the token is handed to the harness at all.
66
+ * Whether a credential reaches the harness's config at all.
48
67
  *
49
- * Codex stores the *name* of an environment variable and reads it when it
50
- * launches, so the secret never reaches its config file and a rotation is
51
- * picked up without re-registering. Claude Code stores the header value.
68
+ * Both are `false` in the ordinary path now the client authorises itself.
69
+ * This still distinguishes what a `--headless` registration would write:
70
+ * Claude Code would hold the header value, and Codex stores only the *name*
71
+ * of an environment variable it reads at launch.
52
72
  */
53
73
  readonly storesToken: boolean;
54
74
  /**
@@ -99,8 +119,10 @@ export const HARNESSES: readonly Harness[] = [
99
119
  'http',
100
120
  name,
101
121
  url,
102
- '--header',
103
- `Authorization: Bearer ${token}`,
122
+ // Only when there is no browser to sign in with. Registering the bare URL
123
+ // is the ordinary path: Claude Code discovers the protected-resource
124
+ // document and runs the authorization itself.
125
+ ...(token ? ['--header', `Authorization: Bearer ${token}`] : []),
104
126
  '--scope',
105
127
  scope,
106
128
  ],
@@ -142,7 +164,7 @@ export const HARNESSES: readonly Harness[] = [
142
164
  // anywhere else: an unresolvable substitution yields the empty string, the
143
165
  // header becomes "Bearer ", and the only symptom is a 401 that reads as a
144
166
  // bad token rather than a command that refused.
145
- ` export ${tokenEnv}="$(lanes link token show --raw --profile ${profile} --target ${target})"`,
167
+ ` export ${tokenEnv}="$(lanes link token show --raw --profile ${profile} --workspace ${target})"`,
146
168
  '',
147
169
  'Add that to your shell profile. This is the better half of the bargain: the token never',
148
170
  'reaches ~/.codex/config.toml, and a "lanes link token rotate" is picked up on next launch',