@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,115 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { defineProvider, type ProviderManifest } from '#connectivity';
5
+ import { ConfigError, layout } from '#profile';
6
+ import { findSecrets, formatSecretFindings } from '#profile';
7
+
8
+ /**
9
+ * Provider manifests supplied by the operator.
10
+ *
11
+ * The built-in list is a convenience, never a boundary. Anything not shipped is
12
+ * a YAML file in `<workspace>/data/<profile>/providers.d/`, validated by the
13
+ * same schema the built-ins are — no code, no rebuild, no pull request.
14
+ *
15
+ * That is the whole scalability claim of this milestone: a service nobody has
16
+ * integrated costs a file.
17
+ *
18
+ * **Per profile**, which the workspace-wide directory this replaced was not. A
19
+ * manifest names a host, an OpenAPI document, and the credential refs that
20
+ * reach them, so it describes somebody's infrastructure — and work's is not
21
+ * personal's to read. The path comes from `layout` for the same reason every
22
+ * other profile-owned path does: one place that knows the on-disk shape.
23
+ * ADR-030.
24
+ */
25
+
26
+ export interface LoadedManifest {
27
+ readonly manifest: ProviderManifest;
28
+ readonly path: string;
29
+ }
30
+
31
+ export async function loadProfileProviders(
32
+ workspaceRoot: string,
33
+ profile: string,
34
+ ): Promise<LoadedManifest[]> {
35
+ const directory = join(workspaceRoot, layout.providers(profile));
36
+
37
+ let entries: string[];
38
+ try {
39
+ entries = await readdir(directory);
40
+ } catch {
41
+ return []; // No custom providers is the normal case.
42
+ }
43
+
44
+ const loaded: LoadedManifest[] = [];
45
+
46
+ for (const name of entries.sort()) {
47
+ if (!name.endsWith('.yaml') && !name.endsWith('.yml')) continue;
48
+ if (name.endsWith('.example.yaml')) continue;
49
+
50
+ const path = join(directory, name);
51
+ loaded.push({ manifest: await parseManifestFile(path), path });
52
+ }
53
+
54
+ return loaded;
55
+ }
56
+
57
+ export async function parseManifestFile(path: string): Promise<ProviderManifest> {
58
+ return resolveSpecPath(parseManifest(await readFile(path, 'utf8'), path), path);
59
+ }
60
+
61
+ /**
62
+ * Make a relative `openapi` path mean what the operator meant.
63
+ *
64
+ * `docs/detailed/creating-a-provider.md` tells people to write `openapi: ./mything.json`,
65
+ * and until now that resolved against the *process* working directory — so it
66
+ * worked when you happened to run `lanes` from the right folder and failed
67
+ * with a confusing ENOENT everywhere else. The built-ins never noticed because
68
+ * they compute an absolute path from `import.meta.url`.
69
+ *
70
+ * Resolved against the manifest's own directory, the way one file referencing
71
+ * another normally works.
72
+ */
73
+ function resolveSpecPath(manifest: ProviderManifest, source: string): ProviderManifest {
74
+ const connector = manifest.connector;
75
+ if (connector.kind !== 'http') return manifest;
76
+ if (/^https?:/i.test(connector.openapi) || isAbsolute(connector.openapi)) return manifest;
77
+
78
+ return {
79
+ ...manifest,
80
+ connector: { ...connector, openapi: resolve(dirname(source), connector.openapi) },
81
+ };
82
+ }
83
+
84
+ export function parseManifest(text: string, source = '<manifest>'): ProviderManifest {
85
+ let raw: unknown;
86
+ try {
87
+ raw = parseYaml(text);
88
+ } catch (error) {
89
+ throw new ConfigError(`${source}: could not parse YAML — ${(error as Error).message}`);
90
+ }
91
+
92
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
93
+ throw new ConfigError(`${source}: expected a YAML mapping at the top level`);
94
+ }
95
+
96
+ // A manifest is config, so it is held to the same rule config is: references
97
+ // into the credential store, never values. Someone writing their first
98
+ // manifest will reach for pasting the API key directly, and this is what
99
+ // stops that becoming a credential in a file they might commit.
100
+ const secrets = findSecrets(raw);
101
+ if (secrets.length > 0) {
102
+ throw new ConfigError(
103
+ `${source}: ${formatSecretFindings(secrets)}`,
104
+ secrets.map((finding) => finding.path),
105
+ );
106
+ }
107
+
108
+ try {
109
+ return defineProvider(raw);
110
+ } catch (error) {
111
+ throw new ConfigError(`${source}: ${(error as Error).message}`);
112
+ }
113
+ }
114
+
115
+ /** A starting point, written out by `lanes link provider new`. */
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The scaffold `lanes link` writes, one per connectivity type.
3
+ *
4
+ * A custom provider is the same declaration a built-in is — this is the *only*
5
+ * difference between `#providers/google/gmail/` and a file an operator drops in
6
+ * their profile's `providers.d/`, and the point of the whole manifest design.
7
+ * So the template offers one starting point per `connector.kind` rather than a
8
+ * single generic one that would have to be edited into shape.
9
+ */
10
+
11
+ export function manifestTemplate(kind: 'mcp' | 'http' | 'imap' | 'dav' | 'fs'): string {
12
+ if (kind === 'mcp') {
13
+ return `# A custom MCP server — a vendor's, a colleague's, or your own.
14
+ #
15
+ # If the server supports Dynamic Client Registration there is nothing else to
16
+ # do: Lanes Link registers itself when you run \`lanes link connect\`.
17
+ id: acme
18
+ name: Acme
19
+ description: Whatever this server exposes.
20
+
21
+ connector:
22
+ kind: mcp
23
+ endpoint: https://mcp.acme.com/mcp
24
+
25
+ auth:
26
+ kind: oauth
27
+ registration: dynamic # or "manual" if the vendor makes you register an app
28
+ `;
29
+ }
30
+
31
+ if (kind === 'imap') {
32
+ return `# Any mailbox: Fastmail, mailbox.org, a company Dovecot.
33
+ #
34
+ # There is no spec to read — IMAP describes its extensions but never its
35
+ # operations — so the capability set is fixed by the protocol. Reading never
36
+ # marks mail as read, and nothing here can delete any.
37
+ id: fastmail
38
+ name: Fastmail
39
+
40
+ connector:
41
+ kind: imap
42
+ host: imap.fastmail.com
43
+ port: 993
44
+ smtp:
45
+ host: smtp.fastmail.com
46
+ port: 465
47
+ starttls: false # 465 is implicit TLS; 587 upgrades in-band
48
+
49
+ auth:
50
+ kind: basic # a username and an app password, per account
51
+
52
+ identity:
53
+ kind: connector # the account is the name the server accepted
54
+
55
+ setup:
56
+ docs: "Create an app password at https://app.fastmail.com/settings/security/apppw"
57
+ prompts:
58
+ - key: username
59
+ label: Email address
60
+ scope: connection
61
+ field: username
62
+ - key: password
63
+ label: App password
64
+ secret: true
65
+ scope: connection
66
+ field: password
67
+ `;
68
+ }
69
+
70
+ if (kind === 'fs') {
71
+ return `# A folder on this machine — iCloud Drive, Dropbox, a project directory.
72
+ #
73
+ # No credential: the permission is the operating system's, so this only works
74
+ # where the files actually are. Everything under the root is reachable and
75
+ # nothing above it is, symlinks included.
76
+ id: dropbox
77
+ name: Dropbox
78
+
79
+ connector:
80
+ kind: fs
81
+ root: ~/Dropbox
82
+ max_file_bytes: 262144 # one read; a large file is refused rather than truncated
83
+ exclude: [] # on top of .git, .ssh, node_modules, which are always refused
84
+
85
+ auth: { kind: none }
86
+ identity: { kind: connector }
87
+ `;
88
+ }
89
+
90
+ if (kind === 'dav') {
91
+ return `# Any CalDAV or CardDAV server: Nextcloud, Radicale, Fastmail.
92
+ #
93
+ # Discovery walks .well-known, then the principal, then the collection home —
94
+ # and remembers the result per account, since many servers answer from a
95
+ # per-user host.
96
+ id: nextcloud_calendar
97
+ name: Nextcloud Calendar
98
+
99
+ connector:
100
+ kind: dav
101
+ base_url: https://cloud.example.com
102
+ service: caldav # or carddav, for contacts
103
+
104
+ auth:
105
+ kind: basic
106
+
107
+ identity:
108
+ kind: connector
109
+
110
+ setup:
111
+ docs: "Use an app password from Settings → Security."
112
+ prompts:
113
+ - key: username
114
+ label: Username
115
+ scope: connection
116
+ field: username
117
+ - key: password
118
+ label: App password
119
+ secret: true
120
+ scope: connection
121
+ field: password
122
+ `;
123
+ }
124
+
125
+ return `# A REST API described by OpenAPI.
126
+ #
127
+ # Operations become capabilities automatically — GET and HEAD land in the
128
+ # "read" bundle (granted by default), everything else in "write".
129
+ id: acme
130
+ name: Acme
131
+ description: Whatever this API exposes.
132
+
133
+ connector:
134
+ kind: http
135
+ base_url: https://api.acme.com/v1
136
+ openapi: https://api.acme.com/openapi.json # a URL, or a path in this workspace
137
+ operations:
138
+ # A large spec yields hundreds of tools, which no agent can reason over.
139
+ # Narrow it by operationId, path, or tag.
140
+ include: ["*Account*", "*Payment*"]
141
+ exclude: []
142
+
143
+ auth:
144
+ kind: header
145
+ header: X-API-Key
146
+ credential_ref: acme/api_key # a reference; never the key itself
147
+
148
+ setup:
149
+ docs: "Generate a key at https://acme.com/settings/api"
150
+ prompts:
151
+ - key: api_key
152
+ label: Acme API key
153
+ secret: true
154
+ credential_ref: acme/api_key
155
+ `;
156
+ }
@@ -0,0 +1,207 @@
1
+ import { z } from 'zod';
2
+ import { defineLocalProvider, keepKeys, type ProviderContext } from '#connectivity';
3
+
4
+ /**
5
+ * The example provider — the reference every other provider is written
6
+ * against, and small enough to reproduce verbatim in
7
+ * `docs/detailed/creating-a-provider.md`.
8
+ *
9
+ * It is also load-bearing beyond being a sample. It declares no auth
10
+ * requirement and touches no third-party service, which makes it an *owner
11
+ * provider* in miniature — the same shape memory, skills, and vault take in
12
+ * M3. If this works, that shape is proven before those exist, and `lanes link connect
13
+ * example` opening no browser proves the CLI does not assume OAuth-shaped
14
+ * setup.
15
+ *
16
+ * Note what is absent: no `connection` argument is declared anywhere. Core
17
+ * injects it, fills its enum per profile from resolved policy, and resolves it
18
+ * to `context.connection` before a handler runs. A provider never thinks about
19
+ * which account it is serving — that is ADR-001, and it is what lets one tool
20
+ * set scale to any number of accounts.
21
+ */
22
+
23
+ const NOTE_KEY_PREFIX = 'note:';
24
+
25
+ async function readNote(context: ProviderContext, key: string): Promise<string | null> {
26
+ return context.state.get(`${NOTE_KEY_PREFIX}${key}`);
27
+ }
28
+
29
+ export const exampleProvider = defineLocalProvider({
30
+ id: 'example',
31
+ name: 'Example',
32
+ version: '1.0.0',
33
+ description:
34
+ 'A trivial provider with no external service. Serves as the provider SDK reference and as a way to exercise connection isolation without any credentials.',
35
+
36
+ configSchema: z.object({}),
37
+ connectionSchema: z.object({}),
38
+
39
+ bundles: [
40
+ {
41
+ name: 'read',
42
+ description: 'Read notes and echo messages.',
43
+ oauth_scopes: [],
44
+ capabilities: ['echo', 'get_note', 'list_notes'],
45
+ default: true,
46
+ },
47
+ {
48
+ name: 'write',
49
+ description: 'Create and modify notes.',
50
+ oauth_scopes: [],
51
+ capabilities: ['set_note', 'delete_note'],
52
+ },
53
+ ],
54
+
55
+ capabilities: [
56
+ {
57
+ kind: 'tool',
58
+ name: 'echo',
59
+ title: 'Echo a message',
60
+ description:
61
+ 'Return the supplied message unchanged, prefixed with the connection it was routed to. Useful for confirming which account a call reached.',
62
+ inputSchema: z.object({
63
+ message: z.string().min(1).describe('Text to echo back'),
64
+ }),
65
+ // The message is the whole payload, so recording it verbatim is the
66
+ // useful choice here. A provider handling real content would not.
67
+ redact: keepKeys('message'),
68
+ async handler({ message }, context) {
69
+ return {
70
+ content: [{ type: 'text', text: `[${context.connection.key}] ${message}` }],
71
+ };
72
+ },
73
+ },
74
+
75
+ {
76
+ kind: 'tool',
77
+ name: 'set_note',
78
+ title: 'Store a note',
79
+ description:
80
+ 'Store a note against this connection. Notes are scoped to the connection, so the same key holds a different value on each one.',
81
+ inputSchema: z.object({
82
+ key: z.string().min(1).describe('Note identifier'),
83
+ value: z.string().describe('Note contents'),
84
+ }),
85
+ // The key is safe to record; the value is the content itself.
86
+ redact: keepKeys('key'),
87
+ async handler({ key, value }, context) {
88
+ await context.state.set(`${NOTE_KEY_PREFIX}${key}`, value);
89
+ context.audit.annotate({ bytes: value.length });
90
+
91
+ return {
92
+ content: [{ type: 'text', text: `Stored note "${key}" on ${context.connection.key}.` }],
93
+ };
94
+ },
95
+ },
96
+
97
+ {
98
+ kind: 'tool',
99
+ name: 'get_note',
100
+ title: 'Read a note',
101
+ description: 'Read a note stored against this connection.',
102
+ inputSchema: z.object({
103
+ key: z.string().min(1).describe('Note identifier'),
104
+ }),
105
+ redact: keepKeys('key'),
106
+ async handler({ key }, context) {
107
+ const value = await readNote(context, key);
108
+
109
+ if (value === null) {
110
+ return {
111
+ content: [
112
+ {
113
+ type: 'text',
114
+ text: `No note "${key}" on ${context.connection.key}.`,
115
+ },
116
+ ],
117
+ isError: true,
118
+ };
119
+ }
120
+
121
+ return { content: [{ type: 'text', text: value }] };
122
+ },
123
+ },
124
+
125
+ {
126
+ kind: 'tool',
127
+ name: 'delete_note',
128
+ title: 'Delete a note',
129
+ description: 'Remove a note from this connection.',
130
+ inputSchema: z.object({
131
+ key: z.string().min(1).describe('Note identifier'),
132
+ }),
133
+ redact: keepKeys('key'),
134
+ async handler({ key }, context) {
135
+ await context.state.delete(`${NOTE_KEY_PREFIX}${key}`);
136
+ return {
137
+ content: [{ type: 'text', text: `Deleted note "${key}" from ${context.connection.key}.` }],
138
+ };
139
+ },
140
+ },
141
+
142
+ {
143
+ kind: 'tool',
144
+ name: 'list_notes',
145
+ title: 'List note keys',
146
+ description: 'List the keys of every note stored against this connection.',
147
+ inputSchema: z.object({}),
148
+ async handler(_input, context) {
149
+ const keys = (await context.state.keys())
150
+ .filter((key) => key.startsWith(NOTE_KEY_PREFIX))
151
+ .map((key) => key.slice(NOTE_KEY_PREFIX.length));
152
+
153
+ return {
154
+ content: [
155
+ {
156
+ type: 'text',
157
+ text:
158
+ keys.length > 0
159
+ ? keys.join('\n')
160
+ : `No notes on ${context.connection.key} yet.`,
161
+ },
162
+ ],
163
+ };
164
+ },
165
+ },
166
+
167
+ /**
168
+ * A resource rather than a tool, deliberately — ADR-006.
169
+ *
170
+ * A note is read-oriented structured context addressed by a stable
171
+ * identifier, which is exactly what resources are for. Making everything a
172
+ * tool is the easy default and the wrong one; the distinction matters much
173
+ * more for M3's memory and skills than it does here, so it is worth
174
+ * exercising now while the cost of getting it wrong is nil.
175
+ */
176
+ {
177
+ kind: 'resource',
178
+ name: 'note',
179
+ title: 'Note',
180
+ description: 'A stored note, addressed by key.',
181
+ uriTemplate: 'example://note/{key}',
182
+ mimeType: 'text/plain',
183
+
184
+ async list(context) {
185
+ const keys = await context.state.keys();
186
+ return keys
187
+ .filter((key) => key.startsWith(NOTE_KEY_PREFIX))
188
+ .map((key) => {
189
+ const name = key.slice(NOTE_KEY_PREFIX.length);
190
+ return { uri: `example://note/${encodeURIComponent(name)}`, name };
191
+ });
192
+ },
193
+
194
+ async read(uri, params, context) {
195
+ const key = params['key'];
196
+ if (!key) throw new Error(`Malformed note URI: ${uri}`);
197
+
198
+ const value = await readNote(context, decodeURIComponent(key));
199
+ if (value === null) throw new Error(`No note "${key}" on ${context.connection.key}`);
200
+
201
+ return { uri, mimeType: 'text/plain', text: value };
202
+ },
203
+ },
204
+ ],
205
+ });
206
+
207
+ export default exampleProvider;
@@ -0,0 +1,66 @@
1
+ import { defineProvider } from '#connectivity';
2
+ import { CALENDAR_IDENTITY, GOOGLE_APP, GOOGLE_OAUTH, specPath } from '../shared/oauth.ts';
3
+ import { googleSetup } from '../shared/setup.ts';
4
+ import { CALENDAR_REDACT } from './redact.ts';
5
+
6
+ /**
7
+ * Read and write events without holding the calendar.
8
+ *
9
+ * Google splits Calendar the way it splits Drive, and here the split is usable.
10
+ * `calendar.events` reaches every event on every calendar and reaches nothing
11
+ * else: it cannot create a calendar, delete one, or change who it is shared
12
+ * with. Those are `auth/calendar`, which is this provider's `mail.google.com`
13
+ * and is not requested.
14
+ *
15
+ * `calendar.readonly` sits beside it and is not redundant. Two of the reads
16
+ * accept nothing narrower — listing the calendars, and `freeBusy`, which is the
17
+ * "when am I free" primitive and answers across calendars whose contents the
18
+ * token may not read. Dropping it would cost the two operations an assistant
19
+ * needs most.
20
+ *
21
+ * `calendar.events` is marked broad in `../shared/scopes.ts` and should stay
22
+ * marked. Two narrower scopes do exist — `calendar.events.owned` and
23
+ * `calendar.app.created`, Calendar's answer to `drive.file` — and neither is
24
+ * usable: the vendored spec's per-operation `security` blocks name only the
25
+ * scopes Google published when it was generated, so requesting one would fail
26
+ * `specs.test.ts` and, more to the point, would fail at Google.
27
+ * `calendar.app.created` also has the `drive.file` disease in a worse form — it
28
+ * reaches only calendars this app made, and the calendar anyone means is the
29
+ * primary one they already had.
30
+ *
31
+ * Note the `base_url`: Calendar carries its version in the host, like Drive
32
+ * (`/calendar/v3` + `/calendars/{id}/events`). Copying the Sheets shape yields
33
+ * `/v3/v3/...` and a 404 on every call.
34
+ */
35
+ const CALENDAR_SCOPES = [
36
+ 'https://www.googleapis.com/auth/calendar.readonly',
37
+ 'https://www.googleapis.com/auth/calendar.events',
38
+ ];
39
+
40
+ export const calendar = defineProvider({
41
+ id: 'calendar',
42
+ name: 'Google Calendar',
43
+ description:
44
+ 'Read and write calendar events — list, search, create, reschedule, and cancel — and answer when you are free, via the Calendar REST API.',
45
+ connector: {
46
+ kind: 'http',
47
+ base_url: 'https://www.googleapis.com/calendar/v3',
48
+ openapi: specPath('calendar.v3.json'),
49
+ },
50
+ auth: {
51
+ kind: 'oauth',
52
+ registration: 'manual',
53
+ app: GOOGLE_APP,
54
+ scopes: CALENDAR_SCOPES,
55
+ ...GOOGLE_OAUTH,
56
+ },
57
+ identity: CALENDAR_IDENTITY,
58
+ setup: googleSetup('Calendar', CALENDAR_SCOPES, {
59
+ // `calendar-json.googleapis.com`, not `calendar.googleapis.com`. The
60
+ // service is registered under its discovery host, and enabling the
61
+ // plausible-looking name leaves consent succeeding and every call
62
+ // answering 403 — the failure this whole setup block exists to prevent.
63
+ apis: ['calendar-json.googleapis.com'],
64
+ }),
65
+ redact: CALENDAR_REDACT,
66
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * What survives into the audit log when an event is read or changed.
3
+ *
4
+ * The generated tools flatten a request body into top-level arguments, so
5
+ * `summary`, `description`, `location`, and `attendees` really are argument
6
+ * names here — and they are the meeting. Keeping them would put "1:1 re
7
+ * redundancy consultation" and the list of who was invited in a log meant to
8
+ * record that a change happened, not to hold a second copy of the calendar.
9
+ *
10
+ * `sendUpdates` is kept on every write, and it is the one flag worth reading
11
+ * back: cancelling a meeting quietly and cancelling it with mail to everyone
12
+ * invited are different acts, and only this argument tells them apart.
13
+ *
14
+ * `q` is withheld on Gmail's reasoning — a search is a question, and "who am I
15
+ * meeting about the redundancy" is content whoever typed it did not expect to
16
+ * keep. `freebusy.query`'s `items` is withheld on the same ground, which is the
17
+ * one call worth explaining: `calendarId` is kept everywhere else because it
18
+ * names the resource being read or written, but a `freeBusy` `items` list is a
19
+ * set of *people you asked about*. That is the query, not the subject.
20
+ */
21
+ export const CALENDAR_REDACT: Record<string, string[]> = {
22
+ 'calendarList.list': ['minAccessRole', 'showHidden', 'maxResults'],
23
+ 'events.list': [
24
+ 'calendarId',
25
+ 'timeMin',
26
+ 'timeMax',
27
+ 'singleEvents',
28
+ 'orderBy',
29
+ 'maxResults',
30
+ 'showDeleted',
31
+ 'eventTypes',
32
+ ],
33
+ 'events.get': ['calendarId', 'eventId'],
34
+ 'events.instances': ['calendarId', 'eventId', 'timeMin', 'timeMax', 'maxResults'],
35
+ 'freebusy.query': ['timeMin', 'timeMax', 'timeZone'],
36
+ 'events.insert': ['calendarId', 'sendUpdates', 'conferenceDataVersion'],
37
+ 'events.patch': ['calendarId', 'eventId', 'sendUpdates'],
38
+ 'events.delete': ['calendarId', 'eventId', 'sendUpdates'],
39
+ 'events.move': ['calendarId', 'eventId', 'destination', 'sendUpdates'],
40
+ };
@@ -0,0 +1,50 @@
1
+ import { defineProvider } from '#connectivity';
2
+ import { GOOGLE_APP, GOOGLE_OAUTH, PEOPLE_IDENTITY, specPath } from '../shared/oauth.ts';
3
+ import { googleSetup } from '../shared/setup.ts';
4
+ import { CONTACTS_REDACT } from './redact.ts';
5
+
6
+ /**
7
+ * Read-only, so that "email Bob" resolves to an address.
8
+ *
9
+ * Two scopes, because Google keeps contacts in two places.
10
+ * `contacts.readonly` is the address book someone curated;
11
+ * `contacts.other.readonly` is where Gmail files an address written to but
12
+ * never saved, which is where most of the Bobs actually are. Asking for only
13
+ * the first would answer confidently and wrongly for the common case.
14
+ *
15
+ * Both are read-only, so neither is marked broad. The write scope is
16
+ * `contacts`, it permanently deletes, and it is not requested — nothing here
17
+ * writes. A contact card is the shape where a round trip silently destroys
18
+ * data, which is the same reason `icloud_contacts` declines to edit one.
19
+ *
20
+ * The provider is `contacts` while Google's API is People. That mismatch is
21
+ * load-bearing in `redact.ts` — see the note there — and it is deliberate: the
22
+ * thing an operator connects is their contacts, and `icloud_contacts` is
23
+ * already the neighbour in the list.
24
+ */
25
+ const CONTACTS_SCOPES = [
26
+ 'https://www.googleapis.com/auth/contacts.readonly',
27
+ 'https://www.googleapis.com/auth/contacts.other.readonly',
28
+ ];
29
+
30
+ export const contacts = defineProvider({
31
+ id: 'contacts',
32
+ name: 'Google Contacts',
33
+ description:
34
+ 'Look up a saved contact by name to find their address or phone number, including the addresses Gmail saved automatically. Read-only, via the People REST API.',
35
+ connector: {
36
+ kind: 'http',
37
+ base_url: 'https://people.googleapis.com',
38
+ openapi: specPath('people.v1.json'),
39
+ },
40
+ auth: {
41
+ kind: 'oauth',
42
+ registration: 'manual',
43
+ app: GOOGLE_APP,
44
+ scopes: CONTACTS_SCOPES,
45
+ ...GOOGLE_OAUTH,
46
+ },
47
+ identity: PEOPLE_IDENTITY,
48
+ setup: googleSetup('Contacts', CONTACTS_SCOPES, { apis: ['people.googleapis.com'] }),
49
+ redact: CONTACTS_REDACT,
50
+ });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * What survives into the audit log when contacts are searched.
3
+ *
4
+ * The keys carry a `people.` prefix that no other Google provider here needs,
5
+ * and it is not a typo. `shortenName` strips the *provider id* from an
6
+ * operationId, and this provider is `contacts` while Google's operationIds all
7
+ * say `people` — so nothing is stripped, and the capability really is called
8
+ * `people.people.searchContacts`. A key written `people.searchContacts` would
9
+ * match nothing, withhold every value, and look exactly like redaction working.
10
+ *
11
+ * `query` is withheld throughout. A name someone is looking up is content, and
12
+ * it is the line `icloud_contacts.search_contacts` already draws. What is kept
13
+ * is the shape of the request: how many were asked for, which fields, and — on
14
+ * `getBatchGet` — the opaque resource names, which are identifiers rather than
15
+ * anything a person wrote.
16
+ */
17
+ export const CONTACTS_REDACT: Record<string, string[]> = {
18
+ 'people.people.searchContacts': ['pageSize', 'readMask'],
19
+ 'people.otherContacts.search': ['pageSize', 'readMask'],
20
+ 'people.people.getBatchGet': ['resourceNames', 'personFields'],
21
+ };