@lanes-sh/link 0.7.2 → 0.9.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 (190) hide show
  1. package/README.md +20 -10
  2. package/instructions/agents/lanes-link-scout.md +2 -2
  3. package/instructions/skills/lanes-link/SKILL.md +148 -73
  4. package/package.json +2 -1
  5. package/src/audit/index.ts +8 -1
  6. package/src/auth/index.ts +58 -2
  7. package/src/auth/lanes/assertion.ts +256 -0
  8. package/src/auth/lanes/callback.ts +135 -0
  9. package/src/auth/lanes/federation.ts +50 -0
  10. package/src/auth/lanes/login.ts +294 -0
  11. package/src/auth/lanes/members.ts +103 -0
  12. package/src/auth/lanes/session.ts +97 -0
  13. package/src/auth/oauth/grant.ts +183 -0
  14. package/src/auth/oauth/result.ts +27 -0
  15. package/src/auth/oauth/server.ts +176 -203
  16. package/src/auth/oauth/store.ts +85 -4
  17. package/src/auth/remote.ts +32 -9
  18. package/src/cli/accepts.ts +109 -0
  19. package/src/cli/argv.ts +57 -3
  20. package/src/cli/audit-change.ts +140 -0
  21. package/src/cli/callback-page.ts +36 -115
  22. package/src/cli/commands/auth-dispatch.ts +48 -0
  23. package/src/cli/commands/auth.ts +229 -0
  24. package/src/cli/commands/connect/accounts.ts +4 -4
  25. package/src/cli/commands/connect/authorise.ts +4 -4
  26. package/src/cli/commands/connect/bind-credential.ts +2 -1
  27. package/src/cli/commands/connect/custom/index.ts +1 -1
  28. package/src/cli/commands/connect/custom/write.ts +2 -2
  29. package/src/cli/commands/connect/grant.ts +29 -14
  30. package/src/cli/commands/connect/index.ts +90 -88
  31. package/src/cli/commands/connect/options.ts +83 -0
  32. package/src/cli/commands/connect/registration.ts +50 -0
  33. package/src/cli/commands/connect/requirements.ts +1 -1
  34. package/src/cli/commands/connect/settle.ts +16 -6
  35. package/src/cli/commands/connect/target-note.ts +7 -2
  36. package/src/cli/commands/connect/unknown.ts +1 -1
  37. package/src/cli/commands/connect/variables.ts +3 -2
  38. package/src/cli/commands/connection-list.ts +116 -0
  39. package/src/cli/commands/connection.ts +183 -165
  40. package/src/cli/commands/grant.ts +140 -0
  41. package/src/cli/commands/identity.ts +24 -12
  42. package/src/cli/commands/knowledge/index.ts +49 -89
  43. package/src/cli/commands/knowledge/migrate.ts +79 -13
  44. package/src/cli/commands/knowledge/show.ts +97 -0
  45. package/src/cli/commands/knowledge.ts +2 -1
  46. package/src/cli/commands/mcp/harnesses.ts +30 -8
  47. package/src/cli/commands/mcp/onboarding.ts +86 -0
  48. package/src/cli/commands/mcp/register.ts +16 -2
  49. package/src/cli/commands/mcp.ts +1 -0
  50. package/src/cli/commands/members.ts +288 -0
  51. package/src/cli/commands/operate/attach.ts +3 -3
  52. package/src/cli/commands/operate/audit.ts +11 -7
  53. package/src/cli/commands/operate/auth.ts +28 -11
  54. package/src/cli/commands/operate/findings.ts +2 -1
  55. package/src/cli/commands/operate/inspect.ts +37 -19
  56. package/src/cli/commands/operate/migrate.ts +33 -13
  57. package/src/cli/commands/operate/outputs.ts +3 -3
  58. package/src/cli/commands/operate/pair-certificate.ts +141 -0
  59. package/src/cli/commands/operate/pair.ts +324 -0
  60. package/src/cli/commands/operate/policy.ts +73 -22
  61. package/src/cli/commands/operate/serve.ts +53 -5
  62. package/src/cli/commands/operate/status.ts +18 -10
  63. package/src/cli/commands/operate/tools.ts +2 -2
  64. package/src/cli/commands/operate.ts +2 -0
  65. package/src/cli/commands/owner/assets.ts +2 -2
  66. package/src/cli/commands/owner/entities.ts +2 -2
  67. package/src/cli/commands/owner/memory.ts +2 -2
  68. package/src/cli/commands/owner/shared.ts +13 -2
  69. package/src/cli/commands/owner/skills.ts +28 -8
  70. package/src/cli/commands/owner/tasks.ts +2 -2
  71. package/src/cli/commands/owner/vault.ts +3 -3
  72. package/src/cli/commands/profile/disposition.ts +236 -0
  73. package/src/cli/commands/profile/removal.ts +154 -64
  74. package/src/cli/commands/profile/remove.ts +83 -7
  75. package/src/cli/commands/profile.ts +79 -16
  76. package/src/cli/commands/relabel.ts +112 -0
  77. package/src/cli/commands/secrets.ts +39 -17
  78. package/src/cli/commands/set-workspace.ts +96 -0
  79. package/src/cli/commands/setup.ts +2 -2
  80. package/src/cli/commands/sync.ts +8 -8
  81. package/src/cli/commands/target.ts +9 -7
  82. package/src/cli/commands/update-migration.ts +54 -0
  83. package/src/cli/commands/update.ts +78 -24
  84. package/src/cli/config-edit.ts +99 -143
  85. package/src/cli/config-migrate.ts +82 -64
  86. package/src/cli/config-repair-sweep.ts +119 -0
  87. package/src/cli/config-repair.ts +131 -125
  88. package/src/cli/config-templates.ts +200 -0
  89. package/src/cli/contract3-credentials.ts +294 -0
  90. package/src/cli/contract3-data.ts +262 -0
  91. package/src/cli/contract3-layout.ts +46 -0
  92. package/src/cli/contract3-shape.ts +212 -0
  93. package/src/cli/contract3.ts +399 -0
  94. package/src/cli/contract4-credentials.ts +207 -0
  95. package/src/cli/contract4-data.ts +399 -0
  96. package/src/cli/contract4-rename.ts +73 -0
  97. package/src/cli/contract4-yaml.ts +223 -0
  98. package/src/cli/contract4.ts +342 -0
  99. package/src/cli/endpoint-url.ts +1 -1
  100. package/src/cli/identity.ts +44 -26
  101. package/src/cli/lanes.ts +25 -1
  102. package/src/cli/main.ts +94 -14
  103. package/src/cli/migrate-move.ts +166 -0
  104. package/src/cli/migrate-plan.ts +12 -6
  105. package/src/cli/output.ts +34 -1
  106. package/src/cli/publish.ts +6 -7
  107. package/src/cli/runtime/open.ts +64 -99
  108. package/src/cli/runtime/registry.ts +6 -7
  109. package/src/cli/runtime/select.ts +2 -11
  110. package/src/cli/runtime/stores.ts +58 -0
  111. package/src/cli/runtime/types.ts +106 -0
  112. package/src/cli/runtime/vault.ts +19 -4
  113. package/src/cli/runtime/workspace.ts +60 -0
  114. package/src/cli/runtime.ts +2 -1
  115. package/src/cli/selection-require.ts +44 -13
  116. package/src/cli/selection.ts +127 -145
  117. package/src/cli/usage.ts +40 -20
  118. package/src/cli/workspace-migrate.ts +152 -22
  119. package/src/connectivity/manifest/provider.ts +34 -13
  120. package/src/connectivity/manifest/requirements.ts +1 -1
  121. package/src/connectivity/transports/imap/parser.ts +70 -9
  122. package/src/deployments/adapters/filesystem.ts +18 -3
  123. package/src/deployments/bind.ts +1 -1
  124. package/src/deployments/deploy.ts +38 -29
  125. package/src/deployments/gcp/bucket.ts +58 -11
  126. package/src/deployments/gcp/provision.ts +7 -7
  127. package/src/deployments/knowledge.ts +9 -4
  128. package/src/deployments/prepare.ts +72 -24
  129. package/src/deployments/record.ts +1 -1
  130. package/src/deployments/report.ts +2 -2
  131. package/src/deployments/serving.ts +15 -74
  132. package/src/deployments/target.ts +34 -13
  133. package/src/deployments/upload.ts +60 -27
  134. package/src/dispatch/deps.ts +88 -0
  135. package/src/dispatch/dispatch.ts +21 -62
  136. package/src/policy/index.ts +47 -15
  137. package/src/profile/connections.ts +195 -0
  138. package/src/profile/deployments.ts +86 -8
  139. package/src/profile/index.ts +35 -6
  140. package/src/profile/knowledge.ts +18 -5
  141. package/src/profile/layout.ts +163 -90
  142. package/src/profile/load.ts +133 -64
  143. package/src/profile/pairing.ts +32 -0
  144. package/src/profile/primitives.ts +35 -1
  145. package/src/profile/registry.ts +6 -6
  146. package/src/profile/schema.ts +181 -21
  147. package/src/profile/targets.ts +21 -9
  148. package/src/profile/testing.ts +104 -2
  149. package/src/profile/workspace.ts +124 -33
  150. package/src/providers/assets/provider.ts +6 -6
  151. package/src/providers/custom/index.ts +1 -1
  152. package/src/providers/custom/load.ts +2 -3
  153. package/src/providers/entities/provider.ts +6 -6
  154. package/src/providers/entities/writes.ts +1 -1
  155. package/src/providers/identity/provider.ts +2 -2
  156. package/src/providers/memory/provider.ts +26 -8
  157. package/src/providers/setup/plan.ts +1 -1
  158. package/src/providers/setup/provider.ts +3 -3
  159. package/src/providers/skills/provider.ts +2 -2
  160. package/src/providers/slack/index.ts +2 -2
  161. package/src/providers/tasks/provider.ts +6 -6
  162. package/src/providers/vault/provider.ts +1 -1
  163. package/src/registry/policy-bridge.ts +33 -11
  164. package/src/registry/reconcile.ts +4 -4
  165. package/src/registry/registry.ts +1 -1
  166. package/src/server/authorization.ts +94 -0
  167. package/src/server/edge.ts +14 -1
  168. package/src/server/endpoint.ts +89 -104
  169. package/src/server/generation.ts +10 -1
  170. package/src/server/harness.ts +71 -13
  171. package/src/server/index.ts +31 -0
  172. package/src/server/mcp/build.ts +20 -1
  173. package/src/server/mcp/client-info.ts +54 -0
  174. package/src/server/mcp/guide.ts +120 -0
  175. package/src/server/mcp/instructions.ts +22 -22
  176. package/src/server/mcp/prompts.ts +7 -3
  177. package/src/server/mcp/resources.ts +16 -8
  178. package/src/server/mcp/routing.ts +3 -3
  179. package/src/server/mcp/tools.ts +25 -6
  180. package/src/server/mcp/visibility.ts +74 -7
  181. package/src/server/oauth.ts +29 -109
  182. package/src/server/read/credential.ts +134 -0
  183. package/src/server/read/deployed.ts +56 -0
  184. package/src/server/read/listener.ts +54 -0
  185. package/src/server/read/open.ts +101 -0
  186. package/src/server/read/routes.ts +247 -0
  187. package/src/server/read/state.ts +171 -0
  188. package/src/stores/blobs/conformance.ts +19 -0
  189. package/src/stores/state/index.ts +76 -10
  190. package/src/stores/state/testing.ts +5 -1
