@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,136 @@
1
+ /**
2
+ * Modified UTF-7, the encoding IMAP mailbox names arrive in (RFC 3501 §5.1.3).
3
+ *
4
+ * A mailbox called `Wichtig` is fine on the wire; one called `Wichtig ✓` is
5
+ * `Wichtig &Ivg-`, and a Japanese folder is unreadable without this. Getting it
6
+ * wrong is not subtle in one direction and dangerously subtle in the other:
7
+ * skipping *decode* shows an agent a folder name nobody recognises, while
8
+ * skipping *encode* sends a `SELECT` for a mailbox the server does not have, and
9
+ * a `SELECT` that fails looks exactly like a mailbox that is empty.
10
+ *
11
+ * It is UTF-7 with two changes: `+` becomes `&` (because `+` is common in
12
+ * mailbox names), and `/` becomes `,` (because `/` is the usual hierarchy
13
+ * delimiter). Padding is omitted.
14
+ */
15
+
16
+ const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,';
17
+
18
+ /** Wire form → the name a person would recognise. */
19
+ export function decodeMailboxName(encoded: string): string {
20
+ let out = '';
21
+ let index = 0;
22
+
23
+ while (index < encoded.length) {
24
+ const amp = encoded.indexOf('&', index);
25
+ if (amp === -1) return out + encoded.slice(index);
26
+
27
+ out += encoded.slice(index, amp);
28
+
29
+ const end = encoded.indexOf('-', amp + 1);
30
+ if (end === -1) {
31
+ // Unterminated: not legal, and not worth throwing over — a mailbox list
32
+ // is not the place to fail a whole call. Pass the rest through as-is.
33
+ return out + encoded.slice(amp);
34
+ }
35
+
36
+ const chunk = encoded.slice(amp + 1, end);
37
+ // `&-` is how a literal ampersand is written.
38
+ out += chunk === '' ? '&' : fromModifiedBase64(chunk);
39
+ index = end + 1;
40
+ }
41
+
42
+ return out;
43
+ }
44
+
45
+ /** The name a person typed → wire form. */
46
+ export function encodeMailboxName(name: string): string {
47
+ let out = '';
48
+ let run = '';
49
+
50
+ const flush = (): void => {
51
+ if (run === '') return;
52
+ out += `&${toModifiedBase64(run)}-`;
53
+ run = '';
54
+ };
55
+
56
+ for (const character of name) {
57
+ const code = character.codePointAt(0)!;
58
+
59
+ if (character === '&') {
60
+ flush();
61
+ out += '&-';
62
+ continue;
63
+ }
64
+
65
+ // Printable US-ASCII represents itself; everything else goes to base64.
66
+ if (code >= 0x20 && code <= 0x7e) {
67
+ flush();
68
+ out += character;
69
+ continue;
70
+ }
71
+
72
+ run += character;
73
+ }
74
+
75
+ flush();
76
+ return out;
77
+ }
78
+
79
+ /** Modified base64 → text, by way of the UTF-16BE the bytes represent. */
80
+ function fromModifiedBase64(chunk: string): string {
81
+ let bits = 0;
82
+ let width = 0;
83
+ const units: number[] = [];
84
+ let pending = 0;
85
+ let pendingBits = 0;
86
+
87
+ for (const character of chunk) {
88
+ const value = BASE64.indexOf(character);
89
+ if (value === -1) continue; // Not representable; skipping beats throwing.
90
+
91
+ bits = (bits << 6) | value;
92
+ width += 6;
93
+
94
+ while (width >= 8) {
95
+ width -= 8;
96
+ const byte = (bits >> width) & 0xff;
97
+ pending = (pending << 8) | byte;
98
+ pendingBits += 8;
99
+
100
+ if (pendingBits === 16) {
101
+ units.push(pending & 0xffff);
102
+ pending = 0;
103
+ pendingBits = 0;
104
+ }
105
+ }
106
+ }
107
+
108
+ // Surrogate pairs survive because they are two code units and this rebuilds
109
+ // code units, not code points.
110
+ return String.fromCharCode(...units);
111
+ }
112
+
113
+ /** Text → modified base64 of its UTF-16BE bytes. */
114
+ function toModifiedBase64(text: string): string {
115
+ const bytes: number[] = [];
116
+ for (let index = 0; index < text.length; index++) {
117
+ const unit = text.charCodeAt(index);
118
+ bytes.push((unit >> 8) & 0xff, unit & 0xff);
119
+ }
120
+
121
+ let out = '';
122
+ let bits = 0;
123
+ let width = 0;
124
+
125
+ for (const byte of bytes) {
126
+ bits = (bits << 8) | byte;
127
+ width += 8;
128
+ while (width >= 6) {
129
+ width -= 6;
130
+ out += BASE64[(bits >> width) & 0x3f];
131
+ }
132
+ }
133
+
134
+ if (width > 0) out += BASE64[(bits << (6 - width)) & 0x3f];
135
+ return out;
136
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Connectivity types — one folder per way of reaching a service.
3
+ *
4
+ * See `README.md` for the table mapping each folder to the `connector.kind` an
5
+ * operator writes, and for the rule that keeps vendor names out of all of them.
6
+ */
7
+
8
+ export { createLocalConnector } from './local/index.ts';
9
+ export { createMcpConnector, inferBundle, type McpConnectorOptions } from './mcp/index.ts';
10
+ export {
11
+ createHttpConnector,
12
+ bundleForMethod,
13
+ globMatches,
14
+ type HttpConnectorOptions,
15
+ } from './http/index.ts';
16
+ export { createImapConnector, type ImapConnectorOptions } from './imap/index.ts';
17
+ export { createDavConnector, type DavConnectorOptions } from './dav/index.ts';
18
+ export { createFsConnector, type FsConnectorOptions } from './fs/index.ts';
19
+ export { type ImapCredential } from './imap/client.ts';
20
+ export { connectorFactory, type ConnectorFactory, type ConnectorFactoryOptions } from './factory.ts';
@@ -0,0 +1,173 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ isPrompt,
4
+ isResource,
5
+ isTool,
6
+ type AnyConnector,
7
+ type Capability,
8
+ type CapabilityResult,
9
+ type ConnectorContext,
10
+ type DiscoveredCapability,
11
+ type ProviderDefinition,
12
+ } from '#connectivity';
13
+
14
+ /**
15
+ * The `local` connector — our own code.
16
+ *
17
+ * Thin by design: it exists so dispatch has **one** path rather than branching
18
+ * on whether a provider happens to carry handlers. `example` and the M4 owner
19
+ * layer are the only users.
20
+ *
21
+ * It is also the only connector that serves anything other than tools. A remote
22
+ * kind reports what an upstream server or an OpenAPI document describes, and
23
+ * both describe operations; resources and prompts exist only where we author
24
+ * the capability ourselves. Until M4 this connector dropped them in `discover`
25
+ * and threw in `invoke`, which is why `docs/detailed/init.md`'s "nothing in core changes
26
+ * to add [the owner layer]" was false — the resource path registered in
27
+ * `packages/mcp` could not reach a provider at all.
28
+ */
29
+ export function createLocalConnector(definition: ProviderDefinition): AnyConnector {
30
+ return {
31
+ kind: 'local',
32
+
33
+ async discover(): Promise<DiscoveredCapability[]> {
34
+ // Local capabilities author Zod; everything downstream speaks JSON Schema,
35
+ // because that is what upstream servers and OpenAPI documents give us.
36
+ // Resources and prompts have no input schema of that shape, so they report
37
+ // an empty one rather than being omitted — a caller counting what a
38
+ // provider exposes should see all of it.
39
+ return definition.capabilities.map((capability) => ({
40
+ name: capability.name,
41
+ ...(capability.title ? { title: capability.title } : {}),
42
+ description: capability.description,
43
+ inputSchema: isTool(capability)
44
+ ? (z.toJSONSchema(capability.inputSchema) as Record<string, unknown>)
45
+ : {},
46
+ ...(bundleFor(definition, capability.name)
47
+ ? { bundle: bundleFor(definition, capability.name)! }
48
+ : {}),
49
+ }));
50
+ },
51
+
52
+ async invoke(capability, args, context: ConnectorContext): Promise<CapabilityResult> {
53
+ const found = definition.capabilities.find((entry) => entry.name === capability.name);
54
+ if (!found) {
55
+ throw new Error(`${definition.manifest.id}.${capability.name} is not a capability`);
56
+ }
57
+
58
+ if (isTool(found)) {
59
+ const parsed = found.inputSchema.safeParse(args);
60
+ if (!parsed.success) {
61
+ throw new Error(invalidArguments(definition, found, parsed.error.issues[0]?.message));
62
+ }
63
+
64
+ return found.handler(parsed.data, context.provider);
65
+ }
66
+
67
+ if (isResource(found)) {
68
+ // A `uri` argument means "read this one"; its absence means "enumerate".
69
+ // Core is the only caller and passes one or the other — the MCP layer
70
+ // builds both from the registration, so neither is client-supplied.
71
+ const uri = args['uri'];
72
+
73
+ if (uri === undefined) {
74
+ if (!found.list) {
75
+ throw new Error(
76
+ `${definition.manifest.id}.${found.name} does not enumerate: its resource space is unbounded.`,
77
+ );
78
+ }
79
+ return { resources: await found.list(context.provider) };
80
+ }
81
+
82
+ if (typeof uri !== 'string') {
83
+ throw new Error(`${definition.manifest.id}.${found.name} needs a string uri`);
84
+ }
85
+
86
+ return {
87
+ contents: [await found.read(uri, extractParams(found.uriTemplate, uri), context.provider)],
88
+ };
89
+ }
90
+
91
+ if (isPrompt(found)) {
92
+ const missing = (found.arguments ?? [])
93
+ .filter((argument) => argument.required && typeof args[argument.name] !== 'string')
94
+ .map((argument) => argument.name);
95
+
96
+ if (missing.length > 0) {
97
+ throw new Error(invalidArguments(definition, found, `missing ${missing.join(', ')}`));
98
+ }
99
+
100
+ return found.render(stringArguments(args), context.provider);
101
+ }
102
+
103
+ // Every branch above returns, so this is unreachable while `Capability`
104
+ // has three members — and becomes a compile error the moment it gains a
105
+ // fourth, which is the point.
106
+ const exhaustive: never = found;
107
+ return exhaustive;
108
+ },
109
+ };
110
+ }
111
+
112
+ function invalidArguments(
113
+ definition: ProviderDefinition,
114
+ capability: Capability,
115
+ detail: string | undefined,
116
+ ): string {
117
+ return `Invalid arguments for ${definition.manifest.id}.${capability.name}: ${detail ?? 'invalid'}`;
118
+ }
119
+
120
+ /**
121
+ * Prompt arguments are strings on the wire, and a provider's `render` is typed
122
+ * for that. Anything else a caller managed to attach is dropped rather than
123
+ * coerced: a prompt that silently received `"[object Object]"` would be worse
124
+ * than one that received nothing.
125
+ */
126
+ function stringArguments(args: Readonly<Record<string, unknown>>): Record<string, string> {
127
+ const out: Record<string, string> = {};
128
+ for (const [key, value] of Object.entries(args)) {
129
+ if (typeof value === 'string') out[key] = value;
130
+ }
131
+ return out;
132
+ }
133
+
134
+ /**
135
+ * Recover a resource's own template variables from a concrete URI.
136
+ *
137
+ * The MCP SDK matches the *registered* template, which core has already
138
+ * rewritten to carry the profile and connection (see `scopeResourceUri` in
139
+ * `packages/mcp`). Matching the provider's own template here instead means a
140
+ * handler receives exactly the variables it declared and never learns how it
141
+ * was routed — the same separation `ConnectionInfo` gives a tool.
142
+ *
143
+ * Aligned from the right, because the routing segments core prepends are on the
144
+ * left. Deliberately a segment-wise match rather than a general RFC 6570
145
+ * expander: templates in this codebase use simple `{name}` expansion, and a
146
+ * partial implementation of the rest would be a trap.
147
+ */
148
+ export function extractParams(template: string, uri: string): Record<string, string> {
149
+ const templateParts = splitUri(template);
150
+ const uriParts = splitUri(uri);
151
+ const params: Record<string, string> = {};
152
+
153
+ for (let offset = 1; offset <= templateParts.length; offset += 1) {
154
+ const templatePart = templateParts[templateParts.length - offset]!;
155
+ const uriPart = uriParts[uriParts.length - offset];
156
+ if (uriPart === undefined) break;
157
+
158
+ const variable = templatePart.match(/^\{(.+)\}$/);
159
+ if (variable) params[variable[1]!] = uriPart;
160
+ }
161
+
162
+ return params;
163
+ }
164
+
165
+ function splitUri(value: string): string[] {
166
+ const scheme = value.indexOf('://');
167
+ return (scheme === -1 ? value : value.slice(scheme + 3)).split('/').filter((part) => part !== '');
168
+ }
169
+
170
+ function bundleFor(definition: ProviderDefinition, capability: string): string | undefined {
171
+ return definition.manifest.bundles?.find((bundle) => bundle.capabilities.includes(capability))
172
+ ?.name;
173
+ }
@@ -0,0 +1,215 @@
1
+ import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
2
+ import type {
3
+ Connector,
4
+ DiscoveryContext,
5
+ DiscoveredCapability,
6
+ ToolResult,
7
+ } from '#connectivity';
8
+ import { READ_BUNDLE, WRITE_BUNDLE } from '#connectivity';
9
+
10
+ /**
11
+ * The `mcp` connector — proxy an upstream MCP server.
12
+ *
13
+ * This is what makes Notion and Linear cost a fifteen-line manifest each: the
14
+ * vendor already wrote the integration, and they maintain it. What we add is
15
+ * the thing they do not have — per-capability policy, audit with redaction, and
16
+ * profile isolation in front of it.
17
+ *
18
+ * Capabilities are **discovered**, never declared. The upstream server is the
19
+ * source of truth for what it exposes, and pretending otherwise would mean a
20
+ * manifest going stale every time the vendor ships.
21
+ */
22
+
23
+ export interface McpConnectorOptions {
24
+ readonly endpoint: string;
25
+ /** Supplies the bearer token for an upstream call; refreshes if needed. */
26
+ readonly accessToken: () => Promise<string | null>;
27
+ readonly fetch?: typeof globalThis.fetch;
28
+ }
29
+
30
+ interface UpstreamTool {
31
+ name: string;
32
+ title?: string;
33
+ description?: string;
34
+ inputSchema?: Record<string, unknown>;
35
+ annotations?: { readOnlyHint?: boolean; destructiveHint?: boolean };
36
+ }
37
+
38
+ /**
39
+ * Guess a bundle from the upstream tool's own hints.
40
+ *
41
+ * MCP tools may carry a `readOnlyHint`, which is exactly the read/write split
42
+ * we want. Where the hint is absent we fall back to the name, and where that is
43
+ * ambiguous we say `write` — the safer answer, since `read` is what `connect`
44
+ * grants by default and over-granting is the failure that matters.
45
+ */
46
+ export function inferBundle(tool: UpstreamTool, name = tool.name): string {
47
+ if (tool.annotations?.readOnlyHint === true) return READ_BUNDLE;
48
+ if (tool.annotations?.readOnlyHint === false) return WRITE_BUNDLE;
49
+
50
+ // Fallback for servers that publish no hint. Matched against the *shortened*
51
+ // name and tolerant of hyphens, because vendors separate words however they
52
+ // like — Notion uses `-`, others `_`. An unrecognised verb means `write`,
53
+ // which is the safer default given `read` is what connect grants.
54
+ return /^(get|list|search|read|find|query|fetch|describe|view|download)([-_]|$)/i.test(name)
55
+ ? READ_BUNDLE
56
+ : WRITE_BUNDLE;
57
+ }
58
+
59
+ /**
60
+ * Drop a redundant provider prefix from an upstream tool name.
61
+ *
62
+ * Notion names every tool `notion-*`, which would make our qualified id
63
+ * `notion.notion-search` and the wire name `notion_notion-search`. Stripping it
64
+ * is safe because the original is kept in `target.tool` and is what we actually
65
+ * call — this only affects how the capability is addressed in policy and audit,
66
+ * which is where readability matters.
67
+ *
68
+ * Skipped entirely if stripping would collide with another tool, since a
69
+ * shorter name is never worth routing to the wrong one.
70
+ */
71
+ export function shortenName(providerId: string, name: string, all: readonly string[]): string {
72
+ for (const separator of ['-', '_', '.']) {
73
+ const prefix = `${providerId}${separator}`;
74
+ if (!name.startsWith(prefix) || name.length === prefix.length) continue;
75
+
76
+ const shortened = name.slice(prefix.length);
77
+ const collides = all.some((other) => other !== name && other === shortened);
78
+ return collides ? name : shortened;
79
+ }
80
+ return name;
81
+ }
82
+
83
+ /**
84
+ * Turn an upstream transport error into something readable.
85
+ *
86
+ * The SDK reports a bad HTTP status by appending the whole response body,
87
+ * which for a large tool list is tens of kilobytes of JSON scrolling past the
88
+ * actual problem. Worse, some servers hide a real explanation inside it:
89
+ * Google answers 403 with a perfectly formed JSON-RPC result whose text says
90
+ * the MCP API is not enabled on the project — the one sentence that tells you
91
+ * what to do, buried in 44KB.
92
+ */
93
+ export function readableUpstreamError(error: unknown, endpoint: string): Error {
94
+ const message = error instanceof Error ? error.message : String(error);
95
+
96
+ const start = message.indexOf('{');
97
+ if (start === -1) return error instanceof Error ? error : new Error(message);
98
+
99
+ try {
100
+ const body = JSON.parse(message.slice(start)) as {
101
+ error?: { message?: string };
102
+ result?: { content?: Array<{ text?: string }> };
103
+ };
104
+
105
+ const detail = body.error?.message ?? body.result?.content?.[0]?.text;
106
+ if (detail) {
107
+ return new Error(`${new URL(endpoint).host}: ${detail.trim()}`);
108
+ }
109
+ } catch {
110
+ // Not JSON, or truncated — fall through to the summary below.
111
+ }
112
+
113
+ return new Error(`${message.slice(0, start).trim()} (${new URL(endpoint).host})`);
114
+ }
115
+
116
+ export function createMcpConnector(options: McpConnectorOptions): Connector {
117
+ /**
118
+ * A fresh client per operation.
119
+ *
120
+ * Upstream sessions are not reused across requests: the server is stateless
121
+ * and may be replaced between them, so holding a connection would be state we
122
+ * have promised not to keep. Access tokens *are* cached in memory by the
123
+ * caller, so the cost is one HTTP connection rather than a token exchange.
124
+ */
125
+ const connect = async (context: DiscoveryContext): Promise<Client> => {
126
+ const token = await options.accessToken();
127
+
128
+ const transport = new StreamableHTTPClientTransport(new URL(options.endpoint), {
129
+ requestInit: {
130
+ headers: token ? { authorization: `Bearer ${token}` } : {},
131
+ },
132
+ ...(options.fetch ? { fetch: options.fetch } : {}),
133
+ } as never);
134
+
135
+ const client = new Client(
136
+ {
137
+ name: 'lanes-link',
138
+ version: context.manifest.version,
139
+ },
140
+ {
141
+ // Negotiate the era rather than assuming one. A gateway proxying
142
+ // arbitrary vendors cannot assume they have all moved: Notion and
143
+ // Linear speak the modern handshake-free revision, while Google's
144
+ // Gmail and Drive servers are on 2025-06-18 and answer 403 to anything
145
+ // that arrives before `initialize`. `auto` probes with
146
+ // `server/discover` and falls back to the legacy handshake.
147
+ versionNegotiation: { mode: 'auto' },
148
+ },
149
+ );
150
+
151
+ await client.connect(transport);
152
+ return client;
153
+ };
154
+
155
+ return {
156
+ kind: 'mcp',
157
+
158
+ async discover(context): Promise<DiscoveredCapability[]> {
159
+ const client = await connect(context);
160
+
161
+ try {
162
+ const { tools = [] } = (await client.listTools()) as { tools?: UpstreamTool[] };
163
+ const names = tools.map((tool) => tool.name);
164
+
165
+ return tools.map((tool) => ({
166
+ name: shortenName(context.manifest.id, tool.name, names),
167
+ ...(tool.title ? { title: tool.title } : {}),
168
+ description: tool.description ?? `${context.manifest.name} ${tool.name}`,
169
+ inputSchema: tool.inputSchema ?? { type: 'object', properties: {} },
170
+ bundle: inferBundle(tool, shortenName(context.manifest.id, tool.name, names)),
171
+ // The upstream name is kept verbatim: ours may differ once it has
172
+ // been through name normalisation, and calling the wrong tool
173
+ // upstream would be a silent, expensive mistake.
174
+ target: { tool: tool.name },
175
+ }));
176
+ } catch (error) {
177
+ throw readableUpstreamError(error, options.endpoint);
178
+ } finally {
179
+ await client.close().catch(() => {});
180
+ }
181
+ },
182
+
183
+ async invoke(capability, args, context): Promise<ToolResult> {
184
+ const client = await connect(context);
185
+ const upstreamName = (capability.target?.['tool'] as string | undefined) ?? capability.name;
186
+
187
+ try {
188
+ const result = (await client.callTool({
189
+ name: upstreamName,
190
+ arguments: args as Record<string, unknown>,
191
+ })) as {
192
+ content?: Array<{ type: string; text?: string; uri?: string; name?: string }>;
193
+ isError?: boolean;
194
+ };
195
+
196
+ return {
197
+ content: (result.content ?? []).map((block) =>
198
+ block.type === 'text'
199
+ ? { type: 'text' as const, text: block.text ?? '' }
200
+ : {
201
+ type: 'resource_link' as const,
202
+ uri: block.uri ?? '',
203
+ ...(block.name ? { name: block.name } : {}),
204
+ },
205
+ ),
206
+ ...(result.isError ? { isError: true } : {}),
207
+ };
208
+ } catch (error) {
209
+ throw readableUpstreamError(error, options.endpoint);
210
+ } finally {
211
+ await client.close().catch(() => {});
212
+ }
213
+ },
214
+ };
215
+ }
@@ -0,0 +1,63 @@
1
+ # Deployments
2
+
3
+ Where a profile runs. A target names one, and the whole of the difference is
4
+ two backends: a `SecretStore` and a `BlobStore`. Connections, providers, policy
5
+ and limits are declared once and apply to every target.
6
+
7
+ | Folder | Target | Secrets | Blobs | Vault |
8
+ |---|---|---|---|---|
9
+ | `local/` | `local` | encrypted file | `filesystem` | `file` |
10
+ | `gcp/` | `cloud` | `gcp-secret-manager` | `gcs` (or `s3`) | `secret` |
11
+ | `azure/` | — | not implemented | | |
12
+
13
+ There is no database column, and no database. State is one object per key and
14
+ the audit log is one object per event, both in whatever `BlobStore` the target
15
+ already opened — so the deployed target has exactly two standing dependencies
16
+ and neither of them is a service to provision (ADR-020, ADR-021).
17
+
18
+ ## Adapters are named for the protocol, deployments for the vendor
19
+
20
+ `s3` needs an endpoint and a key pair; `gcs` needs neither, because it
21
+ authenticates as the identity already present. R2, MinIO, Supabase Storage and
22
+ AWS differ only in those values, so a vendor name on the adapter would claim a
23
+ coupling that does not exist and would have to be either lied about or
24
+ duplicated the first time someone pointed it elsewhere (ADR-013). The host is a
25
+ deploy-time choice; the protocol is the interface.
26
+
27
+ That is why `adapters/` is flat and shared, and why the GCP folder holds no
28
+ storage code — only the things that genuinely are Google's: how to roll a Cloud
29
+ Run revision, what to create before the first one, and what to ask when the
30
+ config does not say yet.
31
+
32
+ ## Adding one
33
+
34
+ 1. A folder here.
35
+ 2. If it needs a backend nobody has written, an adapter in `adapters/` named for
36
+ the protocol.
37
+ 3. A case in `target.ts`, importing that adapter **inside the branch** — a local
38
+ run must not load a cloud client, or a missing cloud credential fails a
39
+ `lanes link start` that was never going to talk to that cloud.
40
+ 4. A member of the adapter enums in `#profile`'s schema, so a target can name it.
41
+ 5. To be deployable: a `DeployDriver` (`driver.ts`) and a case in `drivers.ts`,
42
+ imported inside the branch for the same reason. Add its name to the
43
+ `deploy.platform` enum in `#profile`'s schema.
44
+
45
+ Nothing else in the codebase learns about it. In particular the CLI does not:
46
+ `lanes link deploy` is a thin wrapper that dispatches to the deployment's own
47
+ driver through `drivers.ts`, and the machinery for rolling a revision lives with
48
+ the vendor that needs it. A driver returns its steps as **data** — argv arrays,
49
+ never shell strings — which is what makes `--dry-run` show the real sequence and
50
+ what lets the commands be asserted in a test with no cloud account anywhere near
51
+ it.
52
+
53
+ What is *not* the driver's: the deployment block is named `deploy` and carries a
54
+ `platform`, so which vendor a target uses is a value rather than the name of a
55
+ key. `project` is optional in that block and required by the driver that needs
56
+ one, on the same reasoning as `credentials.project`.
57
+
58
+ ## Blob storage is not optional in a deployment
59
+
60
+ `adapter: filesystem` on Cloud Run **appears to work**: every write succeeds,
61
+ every read within the life of an instance succeeds, and the bytes are gone when
62
+ the instance recycles. A missing adapter that throws is a smaller problem than a
63
+ present adapter that silently loses data. See ADR-013.