@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,16 +1,22 @@
1
1
  import { BearerAuthenticator, ownerPrincipal } from '#auth';
2
2
  import type { SecretStore } from '#secrets';
3
3
  import type { AuditReader } from '#audit';
4
- import type { RuntimeState } from '#stores/state';
4
+ import { DISCOVERY_NAMESPACE, type RuntimeState } from '#stores/state';
5
5
  import type { BlobStore } from '#stores/blobs';
6
6
  import type { AnyConnector, ProviderManifest } from '#connectivity';
7
7
  import { RateLimiter, allowedConnections } from '#policy';
8
8
  import {
9
+ assertGrantsResolve,
9
10
  layout,
10
11
  listProfiles,
12
+ readConnections,
13
+ selectConnections,
14
+ soleGrantFor,
11
15
  workspacePath,
12
16
  type Config,
17
+ type ConnectionConfig,
13
18
  type Resolution,
19
+ type SelectedConnection,
14
20
  type TargetConfig,
15
21
  } from '#profile';
16
22
  import { ProviderRegistry, toPolicyDocument } from '#registry';
@@ -33,6 +39,10 @@ import { resolveProfile, type GlobalFlags } from './select.ts';
33
39
  import { buildRegistryWithWorkspace, readSkillsForStart, reloadSkills } from './registry.ts';
34
40
  import { discoveryProbe } from './discovery.ts';
35
41
  import { openVault } from './vault.ts';
42
+ import { EMPTY_SKILL_STORE, skillStore } from './stores.ts';
43
+ import type { Runtime } from './types.ts';
44
+
45
+ export type { Runtime } from './types.ts';
36
46
 
37
47
  /**
38
48
  * Assembling a profile's runtime from its declared target.
@@ -43,86 +53,20 @@ import { openVault } from './vault.ts';
43
53
  * nothing at the application layer.
44
54
  */
45
55
 