@@ -4,6 +4,7 @@ import { capabilityIdForToolName } from '#server/mcp';
4
4
  import { ATTACHMENTS_PATH, handleAttachments } from './attachments.ts';
5
5
  import { allowedHostnamesFor, rebindingRefusal } from './rebinding.ts';
6
6
  import { ANY_ORIGIN, corsAware, type CorsPolicy } from './cors.ts';
7
+ import { isReadPath, readRoutes, type ReadDeps } from './read/routes.ts';
7
8
  import type { Generation } from './generation.ts';
8
9
  import type { Generations } from './generations.ts';
9
10
  import {
@@ -67,6 +68,16 @@ export interface ServerOptions {
67
68
  * — a page the owner happens to be visiting — before this would be reached.
68
69
  */
69
70
  readonly meterUnauthenticated?: boolean | undefined;
71
+ /**
72
+ * The dashboard's read surface, when this bind may serve it (ADR-064).
73
+ *
74
+ * Another property of the bind address, decided in the same lines of
75
+ * `serve()` as `cors` and the meter. Absent on loopback, where the TLS
76
+ * listener in `./read/open.ts` serves it on its own port instead — a
77
+ * cross-origin grant on `127.0.0.1` is what `./rebinding.ts` refuses
78
+ * outright, and ADR-039's rule is not being relaxed to fit this in.
79
+ */
80
+ readonly read?: ReadDeps | undefined;
70
81
  }
71
82
 
72
83
  export const MCP_PATH = '/mcp';
@@ -155,6 +166,8 @@ export function createRequestHandler(options: ServerOptions): RequestHandler {
155
166
  healthPath: HEALTH_PATH,
156
167
  isAuthorizationPath,
157
168
  authorizationEnabled: options.authorization !== undefined,
169
+ isReadPath,
170
+ readEnabled: options.read !== undefined,
158
171
  });
