@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,400 @@
1
+ import { Client, InMemoryTransport } from '@modelcontextprotocol/client';
2
+ import {
3
+ AuthenticatorChain,
4
+ BearerAuthenticator,
5
+ IssuedTokenAuthenticator,
6
+ OAuthServer,
7
+ OAuthStore,
8
+ tokensMatch,
9
+ } from '#auth';
10
+ import { oneProfile, type ProfileRuntime } from './mcp/index.ts';
11
+ import { parseConfig, type Config } from '#profile';
12
+ import { ProviderRegistry, toPolicyDocument } from '#registry';
13
+ import { Dispatcher } from '#dispatch';
14
+ import { createMemoryCredentials, createMemoryState } from '#stores/state/testing.ts';
15
+ import { RateLimiter } from '#policy';
16
+ import { silentLogger, type Logger } from './logging.ts';
17
+ import { createMemoryBlobStore } from '#stores/blobs/testing.ts';
18
+ import { exampleProvider } from '#providers/example/provider.ts';
19
+ import { createLocalConnector } from '#connectivity/transports';
20
+ import type { RuntimeState } from '#stores/state';
21
+ import type { AuditStore } from '#audit';
22
+ import { createBlobAuditStore } from '#deployments/adapters/audit-blob.ts';
23
+ import type { AnyConnector, ProviderDefinition } from '#connectivity';
24
+ import { serve, type RunningServer } from './index.ts';
25
+ import { Generations } from './generations.ts';
26
+ import { serveOverStdio } from './stdio.ts';
27
+
28
+ /**
29
+ * Test harness: a fully wired profile served over real HTTP on a real port.
30
+ *
31
+ * Deliberately not a mock of the transport. The point of these tests is that
32
+ * an agent talking to the actual endpoint sees the right tools and is refused
33
+ * the right calls, and a mocked transport cannot demonstrate that.
34
+ */
35
+
36
+ export const TEST_TOKEN = 'llk_test_token_value';
37
+
38
+ export function configFor(profile: string, port: number, policy: string): Config {
39
+ return parseConfig(`
40
+ contract: 1
41
+ instance:
42
+ profile: ${profile}
43
+ default_target: local
44
+ port: ${port}
45
+ targets:
46
+ local:
47
+ credentials: { adapter: file, path: ./data/${profile}/credentials.enc }
48
+ storage: { adapter: filesystem, path: ./data/${profile} }
49
+ limits:
50
+ requests_per_minute: 1000
51
+ upstream_calls_per_minute: 1000
52
+ connections:
53
+ - id: a
54
+ provider: example
55
+ account: Scratch A
56
+ - id: b
57
+ provider: example
58
+ account: Scratch B
59
+ policy:
60
+ ${policy}
61
+ `).config;
62
+ }
63
+
64
+ export interface Harness {
65
+ readonly server: RunningServer;
66
+ readonly state: RuntimeState;
67
+ readonly audit: AuditStore;
68
+ readonly token: string;
69
+ /**
70
+ * The primary profile's dispatcher.
71
+ *
72
+ * Exposed because policy denial and MCP discovery filtering are two different
73
+ * observations of one decision, and only one of them is visible over the
74
+ * wire: a capability a profile cannot reach is never registered, so calling
75
+ * it is answered by the SDK before any of our code runs. Asserting that the
76
+ * *denial* is recorded means asking the layer that records it.
77
+ */
78
+ readonly dispatcher: Dispatcher;
79
+ /** The generation holder, so a test can drive a reload without an HTTP call. */
80
+ readonly generations: Generations;
81
+ stop(): Promise<void>;
82
+ }
83
+
84
+ export { parseConfig } from '#profile';
85
+
86
+ export interface HarnessOptions {
87
+ profile: string;
88
+ log?: Logger;
89
+ port: number;
90
+ policy: string;
91
+ token?: string;
92
+ /** Extra providers to register beyond `example`. */
93
+ providers?: readonly ProviderDefinition[];
94
+ /** Credentials to seed, beyond the profile token. */
95
+ credentials?: Record<string, string>;
96
+ /** Overrides the generated config wholesale, for providers needing more than the default. */
97
+ config?: Config;
98
+ /** Extra profiles this one endpoint also serves, each with its own policy. */
99
+ alsoServe?: ReadonlyArray<{ profile: string; policy: string }>;
100
+ /**
101
+ * What the endpoint calls to re-read skills before serving a request.
102
+ *
103
+ * Handed the registry so a test can swap the skills provider in it, which is
104
+ * what `openRuntime` does for real. Passed in rather than built here because
105
+ * where skills are stored is the runtime's business, not the transport's.
106
+ */
107
+ refreshSkills?: (registry: ProviderRegistry) => Promise<void>;
108
+ /** Serve the `self` authorization flow alongside the bearer token. */
109
+ authorization?: boolean;
110
+ /**
111
+ * What a reload re-reads, standing in for `openReconciled` over a workspace
112
+ * this harness does not have. Throwing is how the "a failed reload keeps
113
+ * serving the old generation" case is reached; absent means nothing new.
114
+ */
115
+ reopen?: () => Promise<ReadonlyMap<string, ProfileRuntime>>;
116
+ }
117
+
118
+ /**
119
+ * Everything a transport needs, wired from one set of options.
120
+ *
121
+ * Extracted when stdio arrived: the two transports differ in how a caller
122
+ * reaches the profiles, and in nothing else. A second copy of this wiring would
123
+ * have let them drift, which is exactly what the stdio tests are for.
124
+ */
125
+ interface WiredProfiles {
126
+ readonly profiles: Map<string, ProfileRuntime>;
127
+ readonly state: RuntimeState;
128
+ /** The real blob-backed log over an in-memory store, not a second fake. */
129
+ readonly audit: AuditStore;
130
+ readonly dispatcher: Dispatcher;
131
+ readonly credentials: ReturnType<typeof createMemoryCredentials>;
132
+ readonly token: string;
133
+ }
134
+
135
+ export function wireProfiles(options: HarnessOptions): WiredProfiles {
136
+ const config = options.config ?? configFor(options.profile, options.port, options.policy);
137
+ const token = options.token ?? TEST_TOKEN;
138
+
139
+ const state = createMemoryState();
140
+ const audit = createBlobAuditStore({ storage: createMemoryBlobStore() });
141
+ const credentials = createMemoryCredentials({ 'profile/token': token, ...options.credentials });
142
+ // `allowReserved` for the same reason `buildRegistry` passes it: the owner
143
+ // layer claims `memory`, `skills`, and `vault`, and the guard still refuses
144
+ // them everywhere else.
145
+ const registry = new ProviderRegistry({ allowReserved: true });
146
+ registry.register(exampleProvider);
147
+ for (const provider of options.providers ?? []) registry.register(provider);
148
+
149
+ const policy = toPolicyDocument(config);
150
+
151
+ const dispatcher = new Dispatcher({
152
+ config,
153
+ registry,
154
+ connectorFor: (providerId): AnyConnector | undefined => {
155
+ const entry = registry.get(providerId);
156
+ return entry?.definition ? createLocalConnector(entry.definition) : undefined;
157
+ },
158
+ policy,
159
+ state,
160
+ audit,
161
+ credentials,
162
+ storage: createMemoryBlobStore(),
163
+ limiter: new RateLimiter(),
164
+ log: silentLogger(),
165
+ });
166
+
167
+ // Each extra profile gets its own state and its own log, so the isolation
168
+ // the tests assert is real rather than a naming convention.
169
+ const profiles = new Map(
170
+ oneProfile(options.profile, {
171
+ config,
172
+ registry,
173
+ dispatcher,
174
+ policy,
175
+ ...(options.refreshSkills ? { refreshSkills: () => options.refreshSkills!(registry) } : {}),
176
+ }),
177
+ );
178
+
179
+ for (const extra of options.alsoServe ?? []) {
180
+ const extraConfig = configFor(extra.profile, options.port, extra.policy);
181
+ const extraPolicy = toPolicyDocument(extraConfig);
182
+ const extraDatabase = createMemoryState();
183
+
184
+ profiles.set(extra.profile, {
185
+ config: extraConfig,
186
+ registry,
187
+ policy: extraPolicy,
188
+ dispatcher: new Dispatcher({
189
+ config: extraConfig,
190
+ registry,
191
+ connectorFor: (providerId): AnyConnector | undefined => {
192
+ const entry = registry.get(providerId);
193
+ return entry?.definition ? createLocalConnector(entry.definition) : undefined;
194
+ },
195
+ policy: extraPolicy,
196
+ state: extraDatabase,
197
+ audit: createBlobAuditStore({ storage: createMemoryBlobStore() }),
198
+ credentials,
199
+ storage: createMemoryBlobStore(),
200
+ limiter: new RateLimiter(),
201
+ log: silentLogger(),
202
+ }),
203
+ });
204
+ }
205
+
206
+ return { profiles, state, audit, dispatcher, credentials, token };
207
+ }
208
+
209
+ export function startHarness(options: HarnessOptions): Harness {
210
+ const { profiles, state, audit, dispatcher, credentials, token } = wireProfiles(options);
211
+
212
+ const bearer = new BearerAuthenticator({
213
+ profile: options.profile,
214
+ tokenRef: 'profile/token',
215
+ credentials,
216
+ });
217
+
218
+ // The real wiring from `endpoint.ts`, not a stand-in: the flow under test is
219
+ // the one a connector drives over HTTP, and a fake authorization server would
220
+ // demonstrate that the fake works.
221
+ const store = options.authorization ? new OAuthStore(state.kv) : null;
222
+ const gate = store
223
+ ? {
224
+ surface: {
225
+ server: new OAuthServer({
226
+ store,
227
+ accessTokenTtlMs: 3_600_000,
228
+ verifyOwner: (presented) => Promise.resolve(tokensMatch(presented, token)),
229
+ }),
230
+ issuer: (origin: string) => origin,
231
+ mcpPath: '/mcp',
232
+ target: 'local',
233
+ },
234
+ authenticator: new IssuedTokenAuthenticator(store, options.profile),
235
+ }
236
+ : null;
237
+
238
+ const log = options.log ?? silentLogger();
239
+
240
+ const nothing = () => Promise.resolve();
241
+ const generations = new Generations(
242
+ { profiles, close: nothing },
243
+ async () => ({ profiles: (await options.reopen?.()) ?? profiles, close: nothing }),
244
+ { primary: options.profile, log },
245
+ );
246
+
247
+ const server = serve({
248
+ generations,
249
+ primary: options.profile,
250
+ authenticator: gate ? new AuthenticatorChain([bearer, gate.authenticator]) : bearer,
251
+ ...(gate ? { authorization: gate.surface } : {}),
252
+ log,
253
+ });
254
+
255
+ return {
256
+ server,
257
+ state,
258
+ audit,
259
+ token,
260
+ dispatcher,
261
+ generations,
262
+ stop: () => server.stop(),
263
+ };
264
+ }
265
+
266
+ export interface StdioHarness {
267
+ /** A real MCP client, connected to the real surface over a linked transport pair. */
268
+ readonly client: Client;
269
+ readonly state: RuntimeState;
270
+ readonly audit: AuditStore;
271
+ readonly dispatcher: Dispatcher;
272
+ stop(): Promise<void>;
273
+ }
274
+
275
+ /**
276
+ * The same profiles, served over stdio instead of a port.
277
+ *
278
+ * `InMemoryTransport.createLinkedPair()` stands in for the pipe: the surface
279
+ * gets one end, a real `Client` gets the other, and nothing about the surface
280
+ * knows the difference — `serveOverStdio` takes the transport for exactly this
281
+ * reason. What is not exercised is the process boundary itself, which is the
282
+ * client's business rather than ours.
283
+ */
284
+ export async function startStdioHarness(
285
+ options: HarnessOptions & { clientLabel?: string },
286
+ ): Promise<StdioHarness> {
287
+ const { profiles, state, audit, dispatcher } = wireProfiles(options);
288
+ const [clientSide, serverSide] = InMemoryTransport.createLinkedPair();
289
+
290
+ const surface = serveOverStdio({
291
+ profiles,
292
+ primary: options.profile,
293
+ log: silentLogger(),
294
+ transport: serverSide,
295
+ ...(options.clientLabel ? { clientLabel: options.clientLabel } : {}),
296
+ });
297
+
298
+ const client = new Client({ name: options.clientLabel ?? 'lanes-link-stdio-test', version: '0.0.0' });
299
+ await client.connect(clientSide);
300
+
301
+ return {
302
+ client,
303
+ state,
304
+ audit,
305
+ dispatcher,
306
+ async stop() {
307
+ await client.close();
308
+ await surface.close();
309
+ },
310
+ };
311
+ }
312
+
313
+ export interface RpcResult {
314
+ readonly status: number;
315
+ readonly body: Record<string, unknown>;
316
+ }
317
+
318
+ /**
319
+ * One JSON-RPC call over the wire.
320
+ *
321
+ * Handles both plain JSON and SSE-framed responses, because a streamable HTTP
322
+ * server may answer either and a test that only understands one would fail for
323
+ * the wrong reason.
324
+ */
325
+ export async function rpc(
326
+ url: string,
327
+ method: string,
328
+ params: Record<string, unknown>,
329
+ options: { token?: string | null; clientLabel?: string; id?: number } = {},
330
+ ): Promise<RpcResult> {
331
+ const headers: Record<string, string> = {
332
+ 'content-type': 'application/json',
333
+ accept: 'application/json, text/event-stream',
334
+ 'mcp-protocol-version': '2026-07-28',
335
+ // 2026-07-28 requires the method — and, where the payload names one, the
336
+ // target — in headers as well as in the body, so an intermediary can route
337
+ // without parsing the payload. The server rejects the request if the two
338
+ // disagree, which is what makes the header trustworthy.
339
+ 'mcp-method': method,
340
+ };
341
+ // 2026-07-28 mirrors `params.name` for `tools/call` and `prompts/get`, and
342
+ // `params.uri` for `resources/read`, into `Mcp-Name`. The server rejects the
343
+ // request when the two disagree — including when the header is simply absent
344
+ // — so a resource test that only mirrored `name` would fail for the wrong
345
+ // reason.
346
+ const named = params['name'] ?? params['uri'];
347
+ if (typeof named === 'string') headers['mcp-name'] = named;
348
+ if (options.token !== null) headers['authorization'] = `Bearer ${options.token ?? TEST_TOKEN}`;
349
+ if (options.clientLabel) headers['x-mcp-client'] = options.clientLabel;
350
+
351
+ const response = await fetch(url, {
352
+ method: 'POST',
353
+ headers,
354
+ body: JSON.stringify({
355
+ jsonrpc: '2.0',
356
+ id: options.id ?? 1,
357
+ method,
358
+ params: {
359
+ ...params,
360
+ // The 2026-07-28 envelope: every request declares its protocol
361
+ // version and the caller's capabilities, since there is no
362
+ // handshake left to establish them once.
363
+ _meta: {
364
+ 'io.modelcontextprotocol/protocolVersion': '2026-07-28',
365
+ 'io.modelcontextprotocol/clientCapabilities': {},
366
+ 'io.modelcontextprotocol/clientInfo': {
367
+ name: options.clientLabel ?? 'lanes-link-test',
368
+ version: '0.0.0',
369
+ },
370
+ },
371
+ },
372
+ }),
373
+ });
374
+
375
+ const text = await response.text();
376
+ return { status: response.status, body: parseBody(text) };
377
+ }
378
+
379
+ function parseBody(text: string): Record<string, unknown> {
380
+ if (text.trim().length === 0) return {};
381
+
382
+ if (text.startsWith('event:') || text.includes('\ndata:') || text.startsWith('data:')) {
383
+ const line = text
384
+ .split('\n')
385
+ .find((candidate) => candidate.startsWith('data:'));
386
+ return line ? (JSON.parse(line.slice('data:'.length).trim()) as Record<string, unknown>) : {};
387
+ }
388
+
389
+ try {
390
+ return JSON.parse(text) as Record<string, unknown>;
391
+ } catch {
392
+ return { raw: text };
393
+ }
394
+ }
395
+
396
+ /** Port allocator, so parallel test files never collide. */
397
+ let nextPort = 7900;
398
+ export function allocatePort(): number {
399
+ return nextPort++;
400
+ }