@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,429 @@
1
+ import { z } from 'zod';
2
+ import { splitOptionalFrontmatter, stringList, withFrontmatter } from '#providers/shared/frontmatter.ts';
3
+ import {
4
+ defineLocalProvider,
5
+ keepKeys,
6
+ type BlobStore,
7
+ type ProviderDefinition,
8
+ } from '#connectivity';
9
+
10
+ /**
11
+ * `memory` — the owner's accumulated knowledge.
12
+ *
13
+ * **Reading and writing are separate capabilities**, and that separation is the
14
+ * whole security argument for this provider (ADR-012 §2, `docs/detailed/security.md`).
15
+ * Upstream content is already treated as potentially prompt-injecting and passed
16
+ * through unscreened; memory an agent can *write to* changes the shape of that
17
+ * risk rather than its size, because an injected instruction is stored once and
18
+ * re-served to every future session, including to a different agent. A read-only
19
+ * memory cannot do that. Nothing here screens what is written — this separates
20
+ * the privilege, and makes no claim to detect anything.
21
+ *
22
+ * **One entry is one Markdown file.** Title, tags, and timestamp are YAML
23
+ * frontmatter above the body, in the same format a skill uses, and the whole
24
+ * thing is a single `BlobStore` object — locally a file under the profile's
25
+ * storage directory, in a deployment an S3 object. There is no index row.
26
+ *
27
+ * That is a deliberate reversal of the original split (ADR-014). An index in
28
+ * `ScopedStore` beside a body in `BlobStore` made listing cheap, and cost more
29
+ * than it bought: the two could disagree, the entry could not be opened in an
30
+ * editor, and a hand-written file was not an entry at all. `lanes link memory` and a
31
+ * text editor now reach the same bytes, which is the point of the format.
32
+ *
33
+ * The store is namespaced `<provider>/<connection>` by core before this
34
+ * provider sees it, so one connection's memory is not addressable from another.
35
+ */
36
+
37
+ const DEFAULT_LIMIT = 10;
38
+ /**
39
+ * No key prefix.
40
+ *
41
+ * There was an `entry/` one, which put a third segment under a path that was
42
+ * already `<provider>/<connection>` — an entry landed at
43
+ * `memory/memory/entry/<id>.md`. The namespace is the isolation boundary and it
44
+ * is already applied by the time a key reaches here, so the prefix only ever
45
+ * separated entries from other things this provider does not store.
46
+ */
47
+ const ENTRY_PREFIX = '';
48
+
49
+ /** An entry's frontmatter, parsed. Every field has a fallback — see `readEntry`. */
50
+ interface Entry {
51
+ readonly id: string;
52
+ readonly title: string;
53
+ readonly tags: readonly string[];
54
+ readonly updatedAt: string;
55
+ readonly body: string;
56
+ readonly bytes: number;
57
+ }
58
+
59
+ const ENTRY_ID = /^[a-z0-9][a-z0-9_-]*$/;
60
+
61
+ function entryKey(id: string): string {
62
+ return `${ENTRY_PREFIX}${id}.md`;
63
+ }
64
+
65
+ function idFromKey(key: string): string | null {
66
+ if (!key.startsWith(ENTRY_PREFIX) || !key.endsWith('.md')) return null;
67
+ const id = key.slice(ENTRY_PREFIX.length, -'.md'.length);
68
+ return ENTRY_ID.test(id) ? id : null;
69
+ }
70
+
71
+ /**
72
+ * Parse one stored entry.
73
+ *
74
+ * Frontmatter is optional, and its absence is not an error: these files are in
75
+ * a directory the owner may edit, and a plain Markdown file dropped in there
76
+ * should read as an entry titled after its id rather than break the listing
77
+ * that would have shown it. `lanes link memory write` puts the frontmatter back.
78
+ */
79
+ function parseEntry(id: string, text: string, fallbackUpdatedAt: string): Entry {
80
+ const { frontmatter, body } = splitOptionalFrontmatter(text);
81
+ const title = frontmatter['title'];
82
+ const updatedAt = frontmatter['updated_at'];
83
+
84
+ return {
85
+ id,
86
+ title: typeof title === 'string' && title.trim().length > 0 ? title : id,
87
+ tags: stringList(frontmatter['tags']),
88
+ updatedAt: typeof updatedAt === 'string' ? updatedAt : fallbackUpdatedAt,
89
+ body: body.trimEnd(),
90
+ bytes: new TextEncoder().encode(text).byteLength,
91
+ };
92
+ }
93
+
94
+ function serialiseEntry(entry: {
95
+ title: string;
96
+ tags: readonly string[];
97
+ updatedAt: string;
98
+ body: string;
99
+ }): string {
100
+ return withFrontmatter(
101
+ {
102
+ title: entry.title,
103
+ ...(entry.tags.length > 0 ? { tags: [...entry.tags] } : {}),
104
+ updated_at: entry.updatedAt,
105
+ },
106
+ `${entry.body.trimEnd()}\n`,
107
+ );
108
+ }
109
+
110
+ async function readEntry(storage: BlobStore, id: string): Promise<Entry | null> {
111
+ const bytes = await storage.get(entryKey(id));
112
+ if (bytes === null) return null;
113
+
114
+ return parseEntry(id, new TextDecoder().decode(bytes), new Date(0).toISOString());
115
+ }
116
+
117
+ /**
118
+ * How many entries are read at once.
119
+ *
120
+ * A bound rather than `Promise.all` over the whole listing: against a bucket
121
+ * each read is an HTTPS request, and firing four hundred at once trades a slow
122
+ * search for a rate-limited one. Against a local directory the cap costs
123
+ * nothing measurable.
124
+ */
125
+ const READ_CONCURRENCY = 16;
126
+
127
+ /**
128
+ * Every entry, newest first.
129
+ *
130
+ * **This reads every entry, and is honest about being one pass over all of
131
+ * them.** That is the cost of one file per entry: the metadata a listing needs
132
+ * is inside the document, so there is nothing cheaper to consult. At owner
133
+ * scale — hundreds of entries, one process, a local directory — it is fine, and
134
+ * the previous index row bought its speed by being a second copy that could
135
+ * disagree with the file it described.
136
+ *
137
+ * The reads are concurrent, which is not a micro-optimisation: one pass over
138
+ * four hundred entries is four hundred serial round trips once the store is a
139
+ * bucket rather than a directory, and serial round trips are the whole of the
140
+ * difference between a search that feels instant and one that times out.
141
+ *
142
+ * If this ever has to serve tens of thousands of entries, the fix is a derived
143
+ * cache that can be rebuilt from the files, never a second source of truth.
144
+ */
145
+ async function allEntries(storage: BlobStore): Promise<Entry[]> {
146
+ const blobs = (await storage.list(ENTRY_PREFIX)).flatMap((blob) => {
147
+ const id = idFromKey(blob.key);
148
+ return id === null ? [] : [{ blob, id }];
149
+ });
150
+
151
+ const entries: Entry[] = [];
152
+
153
+ for (let start = 0; start < blobs.length; start += READ_CONCURRENCY) {
154
+ const batch = await Promise.all(
155
+ blobs.slice(start, start + READ_CONCURRENCY).map(async ({ blob, id }) => {
156
+ const bytes = await storage.get(blob.key);
157
+ return bytes === null
158
+ ? null
159
+ : parseEntry(id, new TextDecoder().decode(bytes), blob.modifiedAt.toISOString());
160
+ }),
161
+ );
162
+ for (const entry of batch) if (entry) entries.push(entry);
163
+ }
164
+
165
+ return entries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
166
+ }
167
+
168
+ /** A stable id from a title, so writing does not demand one be invented. */
169
+ function slugify(title: string): string {
170
+ const slug = title
171
+ .toLowerCase()
172
+ .replace(/[^a-z0-9]+/g, '-')
173
+ .replace(/^-+|-+$/g, '')
174
+ .slice(0, 60);
175
+
176
+ return slug.length > 0 ? slug : `entry-${title.length}`;
177
+ }
178
+
179
+ export const memoryProvider: ProviderDefinition = defineLocalProvider({
180
+ id: 'memory',
181
+ name: 'Memory',
182
+ version: '1.0.0',
183
+ description:
184
+ "The owner's accumulated knowledge. Entries are addressed by id as resources and found by content with memory.search. Writing is a separate capability from reading.",
185
+
186
+ configSchema: z.object({}),
187
+ connectionSchema: z.object({}),
188
+
189
+ bundles: [
190
+ {
191
+ name: 'read',
192
+ description: 'Retrieve and search entries.',
193
+ oauth_scopes: [],
194
+ capabilities: ['entry', 'search', 'get'],
195
+ default: true,
196
+ },
197
+ {
198
+ // Not in the default bundle, and worth denying explicitly for any agent
199
+ // that does not need it — see the provider docstring.
200
+ name: 'write',
201
+ description: 'Create, replace, and delete entries.',
202
+ oauth_scopes: [],
203
+ capabilities: ['write', 'forget'],
204
+ },
205
+ ],
206
+
207
+ capabilities: [
208
+ /**
209
+ * Retrieval by address — a resource, not a tool (ADR-006).
210
+ *
211
+ * A memory entry is read-oriented context with a stable identifier, which is
212
+ * the definition of the primitive. It is the case ADR-006 said the
213
+ * distinction would matter for.
214
+ */
215
+ {
216
+ kind: 'resource',
217
+ name: 'entry',
218
+ title: 'Memory entry',
219
+ description: 'One stored memory entry, addressed by its id.',
220
+ uriTemplate: 'memory://entry/{id}',
221
+ mimeType: 'text/markdown',
222
+ // The address is worth recording and the content is not — the same trade
223
+ // `gmail.get_message` makes with a message id.
224
+ redact: keepKeys('uri'),
225
+
226
+ async list(context) {
227
+ return (await allEntries(context.storage)).map((entry) => ({
228
+ uri: `memory://entry/${encodeURIComponent(entry.id)}`,
229
+ name: entry.title,
230
+ }));
231
+ },
232
+
233
+ async read(uri, params, context) {
234
+ const raw = params['id'];
235
+ if (!raw) throw new Error(`Malformed memory URI: ${uri}`);
236
+
237
+ const id = decodeURIComponent(raw);
238
+ const entry = await readEntry(context.storage, id);
239
+ if (entry === null) throw new Error(`No memory entry "${id}" on ${context.connection.key}`);
240
+
241
+ return { uri, mimeType: 'text/markdown', text: entry.body };
242
+ },
243
+ },
244
+
245
+ {
246
+ kind: 'tool',
247
+ name: 'get',
248
+ title: 'Read a memory entry',
249
+ description:
250
+ 'Return one entry by id. The resource memory://entry/{id} is the same content; this exists for clients that do not read resources.',
251
+ inputSchema: z.object({
252
+ id: z.string().min(1).describe('Entry id'),
253
+ }),
254
+ redact: keepKeys('id'),
255
+ async handler({ id }, context) {
256
+ const entry = await readEntry(context.storage, id);
257
+
258
+ if (entry === null) {
259
+ return {
260
+ content: [{ type: 'text', text: `No memory entry "${id}" on ${context.connection.key}.` }],
261
+ isError: true,
262
+ };
263
+ }
264
+
265
+ return { content: [{ type: 'text', text: entry.body }] };
266
+ },
267
+ },
268
+
269
+ {
270
+ kind: 'tool',
271
+ name: 'search',
272
+ title: 'Search memory',
273
+ description:
274
+ 'Find entries whose title, tags, or body contain the query. Case-insensitive substring matching, not ranked relevance.',
275
+ inputSchema: z.object({
276
+ query: z.string().min(1).describe('Text to look for'),
277
+ tag: z.string().optional().describe('Restrict to entries carrying this tag'),
278
+ limit: z.number().int().min(1).max(50).optional().describe(`Maximum results (default ${DEFAULT_LIMIT})`),
279
+ }),
280
+ // Nothing kept: a memory query is as revealing as a Gmail search query,
281
+ // and frequently more so — it is the owner's own material being asked for.
282
+ async handler({ query, tag, limit }, context) {
283
+ const needle = query.toLowerCase();
284
+ const entries = await allEntries(context.storage);
285
+ const candidates = tag ? entries.filter((entry) => entry.tags.includes(tag)) : entries;
286
+
287
+ const matches: Array<{ entry: Entry; snippet: string }> = [];
288
+
289
+ for (const entry of candidates) {
290
+ if (matches.length >= (limit ?? DEFAULT_LIMIT)) break;
291
+
292
+ const inHeader =
293
+ entry.title.toLowerCase().includes(needle) ||
294
+ entry.tags.some((value) => value.toLowerCase().includes(needle));
295
+
296
+ const at = entry.body.toLowerCase().indexOf(needle);
297
+ if (!inHeader && at === -1) continue;
298
+
299
+ matches.push({ entry, snippet: at === -1 ? entry.title : excerpt(entry.body, at) });
300
+ }
301
+
302
+ context.audit.annotate({ scanned: candidates.length, matched: matches.length });
303
+
304
+ if (matches.length === 0) {
305
+ return { content: [{ type: 'text', text: `No memory entry matches on ${context.connection.key}.` }] };
306
+ }
307
+
308
+ // A `resource_link` rather than a URI spelled into the text: core routes
309
+ // the link to the profile and connection this call was made on, and a
310
+ // provider must not learn either. Written as text it would name an
311
+ // address no client could read.
312
+ return {
313
+ content: matches.flatMap(({ entry, snippet }) => [
314
+ {
315
+ type: 'resource_link' as const,
316
+ uri: `memory://entry/${encodeURIComponent(entry.id)}`,
317
+ name: entry.title,
318
+ },
319
+ {
320
+ type: 'text' as const,
321
+ text: `${entry.title}${entry.tags.length > 0 ? ` [${entry.tags.join(', ')}]` : ''}\n${snippet}`,
322
+ },
323
+ ]),
324
+ };
325
+ },
326
+ },
327
+
328
+ {
329
+ kind: 'tool',
330
+ name: 'write',
331
+ title: 'Store a memory entry',
332
+ description:
333
+ 'Create or replace an entry. Deliberately a separate capability from reading: an instruction written here is served to every later session, so an agent that only needs to recall should not be granted this.',
334
+ inputSchema: z.object({
335
+ title: z.string().min(1).describe('Short human-readable title'),
336
+ text: z.string().min(1).describe('The entry body, as Markdown'),
337
+ id: z.string().optional().describe('Entry id. Derived from the title when omitted; naming an existing one replaces it.'),
338
+ tags: z.array(z.string()).optional().describe('Labels for filtering searches'),
339
+ }),
340
+ // The title and the id are addresses; the text is the content itself.
341
+ redact: keepKeys('id', 'title', 'tags'),
342
+ async handler({ title, text, id, tags }, context) {
343
+ const entryId = id ?? slugify(title);
344
+ assertEntryId(entryId);
345
+
346
+ const document = serialiseEntry({
347
+ title,
348
+ tags: tags ?? [],
349
+ updatedAt: new Date().toISOString(),
350
+ body: text,
351
+ });
352
+ const bytes = new TextEncoder().encode(document);
353
+ await context.storage.put(entryKey(entryId), bytes, { contentType: 'text/markdown' });
354
+
355
+ context.audit.annotate({ entry: entryId, bytes: bytes.byteLength });
356
+
357
+ return {
358
+ content: [
359
+ { type: 'text', text: `Stored memory entry "${entryId}" on ${context.connection.key}.` },
360
+ { type: 'resource_link', uri: `memory://entry/${entryId}`, name: title },
361
+ ],
362
+ };
363
+ },
364
+ },
365
+
366
+ {
367
+ kind: 'tool',
368
+ name: 'forget',
369
+ title: 'Delete a memory entry',
370
+ description: 'Remove an entry and its body.',
371
+ inputSchema: z.object({
372
+ id: z.string().min(1).describe('Entry id'),
373
+ }),
374
+ redact: keepKeys('id'),
375
+ async handler({ id }, context) {
376
+ const existed = await context.storage.has(entryKey(id));
377
+ await context.storage.delete(entryKey(id));
378
+
379
+ return {
380
+ content: [
381
+ {
382
+ type: 'text',
383
+ text: existed
384
+ ? `Forgot memory entry "${id}" on ${context.connection.key}.`
385
+ : `No memory entry "${id}" on ${context.connection.key}.`,
386
+ },
387
+ ],
388
+ ...(existed ? {} : { isError: true }),
389
+ };
390
+ },
391
+ },
392
+ ],
393
+ });
394
+
395
+ export function assertEntryId(id: string): void {
396
+ if (!ENTRY_ID.test(id)) {
397
+ throw new Error(
398
+ `Memory entry id ${JSON.stringify(id)} must be lowercase letters, digits, "_" or "-".`,
399
+ );
400
+ }
401
+ }
402
+
403
+ /** A window around a match, so a result says why it matched. */
404
+ function excerpt(body: string, at: number, width = 160): string {
405
+ const start = Math.max(0, at - width / 4);
406
+ const text = body.slice(start, start + width).replace(/\s+/g, ' ').trim();
407
+ return `${start > 0 ? '…' : ''}${text}${start + width < body.length ? '…' : ''}`;
408
+ }
409
+
410
+ export default memoryProvider;
411
+
412
+ /**
413
+ * The pieces `lanes link memory` needs to reach the same bytes this provider does.
414
+ *
415
+ * Exported rather than reimplemented in the CLI: two spellings of one storage
416
+ * layout is exactly how a control plane and its data plane drift apart.
417
+ */
418
+ export const memoryStorage = {
419
+ key: entryKey,
420
+ idFromKey,
421
+ parse: parseEntry,
422
+ serialise: serialiseEntry,
423
+ read: readEntry,
424
+ all: allEntries,
425
+ prefix: ENTRY_PREFIX,
426
+ slugify,
427
+ };
428
+
429
+ export type MemoryEntry = Entry;
@@ -0,0 +1,19 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /**
4
+ * Notion supports Dynamic Client Registration, so there is genuinely nothing
5
+ * for an operator to do: we register ourselves with their authorization server
6
+ * at connect time. Browser, approve, done.
7
+ */
8
+ export const notion = defineProvider({
9
+ id: 'notion',
10
+ name: 'Notion',
11
+ description: 'Pages, databases, comments, and workspace search, via Notion\'s official MCP server.',
12
+ connector: { kind: 'mcp', endpoint: 'https://mcp.notion.com/mcp' },
13
+ auth: { kind: 'oauth', registration: 'dynamic' },
14
+ // No identity block on purpose. Notion exposes no "who am I" tool:
15
+ // `get-users` leads with integration bots rather than the person, and
16
+ // `get-teams` returns teamspaces, which are not the workspace and may be in
17
+ // the trash. Both would produce a confident wrong label, so `connect` asks
18
+ // instead — the fallback exists for exactly this.
19
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The owner layer — memory, skills, vault.
3
+ *
4
+ * Three providers that hold no third-party account: no OAuth, no vendor API, no
5
+ * rate limit anyone else imposes. They are ordinary `defineLocalProvider`
6
+ * registrations, configured by the same `lanes link connect <provider>`, scoped by the
7
+ * same profiles, and gated by the same policy evaluation as everything else —
8
+ * which was the claim `docs/detailed/init.md` made and called the real test of the
9
+ * architecture.
10
+ *
11
+ * They live in `./memory/`, `./skills/` and `./vault/`, beside `google/` and
12
+ * `icloud/`, because that claim is only true if they are providers in the
13
+ * layout as well as in the prose. This file is the one thing they share: a
14
+ * barrel, because each is *constructed* with a store rather than declared as
15
+ * data, so `#profile`'s registry builder needs all three together.
16
+ *
17
+ * `setup` joined them later and is the same shape: it holds no account either,
18
+ * and it describes the other three rather than reaching anything. It is
19
+ * read-only by construction — see ADR-019 for why describing setup is not one
20
+ * of ADR-007's control-plane exclusions.
21
+ *
22
+ * The ids `memory`, `skills`, `vault`, and `setup` are reserved (`RESERVED_PROVIDER_IDS`)
23
+ * and still refused by default — the registry has to be built with
24
+ * `allowReserved` to hold them, so a third-party provider cannot claim a
25
+ * namespace whose policy rules would then mean something else.
26
+ */
27
+
28
+ export { memoryProvider, memoryStorage, assertEntryId, type MemoryEntry } from './memory/provider.ts';
29
+ export { createSkillsProvider, type SkillsProviderOptions } from './skills/provider.ts';
30
+ export { createVaultProvider, type VaultProviderOptions } from './vault/provider.ts';
31
+ export { createSetupProvider, type SetupProviderOptions } from './setup/provider.ts';
32
+ export { planAll, planFor, type PlanContext, type ProviderPlan } from './setup/plan.ts';
33
+ // The vault's *store* is not here: it is `#secrets`, beside the system
34
+ // credential store it must never become. What lives in `./vault/` is the
35
+ // provider — the capabilities, and the rule that each item is its own.
36
+ export {
37
+ assertItemId,
38
+ createBlobVaultStore,
39
+ createFileVaultStore,
40
+ createMemoryVaultStore,
41
+ createSecretVaultStore,
42
+ generateVaultKey,
43
+ VAULT_ITEM_ID,
44
+ type BlobVaultStoreOptions,
45
+ type SecretVaultStoreOptions,
46
+ type FileVaultStoreOptions,
47
+ type VaultItem,
48
+ type VaultStore,
49
+ } from '#secrets';
@@ -0,0 +1,26 @@
1
+ import { GOOGLE_SCOPE_MEANINGS } from './google/shared/scopes.ts';
2
+ import { LINEAR_SCOPE_MEANINGS } from './linear/scopes.ts';
3
+
4
+ /**
5
+ * What a scope actually permits, contributed by the provider that requests it.
6
+ *
7
+ * Least privilege is the goal, and a vendor's *advertised* scope list is not the
8
+ * same as its required one. Where a grant does have to be broad it must not
9
+ * happen quietly: an over-broad grant is invisible after the fact, and a consent
10
+ * screen listing five Google-worded scopes is not where someone discovers that
11
+ * "Gmail" meant permanent delete.
12
+ *
13
+ * The table is here rather than in the CLI so that a provider owns its own
14
+ * vocabulary. The CLI renders it and never learns a vendor's words.
15
+ */
16
+ export interface ScopeMeaning {
17
+ /** Plain-English meaning. */
18
+ readonly meaning: string;
19
+ /** Unrestricted access to the service, or close enough to warrant a stop. */
20
+ readonly broad?: boolean;
21
+ }
22
+
23
+ export const SCOPE_MEANINGS: Record<string, ScopeMeaning> = {
24
+ ...GOOGLE_SCOPE_MEANINGS,
25
+ ...LINEAR_SCOPE_MEANINGS,
26
+ };
@@ -0,0 +1,141 @@
1
+ import type { ProviderManifest } from '#connectivity';
2
+ import { setupRequirements, type SetupRequirement } from '#connectivity';
3
+
4
+ /**
5
+ * What connecting a provider involves, assembled from its manifest.
6
+ *
7
+ * One implementation, two consumers: `lanes link setup plan` renders it for a
8
+ * terminal, and the `setup.provider` capability renders it for a model. Both
9
+ * must emit the *same* command, because the model's job is to hand a person
10
+ * something to paste — a tool that suggested a command the CLI would reject is
11
+ * worse than one that suggested nothing.
12
+ *
13
+ * Nothing here reads the credential store. What a provider *requires* is a
14
+ * property of shipped code; whether it is *satisfied* needs a store, and only
15
+ * the CLI asks that — ADR-007.
16
+ */
17
+
18
+ export interface ProviderPlan {
19
+ readonly id: string;
20
+ readonly name: string;
21
+ readonly description: string;
22
+ /** Connection keys of this provider already configured, e.g. `gmail.main`. */
23
+ readonly connected: readonly string[];
24
+ /**
25
+ * Whether this provider holds accounts at all, and so can hold another.
26
+ *
27
+ * `auth.kind === 'none'` means there is no credential to key on a connection
28
+ * id, so a second row would address the same thing the first does and "connect
29
+ * another" is meaningless. Everything else derives `<provider>/<id>` (or the
30
+ * `app` it shares), so a second account is a second `connect`.
31
+ *
32
+ * Read that as the credential test it is, not as "the owner layer". The owner
33
+ * layer never reaches here — `catalogue` is `PROVIDER_MANIFESTS`, the shipped
34
+ * third-party providers, and `memory`, `skills`, `vault` and `setup` are
35
+ * registered separately and are not in it. The one manifest this actually
36
+ * excludes is `icloud_drive`, which is `auth: none` because Apple exposes a
37
+ * synced folder rather than a protocol: its root is a manifest field, so a
38
+ * second connection would read the same files off the same Mac. Correct, and
39
+ * for a reason worth stating, because the next `auth: none` provider added
40
+ * with a per-connection identity would be excluded here silently and wrongly.
41
+ *
42
+ * This exists because the overview used to drop a provider entirely once it
43
+ * had one connection, which made "connect another Gmail account" unanswerable
44
+ * from the surface built to answer it.
45
+ */
46
+ readonly multiAccount: boolean;
47
+ /**
48
+ * Finishing this needs a person at a browser.
49
+ *
50
+ * The one fact that decides whether an agent can do it. Everything else is a
51
+ * value that can be written to the store and then used.
52
+ */
53
+ readonly browser: boolean;
54
+ readonly summary?: string;
55
+ readonly docsUrl?: string;
56
+ readonly steps: readonly string[];
57
+ readonly requires: readonly SetupRequirement[];
58
+ /** True when the command below needs `--id` filling in first. */
59
+ readonly needsId: boolean;
60
+ /** The one line that connects it. */
61
+ readonly command: string;
62
+ /**
63
+ * The OAuth client is operated by somebody else, so `requires` is empty
64
+ * because there is nothing to register — not because setup is trivial.
65
+ */
66
+ readonly brokered: boolean;
67
+ /** Who operates that client, for the sentence shown before consent. */
68
+ readonly clientOperator?: string;
69
+ /** The line that opts out of it and registers one of your own instead. */
70
+ readonly ownClientCommand?: string;
71
+ }
72
+
73
+ export interface PlanContext {
74
+ readonly profile: string;
75
+ /** Every configured connection this caller may see, as `provider.id`. */
76
+ readonly connections: readonly string[];
77
+ /**
78
+ * `oauth_apps` entries this profile declares.
79
+ *
80
+ * Which clients are the operator's own, so a profile that has registered one
81
+ * is described as needing it rather than as needing nothing.
82
+ */
83
+ readonly ownClients?: readonly string[];
84
+ }
85
+
86
+ export function planFor(
87
+ manifest: ProviderManifest,
88
+ context: PlanContext,
89
+ connectionId?: string,
90
+ ): ProviderPlan {
91
+ const { requirements, needsId, brokered } = setupRequirements(
92
+ manifest,
93
+ connectionId,
94
+ context.profile,
95
+ { ...(context.ownClients ? { ownClients: context.ownClients } : {}) },
96
+ );
97
+
98
+ const connected = context.connections.filter((key) => key.startsWith(`${manifest.id}.`));
99
+
100
+ // `--profile` always, never conditionally. One endpoint serves every profile,
101
+ // and the shell this command is pasted into may default to a different one —
102
+ // which is exactly what `resolveSelection`'s "never a silent pick" rule
103
+ // exists to prevent.
104
+ const command =
105
+ `lanes link connect ${manifest.id} --profile ${context.profile}` +
106
+ (needsId ? ' --id <name>' : connectionId ? ` --id ${connectionId}` : '');
107
+
108
+ return {
109
+ id: manifest.id,
110
+ name: manifest.name,
111
+ description: manifest.description,
112
+ connected,
113
+ multiAccount: manifest.auth.kind !== 'none',
114
+ browser: manifest.auth.kind === 'oauth',
115
+ ...(manifest.setup?.summary ? { summary: manifest.setup.summary } : {}),
116
+ ...(manifest.setup?.docs_url ? { docsUrl: manifest.setup.docs_url } : {}),
117
+ steps: manifest.setup?.steps ?? [],
118
+ requires: requirements,
119
+ needsId,
120
+ command,
121
+ brokered,
122
+ ...(brokered && manifest.auth.kind === 'oauth' && manifest.auth.broker
123
+ ? {
124
+ clientOperator: manifest.auth.broker.operator,
125
+ // The steps stay in `steps` either way. A renderer decides whether to
126
+ // show a console walkthrough for a path nobody has asked for; the
127
+ // plan's job is to say the path exists and what opens it.
128
+ ownClientCommand: `${command} --own-client`,
129
+ }
130
+ : {}),
131
+ };
132
+ }
133
+
134
+ export function planAll(
135
+ manifests: readonly ProviderManifest[],
136
+ context: PlanContext,
137
+ ): ProviderPlan[] {
138
+ return [...manifests]
139
+ .sort((a, b) => a.id.localeCompare(b.id))
140
+ .map((manifest) => planFor(manifest, context));
141
+ }