46
- export interface Runtime {
47
- readonly resolution: Resolution;
48
- readonly config: Config;
49
- readonly target: string;
50
- /**
51
- * The target's adapter set, from the workspace that declares it.
52
- *
53
- * Here because the config no longer carries it. Every caller that used to
54
- * reach `config.targets[target]` — for a `deploy` block, a storage adapter, a
55
- * knowledge repository — reads this instead, and reads it without following
56
- * the pointer a second time (ADR-052).
57
- */
58
- readonly declared: TargetConfig;
59
- readonly state: RuntimeState;
60
- /** The durable log, for reading. Copies, if any, are write-only and not here. */
61
- readonly audit: AuditReader;
62
- readonly credentials: SecretStore;
63
- readonly storage: BlobStore;
64
- /** Where skills are kept — `data/<profile>/skills.d/` locally. */
65
- readonly skills: BlobStore;
66
- /**
67
- * Present only when this target keeps memory and skills in a repository.
68
- *
69
- * Nothing on the dispatch path reads it: `storage` and `skills` above are
70
- * already pointed at the right bytes, which is the whole design. It is here
71
- * so `target show` and `doctor` can say where those bytes are without
72
- * re-reading the config, and so the migration can reach the repository it is
73
- * committing to.
74
- */
75
- readonly knowledge?: KnowledgeStores | undefined;
76
- /** The vault's own store, so `lanes link vault` reaches the same bytes MCP does. */
77
- readonly vault: VaultStore;
78
- readonly registry: ProviderRegistry;
79
- /**
80
- * Re-read the skills into the registry when they have changed on the store.
81
- *
82
- * Cheap and idempotent — one `list()`, and a rebuild only when the listing
83
- * differs from the last one. The caller decides how often to ask.
84
- */
85
- refreshSkills(): Promise<void>;
86
- readonly dispatcher: Dispatcher;
87
- readonly authenticator: BearerAuthenticator;
88
- /** Same factory the dispatcher uses, exposed for commands that probe upstream. */
89
- connectorFor(providerId: string, connectionId: string): AnyConnector | undefined;
90
- /**
91
- * Same authorizer the dispatcher uses, for the same reason `connectorFor` is here.
92
- *
93
- * A command that probes upstream needs the credential on the request, and it
94
- * must not learn how to put it there — that is one switch on the resolved
95
- * shape (`connectivity/auth/authorize.ts`) and a second copy would be a
96
- * second answer. `settleIdentity` is the caller: it asks a provider whose
97
- * account was just authorised, over whatever method that provider declares.
98
- */
99
- authorizeRequest(providerId: string, connectionId: string, request: Request): Promise<Request>;
100
- /** A provider's manifest, so an omitted `credential_ref` can be derived from it. */
101
- manifestFor(providerId: string): ProviderManifest | undefined;
102
- close(): Promise<void>;
103
- }
104
-
105
56
  /**
106
- * Where this profile's skills live, in either target.
107
- *
108
- * Locally `data/<profile>/skills.d/`; deployed, the same key under the bucket
109
- * prefix. Going through the store rather than a filesystem path is what gives a
110
- * deployment skills at all — a path is baked into a container image at build
111
- * time and an object key is not, so before ADR-014 a deployed instance could
112
- * only ever serve the skills that existed when its image was built.
57
+ * The accounts a profile reaches, as most callers want them.
113
58
  *
114
- * **Per profile**, which reverses ADR-012 §1. Policy gating `skills.<name>`
115
- * was the whole isolation story while the bytes were shared, and it is a weak
116
- * one: it decides who may *run* a procedure, not who may read that it exists or
117
- * what it says. A skill written for work names work's accounts and work's
118
- * people. ADR-030.
119
- *
120
- * An explicit area, matching `openState` and `openAudit` — a profile that
121
- * declares its own `storage.path` moves its provider blobs, not the reserved
122
- * roots beside them.
59
+ * `runtime.connections` pairs each with the grant governing it, which is what
60
+ * policy and the setup surface need. Everything else probing credentials,
61
+ * listing accounts, reconciling state wants the rows alone, and unwrapping
62
+ * them at twenty call sites is twenty chances to reach for
63
+ * `workspaceConnections` by mistake and answer for accounts this profile was
64
+ * never granted.
123
65
  */
