@lanes-sh/link 0.1.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 (276) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +144 -0
  3. package/bin/lanes +42 -0
  4. package/instructions/agents/lanes-link-scout.md +73 -0
  5. package/instructions/skills/lanes-link/SKILL.md +187 -0
  6. package/package.json +95 -0
  7. package/src/audit/chain.ts +278 -0
  8. package/src/audit/conformance.ts +374 -0
  9. package/src/audit/fanout.ts +97 -0
  10. package/src/audit/index.ts +218 -0
  11. package/src/audit/stdout.ts +60 -0
  12. package/src/auth/index.ts +220 -0
  13. package/src/auth/oauth/metadata.ts +75 -0
  14. package/src/auth/oauth/server.ts +385 -0
  15. package/src/auth/oauth/store.ts +215 -0
  16. package/src/auth/oidc.ts +206 -0
  17. package/src/auth/remote.ts +72 -0
  18. package/src/cli/argv.ts +94 -0
  19. package/src/cli/callback-page.ts +256 -0
  20. package/src/cli/commands/connect/accounts.ts +94 -0
  21. package/src/cli/commands/connect/authorise.ts +298 -0
  22. package/src/cli/commands/connect/client.ts +284 -0
  23. package/src/cli/commands/connect/index.ts +398 -0
  24. package/src/cli/commands/connect/outcome.ts +119 -0
  25. package/src/cli/commands/connect/requirements.ts +103 -0
  26. package/src/cli/commands/connect/scopes-gate.ts +146 -0
  27. package/src/cli/commands/connect/settle.ts +136 -0
  28. package/src/cli/commands/connect/setup.ts +276 -0
  29. package/src/cli/commands/mcp/assets.ts +189 -0
  30. package/src/cli/commands/mcp/harnesses.ts +143 -0
  31. package/src/cli/commands/mcp/list.ts +70 -0
  32. package/src/cli/commands/mcp/register.ts +199 -0
  33. package/src/cli/commands/mcp/stdio.ts +57 -0
  34. package/src/cli/commands/mcp.ts +22 -0
  35. package/src/cli/commands/operate/attach.ts +121 -0
  36. package/src/cli/commands/operate/audit.ts +119 -0
  37. package/src/cli/commands/operate/inspect.ts +355 -0
  38. package/src/cli/commands/operate/outputs.ts +206 -0
  39. package/src/cli/commands/operate/policy.ts +80 -0
  40. package/src/cli/commands/operate/serve.ts +55 -0
  41. package/src/cli/commands/operate/status.ts +133 -0
  42. package/src/cli/commands/operate/token.ts +70 -0
  43. package/src/cli/commands/operate.ts +27 -0
  44. package/src/cli/commands/owner/memory.ts +110 -0
  45. package/src/cli/commands/owner/shared.ts +125 -0
  46. package/src/cli/commands/owner/skills.ts +92 -0
  47. package/src/cli/commands/owner/vault.ts +140 -0
  48. package/src/cli/commands/owner.ts +46 -0
  49. package/src/cli/commands/profile/removal.ts +278 -0
  50. package/src/cli/commands/profile/remove.ts +315 -0
  51. package/src/cli/commands/profile.ts +156 -0
  52. package/src/cli/commands/secrets.ts +176 -0
  53. package/src/cli/commands/setup.ts +150 -0
  54. package/src/cli/commands/target.ts +310 -0
  55. package/src/cli/config-edit.ts +397 -0
  56. package/src/cli/endpoint-url.ts +77 -0
  57. package/src/cli/identity.ts +109 -0
  58. package/src/cli/lanes.ts +78 -0
  59. package/src/cli/main.ts +333 -0
  60. package/src/cli/oauth-error.ts +13 -0
  61. package/src/cli/oauth-exchange.ts +146 -0
  62. package/src/cli/oauth.ts +354 -0
  63. package/src/cli/output.ts +184 -0
  64. package/src/cli/prompt.ts +180 -0
  65. package/src/cli/publish.ts +185 -0
  66. package/src/cli/runtime/discovery.ts +123 -0
  67. package/src/cli/runtime/open.ts +342 -0
  68. package/src/cli/runtime/registry.ts +185 -0
  69. package/src/cli/runtime/select.ts +124 -0
  70. package/src/cli/runtime.ts +34 -0
  71. package/src/cli/scopes.ts +63 -0
  72. package/src/cli/usage.ts +111 -0
  73. package/src/cli/version.ts +25 -0
  74. package/src/connectivity/auth/README.md +36 -0
  75. package/src/connectivity/auth/api-key/index.ts +43 -0
  76. package/src/connectivity/auth/authorize.ts +49 -0
  77. package/src/connectivity/auth/basic/index.ts +68 -0
  78. package/src/connectivity/auth/bearer/index.ts +13 -0
  79. package/src/connectivity/auth/credential.ts +19 -0
  80. package/src/connectivity/auth/header/index.ts +18 -0
  81. package/src/connectivity/auth/index.ts +35 -0
  82. package/src/connectivity/auth/none/index.ts +12 -0
  83. package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
  84. package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
  85. package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
  86. package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
  87. package/src/connectivity/auth/resolve.ts +61 -0
  88. package/src/connectivity/auth/strategy/index.ts +14 -0
  89. package/src/connectivity/capability.ts +164 -0
  90. package/src/connectivity/connector.ts +175 -0
  91. package/src/connectivity/context.ts +77 -0
  92. package/src/connectivity/index.ts +94 -0
  93. package/src/connectivity/mail/attachments.ts +368 -0
  94. package/src/connectivity/mail/compose.ts +73 -0
  95. package/src/connectivity/mail/index.ts +40 -0
  96. package/src/connectivity/mail/message.ts +82 -0
  97. package/src/connectivity/mail/nodemailer.d.ts +83 -0
  98. package/src/connectivity/mail/staging.ts +143 -0
  99. package/src/connectivity/mail/url.ts +301 -0
  100. package/src/connectivity/manifest/auth.ts +135 -0
  101. package/src/connectivity/manifest/bundles.ts +23 -0
  102. package/src/connectivity/manifest/connector.ts +168 -0
  103. package/src/connectivity/manifest/credential-ref.ts +73 -0
  104. package/src/connectivity/manifest/identity.ts +43 -0
  105. package/src/connectivity/manifest/index.ts +46 -0
  106. package/src/connectivity/manifest/primitives.ts +20 -0
  107. package/src/connectivity/manifest/provider.ts +208 -0
  108. package/src/connectivity/manifest/requirements.ts +146 -0
  109. package/src/connectivity/manifest/setup.ts +55 -0
  110. package/src/connectivity/provider.ts +163 -0
  111. package/src/connectivity/transports/README.md +33 -0
  112. package/src/connectivity/transports/composite/index.ts +68 -0
  113. package/src/connectivity/transports/dav/calendar.ts +217 -0
  114. package/src/connectivity/transports/dav/capabilities.ts +151 -0
  115. package/src/connectivity/transports/dav/client.ts +226 -0
  116. package/src/connectivity/transports/dav/contacts.ts +75 -0
  117. package/src/connectivity/transports/dav/ical.ts +412 -0
  118. package/src/connectivity/transports/dav/index.ts +143 -0
  119. package/src/connectivity/transports/dav/operations.ts +43 -0
  120. package/src/connectivity/transports/dav/request.ts +161 -0
  121. package/src/connectivity/transports/dav/xml.ts +123 -0
  122. package/src/connectivity/transports/factory.ts +181 -0
  123. package/src/connectivity/transports/fs/capabilities.ts +88 -0
  124. package/src/connectivity/transports/fs/commands.ts +258 -0
  125. package/src/connectivity/transports/fs/index.ts +121 -0
  126. package/src/connectivity/transports/fs/operations.ts +45 -0
  127. package/src/connectivity/transports/fs/paths.ts +120 -0
  128. package/src/connectivity/transports/fs/result.ts +12 -0
  129. package/src/connectivity/transports/http/index.ts +255 -0
  130. package/src/connectivity/transports/imap/attachment.ts +166 -0
  131. package/src/connectivity/transports/imap/capabilities.ts +158 -0
  132. package/src/connectivity/transports/imap/client.ts +398 -0
  133. package/src/connectivity/transports/imap/commands.ts +385 -0
  134. package/src/connectivity/transports/imap/index.ts +152 -0
  135. package/src/connectivity/transports/imap/operations.ts +64 -0
  136. package/src/connectivity/transports/imap/parse.ts +130 -0
  137. package/src/connectivity/transports/imap/parser.ts +272 -0
  138. package/src/connectivity/transports/imap/result.ts +15 -0
  139. package/src/connectivity/transports/imap/send.ts +92 -0
  140. package/src/connectivity/transports/imap/socket.ts +111 -0
  141. package/src/connectivity/transports/imap/utf7.ts +136 -0
  142. package/src/connectivity/transports/index.ts +20 -0
  143. package/src/connectivity/transports/local/index.ts +173 -0
  144. package/src/connectivity/transports/mcp/index.ts +215 -0
  145. package/src/deployments/README.md +63 -0
  146. package/src/deployments/adapters/audit-blob.ts +203 -0
  147. package/src/deployments/adapters/filesystem.ts +184 -0
  148. package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
  149. package/src/deployments/adapters/gcs.ts +191 -0
  150. package/src/deployments/adapters/otlp.ts +128 -0
  151. package/src/deployments/adapters/s3.ts +195 -0
  152. package/src/deployments/azure/README.md +21 -0
  153. package/src/deployments/bootstrap.ts +177 -0
  154. package/src/deployments/deploy.ts +290 -0
  155. package/src/deployments/driver.ts +157 -0
  156. package/src/deployments/drivers.ts +35 -0
  157. package/src/deployments/gcp/Dockerfile +70 -0
  158. package/src/deployments/gcp/cloudbuild.yaml +31 -0
  159. package/src/deployments/gcp/driver.ts +175 -0
  160. package/src/deployments/gcp/gcloud.ts +178 -0
  161. package/src/deployments/gcp/provision.ts +290 -0
  162. package/src/deployments/gcp/survey.ts +319 -0
  163. package/src/deployments/local/README.md +12 -0
  164. package/src/deployments/prepare.ts +257 -0
  165. package/src/deployments/steps.ts +137 -0
  166. package/src/deployments/target.ts +295 -0
  167. package/src/deployments/upload.ts +207 -0
  168. package/src/dispatch/context.ts +195 -0
  169. package/src/dispatch/dispatch.ts +350 -0
  170. package/src/dispatch/index.ts +32 -0
  171. package/src/dispatch/staging.ts +102 -0
  172. package/src/policy/index.ts +179 -0
  173. package/src/policy/limits.ts +77 -0
  174. package/src/profile/authorization.ts +81 -0
  175. package/src/profile/files.ts +71 -0
  176. package/src/profile/index.ts +76 -0
  177. package/src/profile/layout.ts +123 -0
  178. package/src/profile/load.ts +199 -0
  179. package/src/profile/primitives.ts +45 -0
  180. package/src/profile/schema.ts +347 -0
  181. package/src/profile/secret-detection.ts +162 -0
  182. package/src/profile/targets.ts +152 -0
  183. package/src/profile/workspace.ts +262 -0
  184. package/src/providers/custom/index.ts +21 -0
  185. package/src/providers/custom/load.ts +115 -0
  186. package/src/providers/custom/template.ts +156 -0
  187. package/src/providers/example/provider.ts +207 -0
  188. package/src/providers/google/calendar/index.ts +66 -0
  189. package/src/providers/google/calendar/redact.ts +40 -0
  190. package/src/providers/google/contacts/index.ts +50 -0
  191. package/src/providers/google/contacts/redact.ts +21 -0
  192. package/src/providers/google/docs/index.ts +45 -0
  193. package/src/providers/google/drive/hints.ts +28 -0
  194. package/src/providers/google/drive/index.ts +34 -0
  195. package/src/providers/google/drive/redact.ts +39 -0
  196. package/src/providers/google/drive-mcp/index.ts +21 -0
  197. package/src/providers/google/gmail/api.ts +42 -0
  198. package/src/providers/google/gmail/attachment.ts +142 -0
  199. package/src/providers/google/gmail/hints.ts +55 -0
  200. package/src/providers/google/gmail/index.ts +112 -0
  201. package/src/providers/google/gmail/redact.ts +56 -0
  202. package/src/providers/google/gmail/send.ts +365 -0
  203. package/src/providers/google/gmail-mcp/index.ts +35 -0
  204. package/src/providers/google/index.ts +10 -0
  205. package/src/providers/google/shared/oauth.ts +122 -0
  206. package/src/providers/google/shared/scopes.ts +99 -0
  207. package/src/providers/google/shared/setup.ts +80 -0
  208. package/src/providers/google/sheets/hints.ts +45 -0
  209. package/src/providers/google/sheets/index.ts +70 -0
  210. package/src/providers/google/sheets/redact.ts +45 -0
  211. package/src/providers/google/specs/calendar.v3.json +1829 -0
  212. package/src/providers/google/specs/docs.v1.json +381 -0
  213. package/src/providers/google/specs/drive.v3.json +2208 -0
  214. package/src/providers/google/specs/gmail.v1.json +2578 -0
  215. package/src/providers/google/specs/people.v1.json +506 -0
  216. package/src/providers/google/specs/sheets.v4.json +1269 -0
  217. package/src/providers/google/specs/tasks.v1.json +840 -0
  218. package/src/providers/google/specs/vendor.ts +661 -0
  219. package/src/providers/google/tasks/index.ts +53 -0
  220. package/src/providers/google/tasks/redact.ts +34 -0
  221. package/src/providers/harness.ts +95 -0
  222. package/src/providers/icloud/calendar/index.ts +27 -0
  223. package/src/providers/icloud/contacts/index.ts +17 -0
  224. package/src/providers/icloud/drive/index.ts +47 -0
  225. package/src/providers/icloud/index.ts +8 -0
  226. package/src/providers/icloud/mail/index.ts +37 -0
  227. package/src/providers/icloud/shared/setup.ts +66 -0
  228. package/src/providers/index.ts +93 -0
  229. package/src/providers/linear/index.ts +11 -0
  230. package/src/providers/linear/scopes.ts +7 -0
  231. package/src/providers/memory/provider.ts +429 -0
  232. package/src/providers/notion/index.ts +19 -0
  233. package/src/providers/owner.ts +49 -0
  234. package/src/providers/scopes.ts +26 -0
  235. package/src/providers/setup/plan.ts +141 -0
  236. package/src/providers/setup/provider.ts +323 -0
  237. package/src/providers/shared/frontmatter.ts +119 -0
  238. package/src/providers/skills/provider.ts +283 -0
  239. package/src/providers/skills/store.ts +252 -0
  240. package/src/providers/vault/provider.ts +194 -0
  241. package/src/registry/index.ts +36 -0
  242. package/src/registry/policy-bridge.ts +32 -0
  243. package/src/registry/reconcile.ts +313 -0
  244. package/src/registry/registry.ts +240 -0
  245. package/src/secrets/document.ts +293 -0
  246. package/src/secrets/index.ts +154 -0
  247. package/src/secrets/system.ts +173 -0
  248. package/src/secrets/vault.ts +336 -0
  249. package/src/server/attachments.ts +197 -0
  250. package/src/server/container.ts +96 -0
  251. package/src/server/edge.ts +53 -0
  252. package/src/server/endpoint.ts +352 -0
  253. package/src/server/generations.ts +362 -0
  254. package/src/server/harness.ts +400 -0
  255. package/src/server/index.ts +331 -0
  256. package/src/server/logging.ts +41 -0
  257. package/src/server/mcp/build.ts +68 -0
  258. package/src/server/mcp/icon.ts +145 -0
  259. package/src/server/mcp/index.ts +32 -0
  260. package/src/server/mcp/instructions.ts +245 -0
  261. package/src/server/mcp/naming.ts +39 -0
  262. package/src/server/mcp/prompts.ts +78 -0
  263. package/src/server/mcp/resources.ts +106 -0
  264. package/src/server/mcp/routing.ts +117 -0
  265. package/src/server/mcp/schema.ts +78 -0
  266. package/src/server/mcp/tools.ts +186 -0
  267. package/src/server/mcp/visibility.ts +132 -0
  268. package/src/server/oauth.ts +222 -0
  269. package/src/server/rebinding.ts +53 -0
  270. package/src/server/stdio.ts +192 -0
  271. package/src/stores/blobs/conformance.ts +223 -0
  272. package/src/stores/blobs/index.ts +125 -0
  273. package/src/stores/blobs/testing.ts +49 -0
  274. package/src/stores/state/index.ts +247 -0
  275. package/src/stores/state/keys.ts +68 -0
  276. package/src/stores/state/testing.ts +41 -0
