@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,146 @@
1
+ import type { ProviderManifest } from './provider.ts';
2
+ import type { SetupPrompt } from './setup.ts';
3
+ import { credentialRefForConnection } from './credential-ref.ts';
4
+
5
+ /**
6
+ * What a provider needs in the credential store before `connect` can finish.
7
+ *
8
+ * Derived from the manifest's setup prompts and nothing else — pure, with no
9
+ * store access — because three callers have to agree on the answer: the
10
+ * non-interactive preflight in `connect`, `lanes link setup plan`, and the
11
+ * read-only `setup.provider` capability. If the tool that tells someone what to
12
+ * run computed this differently from the command they then run, the command
13
+ * would not work, and the failure would look like a bug in `connect`.
14
+ *
15
+ * The split between here and `missingRequirements` is the one that matters:
16
+ * this file says what setup *requires*, which is a property of shipped code.
17
+ * Whether a value is *satisfied* needs the credential store, and only the CLI
18
+ * ever asks that — ADR-007.
19
+ */
20
+
21
+ /** The placeholder id used when no connection has been named yet. */
22
+ export const UNNAMED_ID = '<id>';
23
+
24
+ export interface SetupRequirement {
25
+ /** Where the value goes. */
26
+ readonly ref: string;
27
+ /** What the value is, in the manifest's own words. */
28
+ readonly label: string;
29
+ readonly secret: boolean;
30
+ readonly scope: 'shared' | 'connection';
31
+ /** The prompt keys this one ref covers — two, for `basic`. */
32
+ readonly prompts: readonly string[];
33
+ /** The exact command that stores it. */
34
+ readonly command: string;
35
+ }
36
+
37
+ export interface SetupNeeds {
38
+ readonly requirements: readonly SetupRequirement[];
39
+ /**
40
+ * A connection-scoped value cannot be placed without an id.
41
+ *
42
+ * The ref derives from the connection id, which `connect` does not settle
43
+ * until it has asked the provider whose account the credential belongs to.
44
+ * Without `--id` the ref would be `<provider>/pending`, which is never what
45
+ * anyone wants — so a non-interactive run has to be told the name up front.
46
+ */
47
+ readonly needsId: boolean;
48
+ /**
49
+ * The client comes from a broker, so there is nothing here to supply.
50
+ *
51
+ * Distinct from an empty `requirements`: a provider that needs nothing and a
52
+ * provider whose client somebody else operates read the same in a list of
53
+ * requirements and mean different things to a person deciding what to do
54
+ * next. A form built from this would render a required field for a value
55
+ * `connect` will never ask for.
56
+ */
57
+ readonly brokered: boolean;
58
+ }
59
+
60
+ /**
61
+ * Build the `secrets set` line for one ref.
62
+ *
63
+ * Spelled exactly as `secrets set` spells it in its own errors, because a
64
+ * command someone is told to paste and a command the CLI suggests should not be
65
+ * two different sentences.
66
+ */
67
+ function storeCommand(ref: string, placeholder: string, profile: string): string {
68
+ return `printf %s "${placeholder}" | lanes link secrets set ${ref} --profile ${profile}`;
69
+ }
70
+
71
+ /** How the value is spelled, for a ref that several prompts combine into. */
72
+ function placeholderFor(prompts: readonly SetupPrompt[]): string {
73
+ const username = prompts.find((prompt) => prompt.field === 'username');
74
+ const password = prompts.find((prompt) => prompt.field === 'password');
75
+
76
+ // RFC 7617's own encoding, which is what `ensureStaticCredential` writes.
77
+ // Showing `<value>` here would be a command that stores half a credential.
78
+ if (username && password) return '<username>:<password>';
79
+
80
+ return '<value>';
81
+ }
82
+
83
+ export function setupRequirements(
84
+ manifest: ProviderManifest,
85
+ connectionId: string | undefined,
86
+ profile: string,
87
+ options: {
88
+ /** `oauth_apps` entries this profile declares — the clients that are its own. */
89
+ readonly ownClients?: readonly string[];
90
+ } = {},
91
+ ): SetupNeeds {
92
+ const prompts = manifest.setup?.prompts ?? [];
93
+
94
+ // A shared prompt exists to collect a client the operator registers. When a
95
+ // broker supplies one and this profile has not declared its own, there is
96
+ // nothing to collect — the prompts stay in the manifest because `--own-client`
97
+ // still needs them, but they are not what this connect will ask for.
98
+ const brokered =
99
+ manifest.auth.kind === 'oauth' &&
100
+ manifest.auth.broker !== undefined &&
101
+ !(manifest.auth.app !== undefined && (options.ownClients ?? []).includes(manifest.auth.app));
102
+
103
+ const shared = brokered ? [] : prompts.filter((prompt) => prompt.scope === 'shared');
104
+ const perConnection = prompts.filter((prompt) => prompt.scope === 'connection');
105
+
106
+ const requirements: SetupRequirement[] = [];
107
+
108
+ // A shared prompt names its own ref: nothing about a connection identifies an
109
+ // OAuth client, so the manifest has to say where it lives.
110
+ for (const prompt of shared) {
111
+ if (!prompt.credential_ref) continue;
112
+ requirements.push({
113
+ ref: prompt.credential_ref,
114
+ label: prompt.label,
115
+ secret: prompt.secret,
116
+ scope: 'shared',
117
+ prompts: [prompt.key],
118
+ command: storeCommand(prompt.credential_ref, '<value>', profile),
119
+ });
120
+ }
121
+
122
+ // Connection-scoped prompts derive one ref, and `basic` puts two answers in
123
+ // it. Grouping by the resolved ref rather than emitting one requirement per
124
+ // prompt is what keeps the emitted command correct for iCloud, where two
125
+ // separate `secrets set` calls would leave the second overwriting the first.
126
+ if (perConnection.length > 0) {
127
+ const ref = credentialRefForConnection(manifest, connectionId ?? UNNAMED_ID);
128
+
129
+ if (ref) {
130
+ requirements.push({
131
+ ref,
132
+ label: perConnection.map((prompt) => prompt.label).join(', then '),
133
+ secret: perConnection.some((prompt) => prompt.secret),
134
+ scope: 'connection',
135
+ prompts: perConnection.map((prompt) => prompt.key),
136
+ command: storeCommand(ref, placeholderFor(perConnection), profile),
137
+ });
138
+ }
139
+ }
140
+
141
+ return {
142
+ requirements,
143
+ needsId: perConnection.length > 0 && connectionId === undefined,
144
+ brokered,
145
+ };
146
+ }
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod';
2
+ import { credentialRef } from './primitives.ts';
3
+
4
+ /**
5
+ * What the CLI renders before asking for anything.
6
+ *
7
+ * The rest of the system stays domain-agnostic because it renders whatever a
8
+ * provider declares. `docs` alone covers the common "here is where to generate
9
+ * that key" case with no code at all.
10
+ */
11
+
12
+ export const setupPromptSchema = z.object({
13
+ key: z.string().min(1),
14
+ label: z.string().min(1),
15
+ secret: z.boolean().default(false),
16
+ /**
17
+ * `shared` — one value for the whole profile, such as an OAuth client. It
18
+ * needs an explicit ref, because nothing about a connection identifies it.
19
+ *
20
+ * `connection` — one value per account, so the ref *derives* and must not be
21
+ * written: a manifest cannot name a connection that does not exist yet, and a
22
+ * second copy of the answer could only ever disagree with the first.
23
+ */
24
+ scope: z.enum(['shared', 'connection']).default('shared'),
25
+ /**
26
+ * Which part of a composite credential this answers.
27
+ *
28
+ * `basic` needs two values in one credential and stores them as
29
+ * `username:password` — RFC 7617's own encoding, so the store holds exactly
30
+ * what the header carries and nothing has to agree about a JSON shape.
31
+ */
32
+ field: z.enum(['value', 'username', 'password']).default('value'),
33
+ credential_ref: credentialRef.optional(),
34
+ });
35
+
36
+ export const setupSchema = z.object({
37
+ summary: z.string().optional(),
38
+ docs: z.string().optional(),
39
+ docs_url: z.url().optional(),
40
+ steps: z.array(z.string()).default([]),
41
+ prompts: z.array(setupPromptSchema).default([]),
42
+ /**
43
+ * One line appended when authentication fails, in the provider's own words.
44
+ *
45
+ * A transport can say "the server refused the credential" and no more, because
46
+ * it must not know which vendor it is talking to. The *provider* knows the
47
+ * likely cause — for Apple it is nearly always an Account password used where
48
+ * an app-specific password belongs — and that sentence is worth far more than
49
+ * the status code. Declared here so the transport stays vendor-free.
50
+ */
51
+ troubleshooting: z.string().optional(),
52
+ });
53
+
54
+ export type SetupDeclaration = z.infer<typeof setupSchema>;
55
+ export type SetupPrompt = z.infer<typeof setupPromptSchema>;
@@ -0,0 +1,163 @@
1
+ import { z } from 'zod';
2
+ import type { SecretRef } from '#secrets';
3
+ import type { Capability } from './capability.ts';
4
+ import type { ProviderContext } from './context.ts';
5
+ import { bundleSchema, defineProvider, type ProviderManifest } from './manifest/index.ts';
6
+
7
+ /**
8
+ * Local providers — the only kind that still carries code.
9
+ *
10
+ * Everything reachable over MCP or HTTP is a manifest; this exists for
11
+ * capabilities that *are* ours: `example`, and the M3 owner layer (memory,
12
+ * skills, vault), which hold no third-party account and could not be expressed
13
+ * as an upstream call.
14
+ *
15
+ * A local provider is a manifest **plus** handlers, so the registry holds one
16
+ * shape regardless of connectivity.
17
+ */
18
+
19
+ /** Local providers hold no third-party account, so this is always `none`. */
20
+ export type AuthRequirement = { readonly kind: 'none' };
21
+
22
+ export interface ProviderDefinition<
23
+ ConfigSchema extends z.ZodType = z.ZodType,
24
+ ConnectionSchema extends z.ZodType = z.ZodType,
25
+ > {
26
+ readonly manifest: ProviderManifest;
27
+
28
+ /** Provider-level settings from the `providers:` block. */
29
+ readonly configSchema: ConfigSchema;
30
+ /** Per-connection settings from a `connections:` entry. */
31
+ readonly connectionSchema: ConnectionSchema;
32
+
33
+ readonly capabilities: readonly Capability[];
34
+
35
+ /**
36
+ * Which credential refs a connection may read. Core turns this into the
37
+ * allowlist behind `ProviderContext.credentials`, so a provider cannot reach
38
+ * a ref it did not declare.
39
+ */
40
+ credentialRefs?(connectionId: string, config: unknown): readonly SecretRef[];
41
+
42
+ /** Optional readiness probe surfaced by `doctor`. Never called during dispatch. */
43
+ healthcheck?(context: ProviderContext): Promise<{ ok: boolean; detail?: string }>;
44
+ }
45
+
46
+ export interface LocalProviderInput<C extends z.ZodType, N extends z.ZodType> {
47
+ readonly id: string;
48
+ readonly name: string;
49
+ readonly version?: string;
50
+ readonly description?: string;
51
+ readonly configSchema: C;
52
+ readonly connectionSchema: N;
53
+ readonly capabilities: readonly Capability[];
54
+ readonly bundles?: readonly z.input<typeof bundleSchema>[];
55
+ readonly redact?: ProviderManifest['redact'];
56
+ credentialRefs?(connectionId: string, config: unknown): readonly SecretRef[];
57
+ healthcheck?(context: ProviderContext): Promise<{ ok: boolean; detail?: string }>;
58
+ }
59
+
60
+ /**
61
+ * Define a local provider. Validates eagerly, so a malformed one fails at
62
+ * import rather than at first invocation.
63
+ */
64
+ export function defineLocalProvider<C extends z.ZodType, N extends z.ZodType>(
65
+ input: LocalProviderInput<C, N>,
66
+ ): ProviderDefinition<C, N> {
67
+ const seen = new Set<string>();
68
+ for (const capability of input.capabilities) {
69
+ if (seen.has(capability.name)) {
70
+ throw new Error(`Provider ${input.id} declares capability ${capability.name} twice`);
71
+ }
72
+ seen.add(capability.name);
73
+ }
74
+
75
+ for (const bundle of input.bundles ?? []) {
76
+ for (const name of bundle.capabilities ?? []) {
77
+ // Local bundles name capabilities exactly; globs are for discovered ones,
78
+ // where we do not know the names in advance.
79
+ if (!name.includes('*') && !seen.has(name)) {
80
+ throw new Error(
81
+ `Provider ${input.id} bundle "${bundle.name}" references unknown capability "${name}"`,
82
+ );
83
+ }
84
+ }
85
+ }
86
+
87
+ const manifest = defineProvider({
88
+ id: input.id,
89
+ name: input.name,
90
+ version: input.version ?? '1.0.0',
91
+ description: input.description ?? '',
92
+ connector: { kind: 'local' },
93
+ auth: { kind: 'none' },
94
+ ...(input.bundles ? { bundles: input.bundles } : {}),
95
+ ...(input.redact ? { redact: input.redact } : {}),
96
+ });
97
+
98
+ return {
99
+ manifest,
100
+ configSchema: input.configSchema,
101
+ connectionSchema: input.connectionSchema,
102
+ capabilities: input.capabilities,
103
+ ...(input.credentialRefs ? { credentialRefs: input.credentialRefs } : {}),
104
+ ...(input.healthcheck ? { healthcheck: input.healthcheck } : {}),
105
+ };
106
+ }
107
+
108
+ /**
109
+ * A manifest-based provider that also carries a few capabilities of its own.
110
+ *
111
+ * The exception, not the pattern. A provider is a declaration precisely so that
112
+ * `providers/gmail` can be fifteen lines of data instead of six hundred lines of
113
+ * endpoint translation, and every capability authored here is a step back
114
+ * towards the latter. Reach for it only where the vendor's API can do something
115
+ * its *document* cannot express — the case that forced it is mail, where sending
116
+ * means handing over one assembled RFC 2822 message and no OpenAPI document
117
+ * describes composing one, so the generated tool obliges the caller to build it
118
+ * and therefore to carry the attachment as base64.
119
+ *
120
+ * Everything else stays as declared: the connector kind, the auth, the discovered
121
+ * capabilities. `createCompositeConnector` answers the authored names and
122
+ * delegates the rest, and the registry adds both sets together with authored
123
+ * winning a collision.
124
+ *
125
+ * There is no `configSchema` or `connectionSchema` argument. Those describe
126
+ * settings a *local* provider reads from the profile, and a manifest provider's
127
+ * connection is already described by its manifest — so they are permissive here
128
+ * rather than inviting a second place to declare the same thing.
129
+ */
130
+ export function defineProviderWithCapabilities(input: {
131
+ readonly manifest: ProviderManifest;
132
+ readonly capabilities: readonly Capability[];
133
+ }): ProviderDefinition {
134
+ if (input.manifest.connector.kind === 'local') {
135
+ throw new Error(
136
+ `Provider "${input.manifest.id}" is a local connector, so it should use defineLocalProvider — there is no remote half to compose with.`,
137
+ );
138
+ }
139
+
140
+ if (input.capabilities.length === 0) {
141
+ throw new Error(
142
+ `Provider "${input.manifest.id}" authors no capabilities, so it is an ordinary manifest — register it directly.`,
143
+ );
144
+ }
145
+
146
+ const seen = new Set<string>();
147
+ for (const capability of input.capabilities) {
148
+ if (seen.has(capability.name)) {
149
+ throw new Error(
150
+ `Provider ${input.manifest.id} declares capability ${capability.name} twice`,
151
+ );
152
+ }
153
+ seen.add(capability.name);
154
+ }
155
+
156
+ return {
157
+ manifest: input.manifest,
158
+ // Permissive on purpose: see the note above.
159
+ configSchema: z.unknown(),
160
+ connectionSchema: z.unknown(),
161
+ capabilities: input.capabilities,
162
+ };
163
+ }
@@ -0,0 +1,33 @@
1
+ # Connectivity types
2
+
3
+ One folder per way of reaching a service. A provider declares exactly one in its
4
+ manifest's `connector.kind`; how it *authenticates* is the other axis entirely
5
+ (`../auth/`).
6
+
7
+ | Folder | `connector.kind` | In plain words |
8
+ |---|---|---|
9
+ | `http/` | `http` | a REST API, described by an OpenAPI document |
10
+ | `mcp/` | `mcp` | an upstream MCP server, proxied |
11
+ | `imap/` | `imap` | a mailbox, over IMAP4rev1 and SMTP |
12
+ | `dav/` | `dav` | calendars and contacts, over CalDAV and CardDAV |
13
+ | `fs/` | `fs` | a directory on the machine this runs on |
14
+ | `local/` | `local` | our own code — `example`, and the owner layer |
15
+
16
+ The folder is named for the `kind:` an operator writes in YAML rather than for
17
+ the friendlier word, because a second name for the same thing is exactly the
18
+ confusion this layout is meant to remove. "api" is `http/`.
19
+
20
+ ## The rule
21
+
22
+ **Protocol code, not vendor code** (ADR-008). Nothing in here may know that
23
+ iCloud exists. Where a vendor genuinely behaves differently, the difference is a
24
+ field on that transport's schema in `../manifest/connector.ts`, set by the
25
+ provider that needs it — see `fs`'s `placeholder_suffix` and `dav`'s
26
+ `max_range_days`.
27
+
28
+ ## Adding one
29
+
30
+ A folder here, a member of `connectorSchema` in `../manifest/connector.ts`, and
31
+ a case in `factory.ts`. A transport implements `Connector`: `discover()` says
32
+ what the connection exposes, `invoke()` runs one operation, and `identify()` is
33
+ optional.
@@ -0,0 +1,68 @@
1
+ import {
2
+ type AnyConnector,
3
+ type CapabilityResult,
4
+ type ConnectorContext,
5
+ type DiscoveredCapability,
6
+ type DiscoveryContext,
7
+ type ProviderDefinition,
8
+ } from '#connectivity';
9
+ import { createLocalConnector } from '../local/index.ts';
10
+
11
+ /**
12
+ * A remote connector with a few capabilities of our own on top.
13
+ *
14
+ * Every provider picks one connectivity kind, and that has held for every
15
+ * provider so far: a manifest describes how to reach a service and the service
16
+ * describes what it offers. It stops holding when the vendor's API can do
17
+ * something its *document* cannot express. A mail API is the case in hand — it
18
+ * accepts a whole assembled RFC 2822 message as one base64 field, and nothing in
19
+ * an OpenAPI document says "compose MIME from these parts". The generated tool
20
+ * therefore obliges the caller to build the message, which for anything with an
21
+ * attachment means emitting the file as base64 into the conversation. That is the
22
+ * problem this exists to remove.
23
+ *
24
+ * Three alternatives were considered and are worse:
25
+ *
26
+ * - **Teach the HTTP transport to compose mail.** It is the generic transport;
27
+ * mail knowledge in it is vendor knowledge in shared code, which
28
+ * `architecture.test.ts` refuses on purpose.
29
+ * - **A second provider holding just the authored capability.** It would need its
30
+ * own connection, so one mailbox would appear twice, consent and identity
31
+ * labelling would split, and a policy rule would have to name both.
32
+ * - **Reach the other provider's credential from a handler.** `ProviderContext`
33
+ * says plainly that a provider cannot reach another connection, and that is
34
+ * worth more than this feature.
35
+ *
36
+ * So: the definition's capabilities are answered here, everything else is
37
+ * delegated. `discover` delegates untouched, because authored capabilities come
38
+ * from the definition and must not be written into the discovery cache — they are
39
+ * code, and caching them would let a stale row outlive a rename.
40
+ */
41
+ export function createCompositeConnector(input: {
42
+ readonly definition: ProviderDefinition;
43
+ readonly remote: AnyConnector;
44
+ }): AnyConnector {
45
+ const { definition, remote } = input;
46
+ const authored = createLocalConnector(definition);
47
+ const names = new Set(definition.capabilities.map((capability) => capability.name));
48
+
49
+ return {
50
+ // The remote kind, because that is what this connection *is*: `doctor`,
51
+ // `provider list` and the setup walkthrough all describe how the account is
52
+ // reached, and "composite" would answer a question nobody asked.
53
+ kind: remote.kind,
54
+
55
+ async discover(context: DiscoveryContext): Promise<DiscoveredCapability[]> {
56
+ return remote.discover(context);
57
+ },
58
+
59
+ async invoke(capability, args, context: ConnectorContext): Promise<CapabilityResult> {
60
+ return names.has(capability.name)
61
+ ? authored.invoke(capability, args, context)
62
+ : remote.invoke(capability, args, context);
63
+ },
64
+
65
+ ...(remote.identify ? { identify: (): Promise<string | null> => remote.identify!() } : {}),
66
+ ...(remote.close ? { close: (): Promise<void> => remote.close!() } : {}),
67
+ };
68
+ }
@@ -0,0 +1,217 @@
1
+ import type { ToolResult } from '#connectivity';
2
+ import { buildEvent, compact, parseEvents, patchEvent } from './ical.ts';
3
+ import { error, json } from './request.ts';
4
+ import { CALDAV, DAV, escapeXml, findAll, parseDav, textOf } from './xml.ts';
5
+ import type { DavClient } from './client.ts';
6
+
7
+ /**
8
+ * CalDAV's five operations.
9
+ *
10
+ * Free functions over a `DavClient` rather than methods on it, so the class
11
+ * stays the transport and the feature set stays readable — the two halves were
12
+ * a 440-line class where the plumbing and the calendar semantics were
13
+ * interleaved.
14
+ */
15
+
16
+ export async function listEvents(
17
+ dav: DavClient,
18
+ args: Readonly<Record<string, unknown>>,
19
+ ): Promise<ToolResult> {
20
+ const start = String(args['start'] ?? '');
21
+ const end = String(args['end'] ?? '');
22
+ if (!start || !end) return error('start and end are required.');
23
+
24
+ const maxDays = dav.maxRangeDays;
25
+ const span = Date.parse(end) - Date.parse(start);
26
+ if (Number.isFinite(span) && span > maxDays * 86_400_000) {
27
+ return error(
28
+ `That range is longer than ${maxDays} days, which this server refuses. Ask for a narrower window.`,
29
+ );
30
+ }
31
+
32
+ const limit = Math.min(Number(args['limit'] ?? 50) || 50, 200);
33
+ const wanted = args['calendar'] ? String(args['calendar']) : undefined;
34
+
35
+ const calendars = (await dav.collections()).filter(
36
+ (collection) => !wanted || collection.name === wanted,
37
+ );
38
+ if (calendars.length === 0) {
39
+ return error(wanted ? `No calendar named "${wanted}".` : 'No calendars in this account.');
40
+ }
41
+
42
+ const from = compact(start);
43
+ const to = compact(end);
44
+
45
+ // `<C:expand>` asks the *server* to turn repeating events into instances,
46
+ // which is the whole reason there is no recurrence engine in this codebase.
47
+ const body =
48
+ `<?xml version="1.0" encoding="utf-8"?>` +
49
+ `<c:calendar-query xmlns:d="DAV:" xmlns:c="${CALDAV}">` +
50
+ `<d:prop><d:getetag/>` +
51
+ `<c:calendar-data><c:expand start="${from}" end="${to}"/></c:calendar-data>` +
52
+ `</d:prop>` +
53
+ `<c:filter><c:comp-filter name="VCALENDAR">` +
54
+ `<c:comp-filter name="VEVENT"><c:time-range start="${from}" end="${to}"/></c:comp-filter>` +
55
+ `</c:comp-filter></c:filter></c:calendar-query>`;
56
+
57
+ const events: unknown[] = [];
58
+ let expanded = true;
59
+
60
+ for (const collection of calendars) {
61
+ const result = await dav.request(collection.href, 'REPORT', body, { depth: '1' });
62
+ if (result.status >= 400) continue;
63
+
64
+ for (const response of findAll(parseDav(result.text), `{${DAV}}response`)) {
65
+ const data = textOf(response, `{${CALDAV}}calendar-data`);
66
+ if (!data) continue;
67
+
68
+ for (const event of parseEvents(data)) {
69
+ // A surviving RRULE means the server ignored `<C:expand>`; say so
70
+ // rather than presenting a master event as if it were an instance.
71
+ if (event.recurrence_rule) expanded = false;
72
+ events.push({ ...event, calendar: collection.name });
73
+ }
74
+ }
75
+ }
76
+
77
+ events.sort((a, b) =>
78
+ String((a as { start?: string }).start ?? '').localeCompare(
79
+ String((b as { start?: string }).start ?? ''),
80
+ ),
81
+ );
82
+
83
+ return json({
84
+ range: { start, end },
85
+ expanded,
86
+ ...(expanded
87
+ ? {}
88
+ : { note: 'This server did not expand repeating events; those carry recurrence_rule.' }),
89
+ events: events.slice(0, limit),
90
+ });
91
+ }
92
+
93
+ export async function createEvent(
94
+ dav: DavClient,
95
+ args: Readonly<Record<string, unknown>>,
96
+ ): Promise<ToolResult> {
97
+ const name = String(args['calendar'] ?? '');
98
+ const collection = (await dav.collections()).find((entry) => entry.name === name);
99
+ if (!collection) return error(`No calendar named "${name}".`);
100
+
101
+ const uid = `${crypto.randomUUID()}@lanes-link`;
102
+ const ical = buildEvent({
103
+ uid,
104
+ summary: String(args['summary'] ?? ''),
105
+ start: String(args['start'] ?? ''),
106
+ end: String(args['end'] ?? ''),
107
+ allDay: args['all_day'] === true,
108
+ location: args['location'] as string | undefined,
109
+ description: args['description'] as string | undefined,
110
+ timestamp: new Date().toISOString(),
111
+ });
112
+
113
+ const href = new URL(`${uid}.ics`, `${collection.href.replace(/\/?$/, '/')}`).href;
114
+
115
+ const result = await dav.request(href, 'PUT', ical, {
116
+ 'content-type': 'text/calendar; charset=utf-8',
117
+ // Refuse to overwrite: a UID collision means something is already there,
118
+ // and silently replacing someone's event is not a create.
119
+ 'if-none-match': '*',
120
+ });
121
+
122
+ if (result.status >= 400) {
123
+ return error(`The server refused the event (${result.status}). ${result.text.slice(0, 200)}`);
124
+ }
125
+
126
+ return json({ created: true, uid, calendar: name, href });
127
+ }
128
+
129
+ /**
130
+ * Find one event by UID, and keep hold of its address and ETag.
131
+ *
132
+ * The ETag is the whole point. An edit is read-modify-write against a resource
133
+ * two people may be holding, and `If-Match` is what turns a lost update into a
134
+ * refusal — without it the second writer wins silently and the first one's
135
+ * change is simply gone, with no error anywhere.
136
+ */
137
+ export async function getEvent(
138
+ dav: DavClient,
139
+ args: Readonly<Record<string, unknown>>,
140
+ ): Promise<ToolResult> {
141
+ const uid = String(args['uid'] ?? '');
142
+ const found = await dav.locate(uid, args['calendar'] ? String(args['calendar']) : undefined);
143
+ if (!found) return error(`No event with UID "${uid}".`);
144
+
145
+ const [event] = parseEvents(found.ical);
146
+ if (!event) return error(`Entry "${uid}" contains no event.`);
147
+
148
+ return json({
149
+ ...event,
150
+ calendar: found.calendar,
151
+ ...(args['include_raw'] === true ? { raw: found.ical } : {}),
152
+ });
153
+ }
154
+
155
+ export async function updateEvent(
156
+ dav: DavClient,
157
+ args: Readonly<Record<string, unknown>>,
158
+ ): Promise<ToolResult> {
159
+ const uid = String(args['uid'] ?? '');
160
+ const found = await dav.locate(uid, args['calendar'] ? String(args['calendar']) : undefined);
161
+ if (!found) return error(`No event with UID "${uid}".`);
162
+
163
+ const patched = patchEvent(found.ical, {
164
+ summary: args['summary'] as string | undefined,
165
+ start: args['start'] as string | undefined,
166
+ end: args['end'] as string | undefined,
167
+ allDay: args['all_day'] as boolean | undefined,
168
+ location: args['location'] as string | undefined,
169
+ description: args['description'] as string | undefined,
170
+ timestamp: new Date().toISOString(),
171
+ });
172
+
173
+ const reply = await dav.request(found.href, 'PUT', patched, {
174
+ 'content-type': 'text/calendar; charset=utf-8',
175
+ // Not optional. A server with no ETag to match on gets `*`, which at least
176
+ // refuses to create where we meant to replace.
177
+ 'if-match': found.etag ?? '*',
178
+ });
179
+
180
+ if (reply.status === 412) {
181
+ return error(
182
+ `That event changed since it was read, so the edit was refused rather than overwriting someone else's. Read it again and retry.`,
183
+ );
184
+ }
185
+ if (reply.status >= 400) {
186
+ return error(`The server refused the edit (${reply.status}). ${reply.text.slice(0, 200)}`);
187
+ }
188
+
189
+ const [event] = parseEvents(patched);
190
+ return json({ updated: true, uid, calendar: found.calendar, event });
191
+ }
192
+
193
+ export async function deleteEvent(
194
+ dav: DavClient,
195
+ args: Readonly<Record<string, unknown>>,
196
+ ): Promise<ToolResult> {
197
+ const uid = String(args['uid'] ?? '');
198
+ const found = await dav.locate(uid, args['calendar'] ? String(args['calendar']) : undefined);
199
+ if (!found) return error(`No event with UID "${uid}".`);
200
+
201
+ const reply = await dav.request(found.href, 'DELETE', undefined, {
202
+ 'if-match': found.etag ?? '*',
203
+ });
204
+
205
+ if (reply.status === 412) {
206
+ return error(
207
+ `That event changed since it was read, so the deletion was refused. Read it again and retry.`,
208
+ );
209
+ }
210
+ if (reply.status >= 400) {
211
+ return error(`The server refused the deletion (${reply.status}).`);
212
+ }
213
+
214
+ const [event] = parseEvents(found.ical);
215
+ return json({ deleted: true, uid, calendar: found.calendar, was: event?.summary ?? null });
216
+ }
217
+