124
- function skillStore(storage: StorageFactory, profile: string): BlobStore {
125
- return storage(layout.skills(profile));
66
+ export function grantedConnections(
67
+ runtime: Pick<Runtime, 'connections'>,
68
+ ): ConnectionConfig[] {
69
+ return runtime.connections.map(({ connection }) => connection);
126
70
  }
127
71
 
128
72
  /**
@@ -152,6 +96,15 @@ export async function openRuntime(
152
96
  const root = resolved.workspaceRoot;
153
97
  const adapters: TargetInput = { declared, config, root, target };
154
98
 
99
+ // The workspace's accounts, and the check that this profile's grants name
100
+ // real ones (ADR-057). Read here rather than inside `resolveProfile` because
101
+ // it is a property of the workspace rather than of the selection, and because
102
+ // a command that only wants to know which profiles exist should not have to
103
+ // parse every connection to find out.
104
+ const connectionsFile = await readConnections(root);
105
+ assertGrantsResolve(config, connectionsFile.connections);
106
+ const selected = selectConnections(config, connectionsFile.connections);
107
+
155
108
  // Credentials first: an S3 key pair is itself a credential reference, so the
156
109
  // credential store has to exist before the blob store that names it. State
157
110
  // and the log then ride that blob store rather than opening backends of
@@ -187,7 +140,18 @@ export async function openRuntime(
187
140
  (message) => logger.warn(message),
188
141
  );
189
142
 
190
- const skills = knowledge?.skills ?? skillStore(storageFor, config.instance.profile);
143
+ // Rooted at the skills connection this profile grants, not at the profile
144
+ // (ADR-059). At most one can be granted, which is what makes this a lookup
145
+ // rather than a choice — `load.ts` refuses a second one, for the reason a
146
+ // prompt has no argument to route on.
147
+ //
148
+ // `EMPTY_SKILLS` when none is granted: a profile that denies `skills.*` has
149
+ // no store to open, and handing it the workspace root instead would serve
150
+ // every other profile's procedures.
151
+ const skillsConnection = soleGrantFor(config, 'lanes_skills');
152
+ const skills =
153
+ knowledge?.skills ??
154
+ (skillsConnection === undefined ? undefined : skillStore(storageFor, config.instance.profile, skillsConnection));
191
155
 
192
156
  // The vault's own store, beside the credential store and never it: a separate
193
157
  // document, a separate key, and a separate environment variable
@@ -208,6 +172,7 @@ export async function openRuntime(
208
172
  let registry: ProviderRegistry;
209
173
  let fingerprint = '';
210
174
  const refreshSkills = async (): Promise<void> => {
175
+ if (skills === undefined) return;
211
176
  fingerprint = await reloadSkills(registry, skills, fingerprint, refreshSkills);
212
177
  };
213
178
 
@@ -226,23 +191,18 @@ export async function openRuntime(
226
191
  // registered into. Per call rather than a snapshot, so a policy the runtime
227
192
  // was opened with is re-evaluated rather than remembered.
228
193
  const reachable = (): ReadonlyArray<{ key: string; provider: string; account: string }> =>
229
- config.connections
230
- .filter((connection) =>
194
+ selected
195
+ .filter(({ ref, connection }) =>
231
196
  registry
232
197
  .capabilities()
233
198
  .some(
234
199
  ({ id }) =>
235
200
  id.startsWith(`${connection.provider}.`) &&
236
- allowedConnections(
237
- id,
238
- [`${connection.provider}.${connection.id}`],
239
- principal,
240
- policy,
241
- ).length > 0,
201
+ allowedConnections(id, [ref], principal, policy).length > 0,
242
202
  ),
243
203
  )
244
- .map((connection) => ({
245
- key: `${connection.provider}.${connection.id}`,
204
+ .map(({ ref, connection }) => ({
205
+ key: ref,
246
206
  provider: connection.provider,
247
207
  account: connection.account,
248
208
  ...(connection.label ? { label: connection.label } : {}),
@@ -253,14 +213,14 @@ export async function openRuntime(
253
213
  // tolerated when the store is a repository: a local directory that will not
254
214
  // read is a real fault worth failing on, exactly as it always was.
255
215
  const loaded = await readSkillsForStart(
256
- skills,
216
+ skills ?? EMPTY_SKILL_STORE,
257
217
  knowledge !== undefined,
258
218
  (message) => logger.warn(message),
259
- ` --profile ${config.instance.profile} --target ${target}`,
219
+ ` --profile ${config.instance.profile} --workspace ${target}`,
260
220
  );
261
221
 
262
- registry = await buildRegistryWithWorkspace(root, config.instance.profile, {
263
- skillStore: skills,
222
+ registry = await buildRegistryWithWorkspace(root, {
223
+ ...(skills ? { skillStore: skills } : {}),
264
224
  skills: loaded.skills,
265
225
  onSkillsChanged: refreshSkills,
266
226
  vault: { store: vaultStore, items: await vaultStore.ids() },
@@ -269,7 +229,7 @@ export async function openRuntime(
269
229
  target,
270
230
  profiles: await listProfiles(root),
271
231
  catalogue: PROVIDER_MANIFESTS,
272
- ownClients: Object.keys(config.oauth_apps),
232
+ ownClients: Object.keys(connectionsFile.oauth_apps),
273
233
  reachable,
274
234
  },
275
235
  // Straight off the config: unlike `reachable`, nothing has to be filtered
@@ -309,7 +269,7 @@ export async function openRuntime(
309
269
  }
310
270
  }
311
271
 
312
- const cached = await state.kv.get('discovery', entry.manifest.id);
272
+ const cached = await state.kv.get(DISCOVERY_NAMESPACE, entry.manifest.id);
313
273
  if (cached) {
314
274
  try {
315
275
  registry.setDiscovered(entry.manifest.id, JSON.parse(cached));
@@ -329,14 +289,16 @@ export async function openRuntime(
329
289
  credentials,
330
290
  // From the connection row, which is where a per-connection setting has
331
291
  // always lived. One lookup behind both, so they cannot disagree.
332
- connectionConfig: (provider, id) => row(config, provider, id)?.config,
333
- isDeclared: (provider, id) => row(config, provider, id) !== undefined,
292
+ connectionConfig: (provider, id) => row(connectionsFile.connections, provider, id)?.config,
293
+ isDeclared: (provider, id) => row(connectionsFile.connections, provider, id) !== undefined,
334
294
  });
335
295
  const authorizeRequest = requestAuthorizer(registry, credentials);
336
296
  let closed = false;
337
297
 
338
298
  const dispatcher = new Dispatcher({
339
299
  config,
300
+ connections: connectionsFile.connections,
301
+ oauthApps: Object.keys(connectionsFile.oauth_apps),
340
302
  registry,
341
303
  connectorFor,
342
304
  authorizeRequest,
@@ -354,6 +316,9 @@ export async function openRuntime(
354
316
  config,
355
317
  target,
356
318
  declared,
319
+ connections: selected,
320
+ workspaceConnections: connectionsFile.connections,
321
+ ownClients: Object.keys(connectionsFile.oauth_apps),
357
322
  state,
358
323
  audit,
359
324
  credentials,
@@ -390,5 +355,5 @@ export async function openRuntime(
390
355
  };
391
356
  }
392
357
 
393
- const row = (config: Config, provider: string, id: string) =>
394
- config.connections.find((connection) => connection.provider === provider && connection.id === id);
358
+ const row = (connections: readonly ConnectionConfig[], provider: string, id: string) =>
359
+ connections.find((connection) => connection.provider === provider && connection.id === id);
@@ -3,7 +3,7 @@ import type { ProviderDefinition } from '#connectivity';
3
3
  import { ConfigError } from '#profile';
4
4
  import { ProviderRegistry } from '#registry';
5
5
  import { RESERVED_BY_GRAMMAR } from '../commands/connect/custom/spec.ts';
6
- import { loadProfileProviders } from '#providers/custom/index.ts';
6
+ import { loadWorkspaceProviders } from '#providers/custom/index.ts';
7
7
  import { loadProfileSkills, type LoadedSkill } from '#providers/skills/store.ts';
8
8
  import { exampleProvider } from '#providers/example/provider.ts';
9
9
  import {
@@ -138,21 +138,20 @@ export function buildRegistry(owner: OwnerLayerOptions = {}): ProviderRegistry {
138
138
  * overriding it — an operator shadowing `gmail` by accident would be very hard
139
139
  * to diagnose from the outside.
140
140
  *
141
- * The profile is a parameter rather than read from a config here because two of
142
- * the three callers do not have one: `deploy` walks every profile in turn, and
143
- * `profile remove` is holding the name of the one being deleted. Passing it
144
- * makes "which profile's manifests" a question the caller has already answered.
141
+ * No profile parameter any more. Manifests moved to the workspace with the
142
+ * connections they describe (ADR-057), so "which profile's manifests" stopped
143
+ * being a question and with it the whole reason `deploy` had to rebuild a
144
+ * registry per profile to collect credential refs.
145
145
  */
146
146
  export async function buildRegistryWithWorkspace(
147
147
  root: string,
148
- profile: string,
149
148
  owner: OwnerLayerOptions = {},
150
149
  ): Promise<ProviderRegistry> {
151
150
  const skills =
152
151
  owner.skills ?? (owner.skillStore ? await loadProfileSkills(owner.skillStore) : []);
153
152
  const registry = buildRegistry({ ...owner, skills });
154
153
 
155
- for (const { manifest, path } of await loadProfileProviders(root, profile)) {
154
+ for (const { manifest, path } of await loadWorkspaceProviders(root)) {
156
155
  if (registry.has(manifest.id)) {
157
156
  throw new ConfigError(
158
157
  `${path}: provider "${manifest.id}" is already built in. Rename it, or remove the file to use the built-in.`,
@@ -126,21 +126,12 @@ export async function resolveProfileOnly(
126
126
  * inside Google's network. Opening a full runtime for either would fail on the
127
127
  * part that is not needed.
128
128
  */
129
- export async function openSecretStoreFor(
130
- config: Config,
131
- root: string,
132
- target: string,
133
- ): Promise<SecretStore> {
129
+ export async function openSecretStoreFor(root: string, target: string): Promise<SecretStore> {
134
130
  // Resolved here rather than taken from the caller, so `secrets push --from
135
131
  // local --to cloud` can hold two targets that live in two different workspaces
136
132
  // without the caller having to follow either pointer itself.
137
133
  const resolved = await openTarget(root, target);
138
- return openSecrets({
139
- declared: resolved.declared,
140
- config,
141
- root: resolved.workspaceRoot,
142
- target,
143
- });
134
+ return openSecrets({ declared: resolved.declared, root: resolved.workspaceRoot, target });
144
135
  }
145
136
 
146
137
  /**
@@ -0,0 +1,58 @@
1
+ import { layout } from '#profile';
2
+ import type { BlobStore } from '#stores/blobs';
3
+ import type { StorageFactory } from '#deployments/target.ts';
4
+
5
+ /**
6
+ * Where the owner layer's bytes are, for a runtime being assembled.
7
+ *
8
+ * Split from `open.ts` so that file stays inside the size budget, on the seam
9
+ * it already had: `open.ts` decides what a runtime *is* and this decides where
10
+ * one store lives. Both roots are explicit areas rather than derived from
11
+ * `storage.path`, matching `openState` and `openAudit` — a workspace that moves
12
+ * its provider blobs does not move the reserved roots beside them.
13
+ */
14
+
15
+ /**
16
+ * A store with nothing in it, for a profile that grants no skills connection.
17
+ *
18
+ * Written out rather than imported from `#stores/blobs/testing.ts`: that module
19
+ * exists for tests, and production code reaching into it is how a test double
20
+ * ends up on a real path. Five methods, none of which can fail, is cheaper than
21
+ * the import would be to justify.
22
+ */
23
+ export const EMPTY_SKILL_STORE: BlobStore = {
24
+ get: async () => null,
25
+ put: async () => {
26
+ throw new Error('this profile grants no skills connection');
27
+ },
28
+ has: async () => false,
29
+ delete: async () => {},
30
+ list: async () => [],
31
+ };
32
+
33
+ /**
34
+ * Where a connection's skills live, in either workspace.
35
+ *
36
+ * `profiles/<profile>/skills.d/<connection>/`, and deployed the same key under
37
+ * the bucket prefix. Going through the store rather than a filesystem path is what gives a
38
+ * deployment skills at all — a path is baked into a container image at build
39
+ * time and an object key is not, so before ADR-014 a deployed instance could
40
+ * only ever serve the skills that existed when its image was built.
41
+ *
42
+ * **Per profile and per connection**, which is the fourth answer this question
43
+ * has had. Policy gating `skills.<name>` was the whole isolation story while
44
+ * the bytes were shared (ADR-012 §1), and it is a weak one: it decides who may
45
+ * *run* a procedure, not who may read that it exists or what it says. ADR-030
46
+ * made the bytes per profile; ADR-059 made them per connection instead, so two
47
+ * profiles granting one skills connection shared a set; ADR-066 puts the
48
+ * profile back in front of it, so they do not. The connection stays because a
49
+ * profile may still hold more than one set.
50
+ */
51
+ export function skillStore(
52
+ storage: StorageFactory,
53
+ profile: string,
54
+ connection: string,
55
+ ): BlobStore {
56
+ return storage(layout.skills(profile, connection));
57
+ }
58
+
@@ -0,0 +1,106 @@
1
+ import type { BearerAuthenticator } from '#auth';
2
+ import type { SecretStore } from '#secrets';
3
+ import type { AuditReader } from '#audit';
4
+ import type { RuntimeState } from '#stores/state';
5
+ import type { BlobStore } from '#stores/blobs';
6
+ import type { AnyConnector, ProviderManifest } from '#connectivity';
7
+ import type {
8
+ Config,
9
+ ConnectionConfig,
10
+ Resolution,
11
+ SelectedConnection,
12
+ TargetConfig,
13
+ } from '#profile';
14
+ import type { ProviderRegistry } from '#registry';
15
+ import type { Dispatcher } from '#dispatch';
16
+ import type { VaultStore } from '#providers/owner.ts';
17
+ import type { KnowledgeStores } from '#deployments/knowledge.ts';
18
+
19
+ /**
20
+ * What a command is handed once a profile has been opened.
21
+ *
22
+ * Its own file so `open.ts` stays inside the size budget, and because this is
23
+ * the thing every command in the CLI is written against — a reader looking for
24
+ * "what do I get" should not have to read the assembly to find it.
25
+ */
26
+
27
+ export interface Runtime {
28
+ readonly resolution: Resolution;
29
+ readonly config: Config;
30
+ readonly target: string;
31
+ /**
32
+ * The target's adapter set, from the workspace that declares it.
33
+ *
34
+ * Here because the config no longer carries it. Every caller that used to
35
+ * reach `config.targets[target]` — for a `deploy` block, a storage adapter, a
36
+ * knowledge repository — reads this instead, and reads it without following
37
+ * the pointer a second time (ADR-052).
38
+ */
39
+ readonly declared: TargetConfig;
40
+ /**
41
+ * The accounts this profile selects, joined to the grants that govern them.
42
+ *
43
+ * The join is done once, here, because everything that asks — status, doctor,
44
+ * reconcile, setup — wants the same answer and three implementations of it is
45
+ * three chances to disagree (`profile/connections.ts`).
46
+ */
47
+ readonly connections: readonly SelectedConnection[];
48
+ /** Every account the workspace holds, granted or not. `connection list` reads this. */
49
+ readonly workspaceConnections: readonly ConnectionConfig[];
50
+ /** Vendors this workspace registered a client of its own for (`oauth_apps`). */
51
+ readonly ownClients: readonly string[];
52
+ readonly state: RuntimeState;
53
+ /** The durable log, for reading. Copies, if any, are write-only and not here. */
54
+ readonly audit: AuditReader;
55
+ readonly credentials: SecretStore;
56
+ readonly storage: BlobStore;
57
+ /**
58
+ * Where this profile's skills are kept — `data/skills.d/<connection>/` locally.
59
+ *
60
+ * `undefined` when the profile grants no `skills` connection. That is a real
61
+ * state rather than an empty one, and it is worth the optionality: the owner
62
+ * layer arrives granted (ADR-050), so a profile without it was denied it on
63
+ * purpose, and handing back an empty store would report "no skills" for a
64
+ * profile that is not allowed to have any — the same answer for two different
65
+ * facts.
66
+ */
67
+ readonly skills: BlobStore | undefined;
68
+ /**
69
+ * Present only when this target keeps memory and skills in a repository.
70
+ *
71
+ * Nothing on the dispatch path reads it: `storage` and `skills` above are
72
+ * already pointed at the right bytes, which is the whole design. It is here
73
+ * so `target show` and `doctor` can say where those bytes are without
74
+ * re-reading the config, and so the migration can reach the repository it is
75
+ * committing to.
76
+ */
77
+ readonly knowledge?: KnowledgeStores | undefined;
78
+ /** The vault's own store, so `lanes link vault` reaches the same bytes MCP does. */
79
+ readonly vault: VaultStore;
80
+ readonly registry: ProviderRegistry;
81
+ /**
82
+ * Re-read the skills into the registry when they have changed on the store.
83
+ *
84
+ * Cheap and idempotent — one `list()`, and a rebuild only when the listing
85
+ * differs from the last one. The caller decides how often to ask.
86
+ */
87
+ refreshSkills(): Promise<void>;
88
+ readonly dispatcher: Dispatcher;
89
+ readonly authenticator: BearerAuthenticator;
90
+ /** Same factory the dispatcher uses, exposed for commands that probe upstream. */
91
+ connectorFor(providerId: string, connectionId: string): AnyConnector | undefined;
92
+ /**
93
+ * Same authorizer the dispatcher uses, for the same reason `connectorFor` is here.
94
+ *
95
+ * A command that probes upstream needs the credential on the request, and it
96
+ * must not learn how to put it there — that is one switch on the resolved
97
+ * shape (`connectivity/auth/authorize.ts`) and a second copy would be a
98
+ * second answer. `settleIdentity` is the caller: it asks a provider whose
99
+ * account was just authorised, over whatever method that provider declares.
100
+ */
101
+ authorizeRequest(providerId: string, connectionId: string, request: Request): Promise<Request>;
102
+ /** A provider's manifest, so an omitted `credential_ref` can be derived from it. */
103
+ manifestFor(providerId: string): ProviderManifest | undefined;
104
+ close(): Promise<void>;
105
+ }
106
+
@@ -1,4 +1,5 @@
1
- import { layout, workspacePath } from '#profile';
1
+ import {
2
+ soleGrantFor, layout, vaultRef, workspacePath } from '#profile';
2
3
  import type { SecretStore } from '#secrets';
3
4
  import {
4
5
  createBlobVaultStore,
@@ -36,10 +37,15 @@ export function openVault(
36
37
  const { declared, config, root } = input;
37
38
  const vault = declared.vault ?? { adapter: 'file' as const };
38
39
 
40
+ // The vault connection this profile grants (ADR-059). `main` when it grants
41
+ // none, which keeps a profile that denied the vault opening against the same
42
+ // document every other profile uses rather than inventing a second one.
43
+ const connection = soleGrantFor(config, 'lanes_vault') ?? 'main';
44
+
39
45
  switch (vault.adapter) {
40
46
  case 'file':
41
47
  return createFileVaultStore({
42
- path: workspacePath(root, vault.path ?? layout.vault(config.instance.profile)),
48
+ path: workspacePath(root, vault.path ?? layout.vault(config.instance.profile, connection)),
43
49
  });
44
50
 
45
51
  case 'secret':
@@ -47,15 +53,24 @@ export function openVault(
47
53
  // so the credential store holds ciphertext it cannot read. Separate
48
54
  // document, separate key, separate environment variable — the backend
49
55
  // was never what kept the two stores apart. ADR-022.
56
+ //
57
+ // Named per connection, like the file adapter. A `ref` written by hand
58
+ // still wins, because a deployment that already seals under one name has
59
+ // to keep opening it — but the default carries the instance, so two
60
+ // profiles granting different vaults share nothing. Without this only
61
+ // `file` honoured ADR-059 and every deployed workspace, which uses this
62
+ // adapter or `blob`, had one document behind every vault connection:
63
+ // ADR-059 calls that the worst of the three collisions, because the wrong
64
+ // answer is a credential.
50
65
  return createSecretVaultStore({
51
66
  store: credentials,
52
- ...(vault.ref !== undefined ? { ref: vault.ref } : {}),
67
+ ref: vaultRef(declared, config),
53
68
  });
54
69
 
55
70
  case 'blob':
56
71
  return createBlobVaultStore({
57
72
  store: storage(),
58
- ...(vault.path !== undefined ? { key: vault.path } : {}),
73
+ key: vault.path ?? layout.vaultKey(connection),
59
74
  });
60
75
  }
61
76
  }
@@ -0,0 +1,60 @@
1
+ import { ConfigError, listProfiles, openTarget, resolveWorkspaceRoot } from '#profile';
2
+ import { openRuntime, type Runtime } from './open.ts';
3
+ import type { GlobalFlags } from './select.ts';
4
+
5
+ /**
6
+ * A runtime for a workspace, when the command does not act on one profile.
7
+ *
8
+ * Contract 3 moved the credential store, the audit log, the state store and the
9
+ * blob root up to the workspace, so a command that opens any of them opens the
10
+ * same thing whichever profile it was handed. `openRuntime` still wants one,
11
+ * because a `Runtime` carries a resolved profile — so this picks one rather
12
+ * than making the caller ask a question with no answer.
13
+ *
14
+ * `--profile` still narrows where narrowing means something: the audit log is
15
+ * one chain and the flag filters it, rather than selecting which log to read.
16
+ * What it must never do is *decide* anything, which is why the profile chosen
17
+ * here when none is given is simply the first and is never reported as though
18
+ * it were the subject.
19
+ */
20
+ export async function openWorkspaceRuntime(flags: GlobalFlags): Promise<Runtime> {
21
+ if (flags.profile !== undefined) return openRuntime(flags);
22
+
23
+ const root = resolveWorkspaceRoot();
24
+ const resolved = await openTarget(root, flags.target!);
25
+ const names = await listProfiles(resolved.workspaceRoot);
26
+
27
+ if (names.length === 0) {
28
+ throw new ConfigError(
29
+ `Workspace "${flags.target}" holds no profiles, so there is nothing to open.\n` +
30
+ ` Create one with: lanes link profile add <name> --workspace ${flags.target}`,
31
+ );
32
+ }
33
+
34
+ return openRuntime({ ...flags, profile: names[0]! });
35
+ }
36
+
37
+ /**
38
+ * Which profile a workspace-level command should act *as*.
39
+ *
40
+ * Distinct from `openWorkspaceRuntime` because the caller needs the name rather
41
+ * than a runtime — `start` puts it back into flags and hands them on. Same rule:
42
+ * `--profile` wins, and otherwise the first is taken without being reported as
43
+ * a choice, because for the things this is used for it is not one.
44
+ */
45
+ export async function primaryProfile(flags: GlobalFlags): Promise<string> {
46
+ if (flags.profile !== undefined) return flags.profile;
47
+
48
+ const root = resolveWorkspaceRoot();
49
+ const resolved = await openTarget(root, flags.target!);
50
+ const names = await listProfiles(resolved.workspaceRoot);
51
+
52
+ if (names.length === 0) {
53
+ throw new ConfigError(
54
+ `Workspace "${flags.target}" holds no profiles, so there is nothing to serve.\n` +
55
+ ` Create one with: lanes link profile add <name> --workspace ${flags.target}`,
56
+ );
57
+ }
58
+
59
+ return names[0]!;
60
+ }
@@ -32,4 +32,5 @@ export {
32
32
  type OwnerLayerOptions,
33
33
  } from './runtime/registry.ts';
34
34
 
35
- export { openRuntime, type OpenOptions, type Runtime } from './runtime/open.ts';
35
+ export { grantedConnections, openRuntime, type OpenOptions, type Runtime } from './runtime/open.ts';
36
+ export { openWorkspaceRuntime, primaryProfile } from './runtime/workspace.ts';