@@ -0,0 +1,342 @@
1
+ import { BearerAuthenticator, ownerPrincipal } 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 { RateLimiter, allowedConnections } from '#policy';
8
+ import {
9
+ ConfigError,
10
+ layout,
11
+ listProfiles,
12
+ workspacePath,
13
+ type Config,
14
+ type Resolution,
15
+ } from '#profile';
16
+ import { ProviderRegistry, toPolicyDocument } from '#registry';
17
+ import { Dispatcher, createConsoleLogger } from '#dispatch';
18
+ import { PROVIDER_MANIFESTS } from '#providers/index.ts';
19
+ import {
20
+ createBlobVaultStore,
21
+ createFileVaultStore,
22
+ createSecretVaultStore,
23
+ type VaultStore,
24
+ } from '#providers/owner.ts';
25
+ import {
26
+ openAuditSinks,
27
+ openState,
28
+ openSecrets,
29
+ openStorage,
30
+ type StorageFactory,
31
+ type TargetInput,
32
+ } from '#deployments/target.ts';
33
+ import { connectorFactory } from '#connectivity/transports';
34
+ import { requestAuthorizer } from '#connectivity/auth/index.ts';
35
+ import { resolveProfile, type GlobalFlags } from './select.ts';
36
+ import { buildRegistryWithWorkspace, reloadSkills, skillFingerprint } from './registry.ts';
37
+ import { discoveryProbe } from './discovery.ts';
38
+
39
+ /**
40
+ * Assembling a profile's runtime from its declared target.
41
+ *
42
+ * The same config can run in more than one place; a target names an adapter
43
+ * set. Connections, providers, policy, and limits are declared once and apply
44
+ * to every target — only the adapters differ, which is what lets M2 change
45
+ * nothing at the application layer.
46
+ */
47
+
48
+ export interface Runtime {
49
+ readonly resolution: Resolution;
50
+ readonly config: Config;
51
+ readonly target: 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
+ /** Where skills are kept — `data/<profile>/skills.d/` locally. */
58
+ readonly skills: BlobStore;
59
+ /** The vault's own store, so `lanes link vault` reaches the same bytes MCP does. */
60
+ readonly vault: VaultStore;
61
+ readonly registry: ProviderRegistry;
62
+ /**
63
+ * Re-read the skills into the registry when they have changed on the store.
64
+ *
65
+ * Cheap and idempotent — one `list()`, and a rebuild only when the listing
66
+ * differs from the last one. The caller decides how often to ask.
67
+ */
68
+ refreshSkills(): Promise<void>;
69
+ readonly dispatcher: Dispatcher;
70
+ readonly authenticator: BearerAuthenticator;
71
+ /** Same factory the dispatcher uses, exposed for commands that probe upstream. */
72
+ connectorFor(providerId: string, connectionId: string): AnyConnector | undefined;
73
+ /** A provider's manifest, so an omitted `credential_ref` can be derived from it. */
74
+ manifestFor(providerId: string): ProviderManifest | undefined;
75
+ close(): Promise<void>;
76
+ }
77
+
78
+ /**
79
+ * Where this profile's skills live, in either target.
80
+ *
81
+ * Locally `data/<profile>/skills.d/`; deployed, the same key under the bucket
82
+ * prefix. Going through the store rather than a filesystem path is what gives a
83
+ * deployment skills at all — a path is baked into a container image at build
84
+ * time and an object key is not, so before ADR-014 a deployed instance could
85
+ * only ever serve the skills that existed when its image was built.
86
+ *
87
+ * **Per profile**, which reverses ADR-012 §1. Policy gating `skills.<name>`
88
+ * was the whole isolation story while the bytes were shared, and it is a weak
89
+ * one: it decides who may *run* a procedure, not who may read that it exists or
90
+ * what it says. A skill written for work names work's accounts and work's
91
+ * people. ADR-030.
92
+ *
93
+ * An explicit area, matching `openState` and `openAudit` — a profile that
94
+ * declares its own `storage.path` moves its provider blobs, not the reserved
95
+ * roots beside them.
96
+ */
97
+ function skillStore(storage: StorageFactory, profile: string): BlobStore {
98
+ return storage(layout.skills(profile));
99
+ }
100
+
101
+ /**
102
+ * The vault's encrypted document, wherever this target keeps it.
103
+ *
104
+ * Defaults to `file`, so a profile written before ADR-014 needs no change and a
105
+ * local run needs no vault configuration at all. `blob` exists because the file
106
+ * adapter was unconditional before: a deployed instance wrote its vault to a
107
+ * container filesystem, and every item in it was discarded by the next
108
+ * revision without an error to say so.
109
+ */
110
+ function openVault(
111
+ input: TargetInput,
112
+ storage: StorageFactory,
113
+ credentials: SecretStore,
114
+ ): VaultStore {
115
+ const { declared, config, root } = input;
116
+ const vault = declared.vault ?? { adapter: 'file' as const };
117
+
118
+ switch (vault.adapter) {
119
+ case 'file':
120
+ return createFileVaultStore({
121
+ path: workspacePath(root, vault.path ?? layout.vault(config.instance.profile)),
122
+ });
123
+
124
+ case 'secret':
125
+ // The document is sealed under LANES_LINK_VAULT_KEY before it gets here,
126
+ // so the credential store holds ciphertext it cannot read. Separate
127
+ // document, separate key, separate environment variable — the backend
128
+ // was never what kept the two stores apart. ADR-022.
129
+ return createSecretVaultStore({
130
+ store: credentials,
131
+ ...(vault.ref !== undefined ? { ref: vault.ref } : {}),
132
+ });
133
+
134
+ case 'blob':
135
+ return createBlobVaultStore({
136
+ store: storage(),
137
+ ...(vault.path !== undefined ? { key: vault.path } : {}),
138
+ });
139
+ }
140
+ }
141
+
142
+ export async function openRuntime(flags: GlobalFlags): Promise<Runtime> {
143
+ const { resolution, config, target } = await resolveProfile(flags);
144
+ const declared = config.targets[target];
145
+ if (!declared) throw new ConfigError(`Target "${target}" is not declared`);
146
+
147
+ const root = resolution.workspaceRoot;
148
+ const adapters: TargetInput = { declared, config, root, target };
149
+
150
+ // Credentials first: an S3 key pair is itself a credential reference, so the
151
+ // credential store has to exist before the blob store that names it. State
152
+ // and the log then ride that blob store rather than opening backends of
153
+ // their own, which is why there is no migrate step here any more — there is
154
+ // no schema left to migrate.
155
+ const credentials = await openSecrets(adapters);
156
+ const storageFor = await openStorage(adapters, credentials);
157
+ const storage = storageFor();
158
+ const state = openState(storageFor, config.instance.profile);
159
+
160
+ // The durable log, plus any copies the target declares. `sink` is what
161
+ // dispatch writes to; `audit` is what `tail` and `verify` read, and those are
162
+ // not the same object once a fan-out exists — a copy is not the log.
163
+ const logger = createConsoleLogger(flags.quiet ? 'error' : 'info');
164
+ const { sink: auditSink, reader: audit } = await openAuditSinks(
165
+ adapters,
166
+ storageFor,
167
+ credentials,
168
+ (message) => logger.warn(message),
169
+ );
170
+
171
+ const skills = skillStore(storageFor, config.instance.profile);
172
+
173
+ // The vault's own store, beside the credential store and never it: a separate
174
+ // document, a separate key, and a separate environment variable
175
+ // (`LANES_LINK_VAULT_KEY`). One master secret reused across purposes turns any
176
+ // single compromise into a total one — `docs/detailed/security.md`, and the boundary
177
+ // test that has existed since M1.
178
+ //
179
+ // Opened before the registry because each stored item becomes its own
180
+ // `vault.get.<id>` capability, which is what makes per-item policy
181
+ // expressible; an item written since the last start is therefore not readable
182
+ // until the next one, deliberately (ADR-012 §3).
183
+ const vaultStore = openVault(adapters, storageFor, credentials);
184
+
185
+ // `refreshSkills` is declared before the registry it mutates because the
186
+ // provider has to be handed it at construction: a `skills.manage.write` says
187
+ // that something changed, and this is what that means. It is only ever
188
+ // *called* after the registry exists.
189
+ let registry: ProviderRegistry;
190
+ let fingerprint = '';
191
+ const refreshSkills = async (): Promise<void> => {
192
+ fingerprint = await reloadSkills(registry, skills, fingerprint, refreshSkills);
193
+ };
194
+
195
+ // Computed before the registry because the setup surface is handed a way to
196
+ // ask what this principal may reach — through the *same* `allowedConnections`
197
+ // the dispatcher enforces with and `mergeCapabilities` builds the connection
198
+ // enum from. Computing visibility a second way is how discovery and
199
+ // enforcement drift, and a leak in discovery is still a leak.
200
+ const policy = toPolicyDocument(config);
201
+ // The owner, explicitly, rather than assumed: when a delegated principal
202
+ // arrives this becomes the seam, and `reachable` grows a parameter instead of
203
+ // being rewritten.
204
+ const principal = ownerPrincipal(config.instance.profile).id;
205
+
206
+ // Lazy for the same reason `refreshSkills` is: it reads the registry it is
207
+ // registered into. Per call rather than a snapshot, so a policy the runtime
208
+ // was opened with is re-evaluated rather than remembered.
209
+ const reachable = (): ReadonlyArray<{ key: string; provider: string; account: string }> =>
210
+ config.connections
211
+ .filter((connection) =>
212
+ registry
213
+ .capabilities()
214
+ .some(
215
+ ({ id }) =>
216
+ id.startsWith(`${connection.provider}.`) &&
217
+ allowedConnections(
218
+ id,
219
+ [`${connection.provider}.${connection.id}`],
220
+ principal,
221
+ policy,
222
+ ).length > 0,
223
+ ),
224
+ )
225
+ .map((connection) => ({
226
+ key: `${connection.provider}.${connection.id}`,
227
+ provider: connection.provider,
228
+ account: connection.account,
229
+ }));
230
+
231
+ registry = await buildRegistryWithWorkspace(root, config.instance.profile, {
232
+ skillStore: skills,
233
+ onSkillsChanged: refreshSkills,
234
+ vault: { store: vaultStore, items: await vaultStore.ids() },
235
+ setup: {
236
+ profile: config.instance.profile,
237
+ profiles: await listProfiles(root),
238
+ catalogue: PROVIDER_MANIFESTS,
239
+ ownClients: Object.keys(config.oauth_apps),
240
+ reachable,
241
+ },
242
+ });
243
+ // Seeded from the build that just happened, so the first refresh compares
244
+ // against what is registered rather than rebuilding once to find out.
245
+ fingerprint = await skillFingerprint(skills);
246
+
247
+ // Discovered capabilities never come from a live call on the dispatch path —
248
+ // that is what keeps the server stateless. But "not live" is not the same as
249
+ // "cached", and conflating the two was a bug: `connect` was the only writer of
250
+ // this cache, so an operator who upgraded without re-authorising kept whatever
251
+ // their last consent screen happened to discover. Drive shipped nine
252
+ // operations and served six; Gmail served a `drafts.create` this repository
253
+ // had deleted, because the spec is read by the tests and the cache is read by
254
+ // the endpoint.
255
+ //
256
+ // So: derive it where deriving is free, and cache it only where it is not. An
257
+ // `http` provider's capabilities are a pure function of a document committed
258
+ // here — reviewed in a diff, not fetched from a vendor — which is the property
259
+ // that makes re-deriving safe as well as cheap.
260
+ for (const entry of registry.list()) {
261
+ if (entry.manifest.connector.kind === 'local') continue;
262
+
263
+ const probe = discoveryProbe(entry.manifest);
264
+ if (probe?.cost === 'offline') {
265
+ try {
266
+ registry.setDiscovered(entry.manifest.id, await probe.run());
267
+ continue;
268
+ } catch {
269
+ // A malformed committed spec is a build problem, not a reason to refuse
270
+ // to start — fall through to whatever the cache last held.
271
+ }
272
+ }
273
+
274
+ const cached = await state.kv.get('discovery', entry.manifest.id);
275
+ if (cached) {
276
+ try {
277
+ registry.setDiscovered(entry.manifest.id, JSON.parse(cached));
278
+ } catch {
279
+ // A corrupt cache entry means "not discovered yet", which `plan`
280
+ // reports and `connect` fixes — never a reason to fail startup.
281
+ }
282
+ }
283
+ }
284
+
285
+ // One factory for the whole runtime, so its cache actually holds. The
286
+ // dispatcher and the CLI share it deliberately: a stateful connector must be
287
+ // the *same instance* whichever side asks for it, or a held session is held
288
+ // twice.
289
+ const connectorFor = connectorFactory({ registry, credentials });
290
+ let closed = false;
291
+
292
+ const dispatcher = new Dispatcher({
293
+ config,
294
+ registry,
295
+ connectorFor,
296
+ authorizeRequest: requestAuthorizer(registry, credentials),
297
+ policy,
298
+ state,
299
+ audit: auditSink,
300
+ credentials,
301
+ storage,
302
+ limiter: new RateLimiter(),
303
+ log: logger,
304
+ });
305
+
306
+ return {
307
+ resolution,
308
+ config,
309
+ target,
310
+ state,
311
+ audit,
312
+ credentials,
313
+ storage,
314
+ skills,
315
+ vault: vaultStore,
316
+ registry,
317
+ refreshSkills,
318
+ dispatcher,
319
+ authenticator: new BearerAuthenticator({
320
+ profile: config.instance.profile,
321
+ tokenRef: config.auth.token_ref,
322
+ credentials,
323
+ }),
324
+ connectorFor,
325
+ manifestFor: (providerId: string) => registry.manifest(providerId),
326
+ // Sessions first, then the state: a connector may still want to log out
327
+ // cleanly, and LOGOUT is worth more than the microsecond it costs.
328
+ //
329
+ // Idempotent, because a command that finishes early closes explicitly and
330
+ // its `finally` then closes again — and closing a SQLite handle twice is an
331
+ // error that would surface as a crash after the work already succeeded.
332
+ close: async () => {
333
+ if (closed) return;
334
+ closed = true;
335
+ await connectorFor.closeAll();
336
+ // Closing the log is not releasing a handle: it writes the marker that
337
+ // lets `audit verify` tell a run that ended from one whose tail was cut
338
+ // off. A crash leaves the run open, which is the honest outcome.
339
+ await auditSink.close();
340
+ },
341
+ };
342
+ }
@@ -0,0 +1,185 @@
1
+ import type { BlobStore } from '#stores/blobs';
2
+ import type { ProviderDefinition } from '#connectivity';
3
+ import { ConfigError } from '#profile';
4
+ import { ProviderRegistry } from '#registry';
5
+ import { loadProfileProviders } from '#providers/custom/index.ts';
6
+ import { loadProfileSkills, type LoadedSkill } from '#providers/skills/store.ts';
7
+ import { exampleProvider } from '#providers/example/provider.ts';
8
+ import {
9
+ createMemoryVaultStore,
10
+ createSetupProvider,
11
+ createSkillsProvider,
12
+ createVaultProvider,
13
+ memoryProvider,
14
+ type SetupProviderOptions,
15
+ type VaultStore,
16
+ } from '#providers/owner.ts';
17
+ import { PROVIDERS, PROVIDER_MANIFESTS } from '#providers/index.ts';
18
+
19
+ /**
20
+ * Which providers exist, and keeping the skills among them current.
21
+ *
22
+ * Separate from `./open.ts` because building a registry is synchronous and
23
+ * needs no adapters: `lanes link deploy` reads manifests and never opens a database.
24
+ */
25
+
26
+ /** What the owner layer needs from its store before it can be built. */
27
+ export interface OwnerLayerOptions {
28
+ /** Skills already loaded. Each becomes an MCP prompt. */
29
+ readonly skills?: readonly LoadedSkill[];
30
+ /**
31
+ * Where to load them from, and where authoring writes.
32
+ *
33
+ * Its presence is what gives the provider its `skills.manage.*` half: a
34
+ * registry built without one — `lanes link deploy` reads manifests and nothing else —
35
+ * gets the prompts and no way to author them, rather than four capabilities
36
+ * that would fail.
37
+ */
38
+ readonly skillStore?: BlobStore;
39
+ /** Called after a skill is written or removed, so the registry can catch up. */
40
+ readonly onSkillsChanged?: () => Promise<void>;
41
+ /**
42
+ * The vault store, and the items already in it.
43
+ *
44
+ * Both are resolved by the caller because opening an encrypted file is
45
+ * asynchronous and building a registry is not. The item list has to be known
46
+ * *here*: each item is its own `vault.get.<id>` capability, which is what
47
+ * makes per-item policy expressible without teaching the policy engine about
48
+ * arguments (ADR-012 §3).
49
+ */
50
+ readonly vault?: {
51
+ readonly store: VaultStore;
52
+ readonly items: ReadonlyArray<{ id: string; description?: string }>;
53
+ };
54
+ /**
55
+ * What the read-only setup surface describes.
56
+ *
57
+ * `reachable` is a function evaluated per call, and the caller supplies it
58
+ * because filtering it is a policy decision: `#providers` may not import
59
+ * `#policy`, and computing visibility anywhere but through
60
+ * `allowedConnections` is how discovery and enforcement drift apart.
61
+ *
62
+ * Absent for a registry built to read manifests — `lanes link deploy` — which
63
+ * then gets a surface describing the catalogue and no accounts, rather than
64
+ * two capabilities that would report an empty profile as the truth.
65
+ */
66
+ readonly setup?: SetupProviderOptions;
67
+ }
68
+
69
+ /**
70
+ * Built-in providers: the local reference, the owner layer, plus the manifests.
71
+ *
72
+ * Statically imported for now; the registry does not care where a manifest came
73
+ * from, which is what lets workspace YAML register alongside these.
74
+ *
75
+ * `allowReserved` is what admits `memory`, `skills`, and `vault`. The guard
76
+ * stays rather than being retired: it exists so a *third-party* provider cannot
77
+ * claim a namespace whose policy rules would then silently mean something else,
78
+ * and that reason survives the owner layer shipping. Only this one construction
79
+ * site opts in.
80
+ */
81
+ export function buildRegistry(owner: OwnerLayerOptions = {}): ProviderRegistry {
82
+ const registry = new ProviderRegistry({ allowReserved: true });
83
+
84
+ registry.register(exampleProvider);
85
+ registry.register(memoryProvider);
86
+ registry.register(skillsProviderFor(owner));
87
+ registry.register(
88
+ createVaultProvider({
89
+ // No store means no reads to register; `vault.put` still works, and the
90
+ // items it writes become readable at the next start.
91
+ store: owner.vault?.store ?? createMemoryVaultStore(),
92
+ items: owner.vault?.items ?? [],
93
+ }),
94
+ );
95
+
96
+ registry.register(
97
+ createSetupProvider(
98
+ owner.setup ?? { profile: '', catalogue: PROVIDER_MANIFESTS },
99
+ ),
100
+ );
101
+
102
+ for (const manifest of PROVIDERS) registry.register(manifest);
103
+ return registry;
104
+ }
105
+
106
+ /**
107
+ * Built-ins plus whatever the operator has dropped into this profile.
108
+ *
109
+ * A manifest registering under a built-in id is refused rather than silently
110
+ * overriding it — an operator shadowing `gmail` by accident would be very hard
111
+ * to diagnose from the outside.
112
+ *
113
+ * The profile is a parameter rather than read from a config here because two of
114
+ * the three callers do not have one: `deploy` walks every profile in turn, and
115
+ * `profile remove` is holding the name of the one being deleted. Passing it
116
+ * makes "which profile's manifests" a question the caller has already answered.
117
+ */
118
+ export async function buildRegistryWithWorkspace(
119
+ root: string,
120
+ profile: string,
121
+ owner: OwnerLayerOptions = {},
122
+ ): Promise<ProviderRegistry> {
123
+ const skills =
124
+ owner.skills ?? (owner.skillStore ? await loadProfileSkills(owner.skillStore) : []);
125
+ const registry = buildRegistry({ ...owner, skills });
126
+
127
+ for (const { manifest, path } of await loadProfileProviders(root, profile)) {
128
+ if (registry.has(manifest.id)) {
129
+ throw new ConfigError(
130
+ `${path}: provider "${manifest.id}" is already built in. Rename it, or remove the file to use the built-in.`,
131
+ );
132
+ }
133
+ registry.register(manifest, 'workspace');
134
+ }
135
+
136
+ return registry;
137
+ }
138
+
139
+ function skillsProviderFor(owner: OwnerLayerOptions): ProviderDefinition {
140
+ return createSkillsProvider({
141
+ skills: owner.skills ?? [],
142
+ ...(owner.skillStore ? { store: owner.skillStore } : {}),
143
+ ...(owner.onSkillsChanged ? { onChange: owner.onSkillsChanged } : {}),
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Re-read the skills and swap them into the registry, if anything changed.
149
+ *
150
+ * Each skill is its own capability, so both halves of ADR-014's promise need
151
+ * this: a skill written over MCP has to appear without a restart, and so does
152
+ * one written by `lanes link skills add` in another terminal while the endpoint runs.
153
+ *
154
+ * The listing is the change check as well as the source. `BlobMetadata` carries
155
+ * size and mtime, so a fingerprint costs one `list()` and no reads — which
156
+ * matters on S3, where the alternative is fetching every skill on every poll.
157
+ *
158
+ * Returns the fingerprint it settled on, so the caller can decide when to ask
159
+ * again rather than this deciding for it.
160
+ */
161
+ export async function reloadSkills(
162
+ registry: ProviderRegistry,
163
+ store: BlobStore,
164
+ previous: string,
165
+ onChange: () => Promise<void>,
166
+ ): Promise<string> {
167
+ const fingerprint = await skillFingerprint(store);
168
+ if (fingerprint === previous) return previous;
169
+
170
+ registry.replace(
171
+ skillsProviderFor({
172
+ skills: await loadProfileSkills(store),
173
+ skillStore: store,
174
+ onSkillsChanged: onChange,
175
+ }),
176
+ );
177
+ return fingerprint;
178
+ }
179
+
180
+ export async function skillFingerprint(store: BlobStore): Promise<string> {
181
+ return (await store.list())
182
+ .map((blob) => `${blob.key}:${blob.size}:${blob.modifiedAt.getTime()}`)
183
+ .sort()
184
+ .join('\n');
185
+ }
@@ -0,0 +1,124 @@
1
+ import { generateProfileToken, ownerPrincipal } from '#auth';
2
+ import type { SecretStore } from '#secrets';
3
+ import type { BlobStore } from '#stores/blobs';
4
+ import {
5
+ loadProfileConfig,
6
+ resolveSelection,
7
+ resolveTarget,
8
+ undeclaredTarget,
9
+ type Config,
10
+ type Resolution,
11
+ } from '#profile';
12
+ import { openSecrets, openStorage } from '#deployments/target.ts';
13
+
14
+ /**
15
+ * Which profile and target a command acts on, and the credentials that go with
16
+ * them — everything that can be settled without opening a database.
17
+ *
18
+ * Kept apart from `./open.ts` because two callers need exactly this much and
19
+ * nothing more: `secrets push` holds two targets open at once, and `deploy`
20
+ * checks a cloud store only reachable from inside Google's
21
+ * network. A full runtime would fail on the part neither of them uses.
22
+ */
23
+
24
+ export interface GlobalFlags {
25
+ readonly profile?: string | undefined;
26
+ readonly target?: string | undefined;
27
+ readonly quiet?: boolean;
28
+ }
29
+
30
+ export async function resolveProfile(
31
+ flags: GlobalFlags,
32
+ options: {
33
+ /** `deploy` creates the target it is given; see `resolveTarget`. */
34
+ allowUndeclaredTarget?: boolean;
35
+ /** Injected by tests. Both resolutions must read the same one. */
36
+ env?: Record<string, string | undefined>;
37
+ } = {},
38
+ ): Promise<{
39
+ resolution: Resolution;
40
+ config: Config;
41
+ target: string;
42
+ }> {
43
+ // Spread rather than assigned: `exactOptionalPropertyTypes` makes an explicit
44
+ // `env: undefined` a different type from an absent one, and the absent one is
45
+ // what means "read the real environment".
46
+ const env = options.env !== undefined ? { env: options.env } : {};
47
+
48
+ const selection = await resolveSelection({
49
+ profileFlag: flags.profile,
50
+ targetFlag: flags.target,
51
+ ...env,
52
+ });
53
+
54
+ const { config } = await loadProfileConfig(selection.workspaceRoot, selection.profile);
55
+ const { target, source } = resolveTarget(config, flags.target, {
56
+ allowUndeclared: options.allowUndeclaredTarget === true,
57
+ ...env,
58
+ });
59
+
60
+ return {
61
+ resolution: { ...selection, target, targetSource: source },
62
+ config,
63
+ target,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * One target's secret store, without opening its database.
69
+ *
70
+ * `secrets push` holds two targets open at once and touches neither state,
71
+ * and `deploy` checks the cloud store while its Postgres is only reachable from
72
+ * inside Google's network. Opening a full runtime for either would fail on the
73
+ * part that is not needed.
74
+ */
75
+ export async function openSecretStoreFor(
76
+ config: Config,
77
+ root: string,
78
+ target: string,
79
+ ): Promise<SecretStore> {
80
+ const declared = config.targets[target];
81
+ if (!declared) throw undeclaredTarget(target, config);
82
+ return openSecrets({ declared, config, root, target });
83
+ }
84
+
85
+ /**
86
+ * One target's blob store, for a caller with no use for a runtime.
87
+ *
88
+ * The sibling of `openSecretStoreFor` above, and here for the reason this file
89
+ * already gives: removal enumerates a target's objects and never dispatches a
90
+ * call, so a registry and a reconcile would only add parts that can fail.
91
+ *
92
+ * `area` reaches a root other than the profile's own. The default is the
93
+ * profile's blob tree; `profiles` is where a deployed revision reads its config
94
+ * from (ADR-023), which lives outside that tree and still belongs to the
95
+ * profile being removed.
96
+ */
97
+ export async function openBlobStoreFor(
98
+ config: Config,
99
+ root: string,
100
+ target: string,
101
+ area?: string,
102
+ ): Promise<BlobStore> {
103
+ const declared = config.targets[target];
104
+ if (!declared) throw undeclaredTarget(target, config);
105
+
106
+ const input = { declared, config, root, target };
107
+ const storage = await openStorage(input, await openSecrets(input));
108
+ return area === undefined ? storage() : storage(area);
109
+ }
110
+
111
+ /** Mint the profile token if it does not exist yet. Returns it either way. */
112
+ export async function ensureProfileToken(
113
+ credentials: SecretStore,
114
+ tokenRef: string,
115
+ ): Promise<{ token: string; created: boolean }> {
116
+ const existing = await credentials.get(tokenRef);
117
+ if (existing) return { token: existing, created: false };
118
+
119
+ const token = generateProfileToken();
120
+ await credentials.set(tokenRef, token);
121
+ return { token, created: true };
122
+ }
123
+
124
+ export { ownerPrincipal };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The CLI's composition root — what a command gets handed before it runs.
3
+ *
4
+ * Three files, split along what each caller actually needs:
5
+ *
6
+ * select.ts which profile and target, and that target's secrets — the
7
+ * part that opens no database
8
+ * registry.ts which providers exist, and keeping the skills among them
9
+ * current; synchronous, and needs no adapters
10
+ * open.ts the whole runtime: database, storage, vault, dispatcher
11
+ *
12
+ * The seam is not cosmetic. `secrets push` holds two targets open at once and
13
+ * `deploy` reads manifests against a bucket it cannot reach, so both stop at
14
+ * `select.ts` and would fail on what `open.ts` does. This file stays a barrel
15
+ * because `#cli/runtime.ts` is the spelling `#server` and `#deployments` bind,
16
+ * and two test files import it directly.
17
+ */
18
+
19
+ export {
20
+ ensureProfileToken,
21
+ openBlobStoreFor,
22
+ openSecretStoreFor,
23
+ ownerPrincipal,
24
+ resolveProfile,
25
+ type GlobalFlags,
26
+ } from './runtime/select.ts';
27
+
28
+ export {
29
+ buildRegistry,
30
+ buildRegistryWithWorkspace,
31
+ type OwnerLayerOptions,
32
+ } from './runtime/registry.ts';
33
+
34
+ export { openRuntime, type Runtime } from './runtime/open.ts';