159
172
  if (refusal) {
160
173
  options.log.warn('rejected request', { reason: 'unauthenticated_rate' });
@@ -188,6 +201,17 @@ export function createRequestHandler(options: ServerOptions): RequestHandler {
188
201
  });
189
202
  }
190
203
 
204
+ // Above the 404 gate because these are deliberately not in the three-path
205
+ // set, and never through `options.authenticator`: the pairing token is a
206
+ // different credential for a different surface, and one shared check
207
+ // would make each able to do the other's job (ADR-063). Below the meter,
208
+ // because verifying one costs a credential-store read. Only what
209
+ // `isReadPath` matched is handed over — `readRoutes` answers everything
210
+ // it is given, so a wider hand-off would swallow `/mcp`.
211
+ if (options.read && isReadPath(url.pathname)) {
212
+ return await readRoutes(request, options.read);
213
+ }
214
+
191
215
  if (
192
216
  url.pathname !== MCP_PATH &&
193
217
  url.pathname !== ATTACHMENTS_PATH &&
@@ -344,8 +368,15 @@ export function serve(options: ServeOptions): RunningServer {
344
368
  const cors: CorsPolicy | undefined = loopback
345
369
  ? undefined
346
370
  : { allowedOrigins: primary.config.auth.allowed_origins ?? [ANY_ORIGIN] };
371
+ // The fourth property of this bind address, decided with the other three.
372
+ // Never on loopback: `./read/open.ts` serves it there over TLS on its own
373
+ // port, and a deployment-only grant that leaked onto `127.0.0.1` is exactly
374
+ // what ADR-039 refuses. Discarded rather than overridable, as `cors` is.
375
+ const read = loopback ? undefined : options.read;
376
+
347
377
  const handler = createRequestHandler({
348
378
  ...options,
379
+ ...(read ? { read } : { read: undefined }),
349
380
  // Off on loopback for the same reason `cors` is undefined there, and decided
350
381
  // here so every property of the bind address is decided together. An
351
382
  // explicit `true` wins, which is how a test drives the deployed behaviour.
@@ -1,6 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { isPrompt, isResource, isTool } from '#connectivity';
3
3
  import { SERVER_ICONS } from './icon.ts';
4
+ import { GUIDE_TITLE, GUIDE_URI, guideDocument } from './guide.ts';
4
5
  import { serverInstructions } from './instructions.ts';
5
6
  import { SERVER_NAME } from './naming.ts';
6
7
  import { registerPrompt } from './prompts.ts';
@@ -77,12 +78,30 @@ export function buildMcpServer(options: BuildServerOptions): McpServer {
77
78
  // server — to be told nothing is there.
78
79
  capabilities: {
79
80
  tools: { listChanged: false },
80
- ...(offers(merged, isResource) ? { resources: { listChanged: false } } : {}),
81
+ // Unconditional now: `lanes://instructions` is registered below
82
+ // whatever policy said, so this endpoint always has at least one
83
+ // resource and gating the capability on the merged set would advertise
84
+ // nothing while serving something.
85
+ resources: { listChanged: false },
81
86
  ...(offers(merged, isPrompt) ? { prompts: { listChanged: false } } : {}),
82
87
  },
83
88
  },
84
89
  );
85
90
 
91
+ // Always, and ahead of everything policy decided. This describes the surface
92
+ // rather than exposing any of it, so there is nothing here to grant — and a
93
+ // client whose owner has connected nothing at all still gets an account of
94
+ // what the thing is. It also means `resources` is advertised unconditionally,
95
+ // which `offers` below no longer decides on its own.
96
+ server.registerResource(
97
+ 'instructions',
98
+ GUIDE_URI,
99
+ { title: GUIDE_TITLE, description: 'What this endpoint is and how to behave against it', mimeType: 'text/markdown' },
100
+ async (uri: URL) => ({
101
+ contents: [{ uri: uri.href, mimeType: 'text/markdown', text: guideDocument() }],
102
+ }),
103
+ );
104
+
86
105
  for (const [id, entry] of merged) {
87
106
  // Discovered first: an upstream MCP server or an OpenAPI document supplies
88
107
  // the schema, and there is no local capability object to inspect.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Which agent is calling, read off the request rather than off a header.
3
+ *
4
+ * The audit log's `clientLabel` field has always said it holds "the MCP
5
+ * `clientInfo` name". It did not. Over HTTP it read an `x-mcp-client` header,
6
+ * which is not part of MCP and which no client sends; over a pipe nothing set
7
+ * it at all. So the one field that exists to say *who made this call* was empty
8
+ * on every event this endpoint has ever written.
9
+ *
10
+ * The protocol does carry it. A client announces itself at `initialize` and the
11
+ * SDK repeats that announcement in every later request, in `_meta` under
12
+ * `io.modelcontextprotocol/clientInfo`, which the server surfaces on the
13
+ * request envelope. Reading it there rather than from the handshake is what
14
+ * makes it work at all here: streamable HTTP is stateless on this endpoint, a
15
+ * fresh `McpServer` is built and discarded per request (`build.ts`), and a
16
+ * handshake captured on one instance is gone before the next arrives.
17
+ *
18
+ * **A client that announces itself only at `initialize` and never repeats it is
19
+ * still anonymous**, and that is the honest outcome rather than a gap worth
20
+ * papering over. The SDK does not backfill the envelope from the session, so
21
+ * inferring one would mean this endpoint keeping its own session table to hold
22
+ * a field it is not allowed to trust anyway.
23
+ *
24
+ * **Self-reported, and labelled as such wherever it surfaces.** A client may
25
+ * call itself anything. It is recorded so a reader can see which agent made a
26
+ * call and is never consulted to decide what that agent may do — the same rule
27
+ * the field carried when it was a header, and the reason widening it is safe.
28
+ */
29
+
30
+ /** Where the SDK puts the client's own `Implementation` on each request. */
31
+ const CLIENT_INFO_META_KEY = 'io.modelcontextprotocol/clientInfo';
32
+
33
+ /**
34
+ * The name the client gave for itself, or undefined.
35
+ *
36
+ * Deliberately tolerant. This is untrusted input on a path whose failure mode
37
+ * would otherwise be an exception inside a tool call that was going to succeed,
38
+ * and the worst honest outcome is the empty field that already exists.
39
+ */
40
+ export function clientLabelFrom(extra: unknown): string | undefined {
41
+ if (typeof extra !== 'object' || extra === null) return undefined;
42
+
43
+ const request = (extra as { mcpReq?: unknown }).mcpReq;
44
+ if (typeof request !== 'object' || request === null) return undefined;
45
+
46
+ const envelope = (request as { envelope?: unknown }).envelope;
47
+ if (typeof envelope !== 'object' || envelope === null) return undefined;
48
+
49
+ const info = (envelope as Record<string, unknown>)[CLIENT_INFO_META_KEY];
50
+ if (typeof info !== 'object' || info === null) return undefined;
51
+
52
+ const name = (info as { name?: unknown }).name;
53
+ return typeof name === 'string' && name.length > 0 ? name : undefined;
54
+ }
@@ -0,0 +1,120 @@
1
+ import { RESERVED_PROVIDER_IDS } from '#connectivity';
2
+
3
+ /**
4
+ * The long form of what this endpoint is, served as a resource.
5
+ *
6
+ * `initialize.instructions` has a budget it pays on every request forever, so it
7
+ * carries only the habits that must arrive everywhere. This is the document a
8
+ * client reads once when it wants the whole account, and it is served always,
9
+ * without a tool grant, because it describes the surface rather than exposing
10
+ * any of it.
11
+ *
12
+ * **Why a resource rather than a longer `instructions`.** A client caches
13
+ * `initialize` for the life of its registration, so guidance improved after
14
+ * somebody connected never reaches them. A resource is fetched when it is read,
15
+ * so this can be corrected without anybody reconnecting.
16
+ *
17
+ * **No vendor may be named here.** `src/architecture.test.ts` forbids it
18
+ * anywhere under `server/`, and rightly: what an owner has connected is theirs,
19
+ * and prose naming one provider would be wrong for everybody else.
20
+ */
21
+
22
+ export const GUIDE_URI = 'lanes://instructions';
23
+
24
+ export const GUIDE_TITLE = 'How this endpoint works';
25
+
26
+ /**
27
+ * The document.
28
+ *
29
+ * Fixed prose, deliberately: it describes the model rather than this workspace,
30
+ * and the per-principal facts are in `initialize.instructions`, which is
31
+ * computed. Two documents with different lifetimes, and mixing them would make
32
+ * this one wrong the moment a connection was added.
33
+ */
34
+ export function guideDocument(): string {
35
+ return `# Lanes Link
36
+
37
+ One endpoint in front of everything its owner has chosen to expose. It
38
+ authenticates, applies permissions per call, and records what happened.
39
+
40
+ ## The three words
41
+
42
+ **Connection.** One authorised account or store, named \`<provider>.<id>\`. It
43
+ belongs to the workspace, so the same account can be reached from more than one
44
+ profile without being authorised twice.
45
+
46
+ **Profile.** A selection of connections, with the capabilities allowed on each,
47
+ and the people who may consume it. This is how someone keeps work and personal
48
+ apart, and how they hand you one mailbox read-only while another is writable.
49
+
50
+ **Workspace.** Where connections and profiles live, and which credential store
51
+ opens them. You never name one; the endpoint you are talking to is already in it.
52
+
53
+ ## Routing
54
+
55
+ Every tool takes \`profile\` and \`connection\`. Both are enums, and both are
56
+ already narrowed to what you may reach, so anything offered is something you are
57
+ allowed to use.
58
+
59
+ **When it is ambiguous which profile is meant, ask.** Do not default to whichever
60
+ is listed first. A profile is a boundary somebody drew on purpose, and crossing
61
+ it is the failure this design exists to prevent.
62
+
63
+ A connection belongs to a profile. Naming one from a different profile is
64
+ refused, and the refusal lists what is available where you asked.
65
+
66
+ ## The owner's own material
67
+
68
+ ${RESERVED_PROVIDER_IDS.join(', ')} are not third-party services. They are the
69
+ owner's own stores, and each is a connection like any other, so a profile decides
70
+ which instances it can reach and what it may do with them.
71
+
72
+ **Memory and tasks are different stores.** Search memory before concluding you do
73
+ not know something about this person or their work. A thing to *do* is a task,
74
+ and it has a status; "remember to..." is a task, not a memory. Both are served
75
+ back to every later session, so write when asked rather than by habit.
76
+
77
+ **Skills are the owner's procedures**, surfaced as prompts rather than tools.
78
+ That is deliberate: a procedure is selected by the person, not chosen by the
79
+ model, and you cannot read one's body. If a task has a skill for it, say so and
80
+ let them invoke it rather than improvising your own version.
81
+
82
+ **Vault values are credentials.** Use one to do the thing that needs it. Do not
83
+ quote it back, summarise it, or write it anywhere.
84
+
85
+ **Entities are the people, companies and projects this owner deals with.** Before
86
+ using anyone's address or handle, look them up. A lookup returns every match and
87
+ never chooses: more than one means ask which is meant, not take the first.
88
+
89
+ ## What is set up is answerable
90
+
91
+ Before saying something cannot be reached, or that an account must be added, ask
92
+ the setup surface. It reports what exists and what is missing, and it gives the
93
+ exact command for the missing thing.
94
+
95
+ **Running that command is the owner's to do, and inventing one is not.** Every
96
+ change to what exists here is a command a person runs; nothing on this surface
97
+ adds a connection, edits a profile, or changes who may consume it. That is not an
98
+ omission to work around.
99
+
100
+ ## When a call is refused
101
+
102
+ A refusal is information, not an obstacle. Three kinds, and they mean different
103
+ things:
104
+
105
+ - **The profile is not available.** You are not a member of it. Nothing you can
106
+ do; its owner adds you.
107
+ - **The connection is not part of this profile.** Look at what is, in the enum.
108
+ - **The capability is denied.** The profile allows a narrower set than the tool
109
+ suggests. Say what was refused and let the owner decide whether to widen it.
110
+
111
+ Retrying a refusal unchanged produces the same refusal and one more line in the
112
+ owner's audit log.
113
+
114
+ ## Files are named, not carried
115
+
116
+ Where a tool takes attachments, give a path, an HTTPS URL, or an attachment
117
+ already on another message. Do not base64 a file into an argument: it is recorded
118
+ in the audit log, and the log is something the owner reads.
119
+ `;
120
+ }
@@ -53,7 +53,7 @@ import type { MergedCapability } from './visibility.ts';
53
53
  */
54
54
  const OPENING = `This endpoint is one place to reach what its owner has chosen to expose. It
55
55
  authenticates, applies permissions, and records what happened, so you do not
56
- have to.`;
56
+ have to. Read \`lanes://instructions\` for the whole account of how it works.`;
57
57
 
58
58
  const ROUTING = `**Routing.** Every tool takes \`profile\` and \`connection\`. A profile is how
59
59
  someone separates work from personal — when it is ambiguous which one is meant,
@@ -102,12 +102,12 @@ quote it back, summarise it, or write it anywhere.`;
102
102
  * Asked to connect a second mailbox, a client with no setup surface and no
103
103
  * skill answered that it could not and then invented the procedure — edit the
104
104
  * profile YAML, run "the auth command" — neither of which is a thing. It had
105
- * no way to know `setup_overview` answers exactly that, so the instruction has
105
+ * no way to know `lanes_setup_overview` answers exactly that, so the instruction has
106
106
  * to arrive here: this is the only channel that reaches a client which has
107
107
  * merely been pointed at the URL.
108
108
  */
109
109
  const SETUP = `**What is set up is answerable.** Before saying something cannot be reached, or
110
- that an account must be added, call \`setup_overview\` — then \`setup_provider\`
110
+ that an account must be added, call \`lanes_setup_overview\` — then \`lanes_setup_provider\`
111
111
  for the exact command. Running it is the owner's to do; inventing it is not.`;
112
112
 
113
113
  /**
@@ -117,14 +117,14 @@ for the exact command. Running it is the owner's to do; inventing it is not.`;
117
117
  * declaration would put a per-profile list into a string with a fixed ceiling —
118
118
  * so the workspace with the most identities to keep straight is exactly the one
119
119
  * whose list would be summarised away first. A pointer costs the same for one
120
- * profile as for twenty, and `identity_list` has room to say when each applies,
120
+ * profile as for twenty, and `lanes_identity_list` has room to say when each applies,
121
121
  * which is the half that actually prevents the mistake.
122
122
  *
123
123
  * Conditional like the rest: a profile that declares nothing has no `identity`
124
124
  * connection, so the capability is unreachable and this paragraph is unspent.
125
125
  */
126
126
  const IDENTITY = `**Identity is declared, not inferred.** Where a name, address or handle of the
127
- owner's is needed, call \`identity_list\`: a profile may hold several, each with a
127
+ owner's is needed, call \`lanes_identity_list\`: a profile may hold several, each with a
128
128
  note on when it applies.`;
129
129
 
130
130
  /**
@@ -136,7 +136,7 @@ note on when it applies.`;
136
136
  * message sent to the wrong Jan has left.
137
137
  *
138
138
  * It carries two rules rather than one, because dropping the refusal made the
139
- * second necessary. `entities_find` returns every match and sets no error, so a
139
+ * second necessary. `lanes_entities_find` returns every match and sets no error, so a
140
140
  * client is not stopped by anything: nothing but this sentence stands between
141
141
  * "two candidates" and an agent using the first. The count and the wording of
142
142
  * the tool result say it too, and this says it before the first call rather
@@ -166,7 +166,7 @@ which is meant, not take the first.`;
166
166
  * form and this one arrives only once the owner has declared themselves.
167
167
  */
168
168
  const IDENTITY_AND_ENTITIES = `**Who someone is, is declared rather than inferred.** For the owner's own name,
169
- address or handle, call \`identity_list\`. For anyone else — a person, a company,
169
+ address or handle, call \`lanes_identity_list\`. For anyone else — a person, a company,
170
170
  a project — call \`entities_find\`, which returns every match and never chooses:
171
171
  more than one means ask which is meant, not take the first.`;
172
172
 
@@ -201,14 +201,14 @@ not land, do not redo what already succeeded, and offer to retry.`;
201
201
 
202
202
  /** Which paragraph each owner-layer provider brings, when it is reachable alone. */
203
203
  const OWNER_HABITS: Record<string, string> = {
204
- memory: MEMORY,
205
- tasks: TASKS,
206
- assets: ASSETS,
207
- skills: SKILLS,
208
- vault: VAULT,
209
- setup: SETUP,
210
- identity: IDENTITY,
211
- entities: ENTITIES,
204
+ lanes_memory: MEMORY,
205
+ lanes_tasks: TASKS,
206
+ lanes_assets: ASSETS,
207
+ lanes_skills: SKILLS,
208
+ lanes_vault: VAULT,
209
+ lanes_setup: SETUP,
210
+ lanes_identity: IDENTITY,
211
+ lanes_entities: ENTITIES,
212
212
  };
213
213
 
214
214
  /**
@@ -228,14 +228,14 @@ const OWNER_HABITS: Record<string, string> = {
228
228
  */
229
229
  function habitsFor(reachable: readonly string[]): string[] {
230
230
  const present = new Set(reachable);
231
- const stores = present.has('memory') && present.has('tasks');
232
- const people = present.has('identity') && present.has('entities');
231
+ const stores = present.has('lanes_memory') && present.has('lanes_tasks');
232
+ const people = present.has('lanes_identity') && present.has('lanes_entities');
233
233
 
234
234
  return reachable.flatMap((id) => {
235
- if (stores && id === 'memory') return [MEMORY_AND_TASKS];
236
- if (stores && id === 'tasks') return [];
237
- if (people && id === 'identity') return [IDENTITY_AND_ENTITIES];
238
- if (people && id === 'entities') return [];
235
+ if (stores && id === 'lanes_memory') return [MEMORY_AND_TASKS];
236
+ if (stores && id === 'lanes_tasks') return [];
237
+ if (people && id === 'lanes_identity') return [IDENTITY_AND_ENTITIES];
238
+ if (people && id === 'lanes_entities') return [];
239
239
  return OWNER_HABITS[id] ? [OWNER_HABITS[id]!] : [];
240
240
  });
241
241
  }
@@ -270,7 +270,7 @@ function habitsFor(reachable: readonly string[]): string[] {
270
270
  * the wrong address has already sent the message; the mistake happens at the
271
271
  * instant of the send, before a skill would have been loaded, and the client
272
272
  * most in need of the rule is again the one holding no skills directory. The
273
- * paragraph also carries a rule nothing else can enforce: `entities_find` sets
273
+ * paragraph also carries a rule nothing else can enforce: `lanes_entities_find` sets
274
274
  * no error on an ambiguous result, so between "two candidates" and an agent
275
275
  * using the first there is only prose.
276
276
  *
@@ -1,3 +1,5 @@
1
+ import { forProfile } from '#auth';
2
+ import { clientLabelFrom } from './client-info.ts';
1
3
  import type { McpServer } from '@modelcontextprotocol/server';
2
4
  import { z } from 'zod';
3
5
  import { isPromptResult } from '#connectivity';
@@ -48,17 +50,19 @@ export function registerPrompt(
48
50
  description: describeWithConnections(capability.description, entry.reachable),
49
51
  argsSchema: z.object(shape),
50
52
  },
51
- async (args: Record<string, unknown>) => {
53
+ async (args: Record<string, unknown>, extra?: unknown) => {
52
54
  const { profile, connection, ...rest } = args;
53
55
  const scope = resolveScope(entry, profile, connection);
54
56
  if ('error' in scope) throw new Error(scope.error);
55
57
 
58
+ const label = clientLabelFrom(extra) ?? options.clientLabel;
59
+
56
60
  const outcome = await options.profiles.get(scope.profile)!.dispatcher.invoke({
57
- principal: options.principal,
61
+ principal: forProfile(options.principal, scope.profile),
58
62
  capabilityId: id,
59
63
  connectionKey: scope.connectionKey,
60
64
  arguments: rest,
61
- clientLabel: options.clientLabel,
65
+ ...(label ? { clientLabel: label } : {}),
62
66
  });
63
67
 
64
68
  // A prompt has no `isError` to carry a refusal in, so a denial is a
@@ -1,3 +1,5 @@
1
+ import { forProfile } from '#auth';
2
+ import { clientLabelFrom } from './client-info.ts';
1
3
  import { ResourceTemplate, type McpServer } from '@modelcontextprotocol/server';
2
4
  import { isResourceListResult, isResourceResult } from '#connectivity';
3
5
  import type { DispatchOutcome } from '#dispatch';
@@ -28,14 +30,20 @@ export function registerResource(
28
30
  const scope = { profile, connectionId };
29
31
  const scoped = scopeResourceUri(capability.uriTemplate, scope);
30
32
 
31
- const dispatch = (args: Record<string, unknown>): Promise<DispatchOutcome> =>
32
- runtime.dispatcher.invoke({
33
- principal: options.principal,
33
+ const dispatch = (
34
+ args: Record<string, unknown>,
35
+ extra?: unknown,
36
+ ): Promise<DispatchOutcome> => {
37
+ const label = clientLabelFrom(extra) ?? options.clientLabel;
38
+
39
+ return runtime.dispatcher.invoke({
40
+ principal: forProfile(options.principal, profile),
34
41
  capabilityId: id,
35
42
  connectionKey,
36
43
  arguments: args,
37
- clientLabel: options.clientLabel,
44
+ ...(label ? { clientLabel: label } : {}),
38
45
  });
46
+ };
39
47
 
40
48
  const metadata = {
41
49
  description: `${capability.description} (${profile}: ${connectionKey})`,
@@ -44,8 +52,8 @@ export function registerResource(
44
52
 
45
53
  // `uri` is the whole argument — the provider recovers its own template
46
54
  // variables from it and never sees the routing segments core prepended.
47
- const read = async (uri: URL) => {
48
- const outcome = await dispatch({ uri: uri.href });
55
+ const read = async (uri: URL, _variables?: unknown, extra?: unknown) => {
56
+ const outcome = await dispatch({ uri: uri.href }, extra);
49
57
  if (!outcome.ok) throw new Error(outcome.message);
50
58
  if (!isResourceResult(outcome.result)) {
51
59
  throw new Error(`${id} did not return resource contents`);
@@ -83,8 +91,8 @@ export function registerResource(
83
91
  // rather than an oversight. A provider omits `list` when its resource
84
92
  // space is unbounded.
85
93
  list: capability.list
86
- ? async () => {
87
- const outcome = await dispatch({});
94
+ ? async (extra?: unknown) => {
95
+ const outcome = await dispatch({}, extra);
88
96
  if (!outcome.ok) throw new Error(outcome.message);
89
97
  if (!isResourceListResult(outcome.result)) return { resources: [] };
90
98
 
@@ -16,8 +16,8 @@ import type { MergedCapability, ProfileRuntime } from './visibility.ts';
16
16
  * They have to go somewhere that works for *any* template, not just one that
17
17
  * happens to spell `{key}`, so they are inserted as the first two path segments
18
18
  * directly after the authority: `example://note/{key}` becomes
19
- * `example://note/personal/a/{key}`, and `memory://entry/{id}` becomes
20
- * `memory://entry/personal/owner/{id}`.
19
+ * `example://note/personal/a/{key}`, and `lanes-memory://entry/{id}` becomes
20
+ * `lanes-memory://entry/personal/owner/{id}`.
21
21
  *
22
22
  * The previous form substituted the literal token `{key}`, which meant any
23
23
  * provider naming its variable anything else — every provider except `example` —
@@ -53,7 +53,7 @@ function originOf(uri: string): string {
53
53
  /**
54
54
  * Route the `resource_link`s a tool hands back.
55
55
  *
56
- * A provider names its own resources — `memory://entry/deploy_window` — because
56
+ * A provider names its own resources — `lanes-memory://entry/deploy_window` — because
57
57
  * it does not know, and must not learn, which profile or connection it is
58
58
  * serving. Core does. Without this, `memory.search` returns addresses that look
59
59
  * like resources and cannot be read, which is worse than returning none.
@@ -1,10 +1,17 @@
1
+ import { forProfile } from '#auth';
1
2
  import { fromJsonSchema, type McpServer } from '@modelcontextprotocol/server';
2
3
  import { z } from 'zod';
3
4
  import { isToolResult } from '#connectivity';
5
+ import { clientLabelFrom } from './client-info.ts';
4
6
  import { toolNameFor } from './naming.ts';
5
7
  import { resourceLinkRouter } from './routing.ts';
6
8
  import { sanitizeSchema } from './schema.ts';
7
- import { describeWithConnections, type BuildServerOptions, type MergedCapability } from './visibility.ts';
9
+ import {
10
+ accountsByProfile,
11
+ describeWithConnections,
12
+ type BuildServerOptions,
13
+ type MergedCapability,
14
+ } from './visibility.ts';
8
15
 
9
16
  /**
10
17
  * Tools — the capability kind everything else is measured against.
@@ -49,7 +56,11 @@ export function registerDiscoveredTool(
49
56
  toolNameFor(id),
50
57
  {
51
58
  ...(discovered.title ? { title: discovered.title } : {}),
52
- description: describeWithConnections(discovered.description, entry.reachable),
59
+ description: describeWithConnections(
60
+ discovered.description,
61
+ entry.reachable,
62
+ accountsByProfile(options),
63
+ ),
53
64
  // Spread the upstream schema rather than rebuilding it from properties
54
65
  // and required alone. Vendors put `$defs` beside those and `$ref` into
55
66
  // them — Linear's attachment tools do — and a rebuild drops the
@@ -85,7 +96,11 @@ export function registerLocalTool(
85
96
  toolNameFor(id),
86
97
  {
87
98
  ...(capability.title ? { title: capability.title } : {}),
88
- description: describeWithConnections(capability.description, entry.reachable),
99
+ description: describeWithConnections(
100
+ capability.description,
101
+ entry.reachable,
102
+ accountsByProfile(options),
103
+ ),
89
104
  inputSchema: {
90
105
  ...shape,
91
106
  // Injected by core, never declared by a provider — ADR-001. Both enums
@@ -111,9 +126,13 @@ export function registerLocalTool(
111
126
  function makeHandler(capabilityId: string, entry: MergedCapability, options: BuildServerOptions) {
112
127
  // `unknown` because the JSON-Schema overload types it that way; the schema
113
128
  // has already validated the shape by the time this runs.
114
- return async (args: unknown) => {
129
+ return async (args: unknown, extra?: unknown) => {
115
130
  const { profile, connection, ...rest } = (args ?? {}) as Record<string, unknown>;
116
131
 
132
+ // The request first, then whatever the transport knew before the call —
133
+ // which on a stateless POST is nothing, and is why the request is asked.
134
+ const label = clientLabelFrom(extra) ?? options.clientLabel;
135
+
117
136
  const name = String(profile);
118
137
  const runtime = options.profiles.get(name);
119
138
  const reachable = entry.reachable.get(name);
@@ -147,11 +166,11 @@ function makeHandler(capabilityId: string, entry: MergedCapability, options: Bui
147
166
  }
148
167
 
149
168
  const outcome = await runtime.dispatcher.invoke({
150
- principal: options.principal,
169
+ principal: forProfile(options.principal, name),
151
170
  capabilityId,
152
171
  connectionKey: String(connection),
153
172
  arguments: rest,
154
- clientLabel: options.clientLabel,
173
+ ...(label ? { clientLabel: label } : {}),
155
174
  });
156
175
 
157
176
  if (!outcome.ok) {