@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,206 @@
1
+ import { listProfiles } from '#profile';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { deployedUrl, endpointUrl } from '../../endpoint-url.ts';
4
+ import { announce, heading, print, style, warn } from '../../output.ts';
5
+ import { ensureProfileToken, openRuntime, type GlobalFlags } from '../../runtime.ts';
6
+
7
+ export interface OutputsFlags extends GlobalFlags {
8
+ readonly show?: boolean | undefined;
9
+ readonly json?: boolean | undefined;
10
+ }
11
+
12
+ interface Health {
13
+ readonly profile: string;
14
+ readonly profiles: readonly string[];
15
+ }
16
+
17
+ /**
18
+ * What an agent harness needs to reach this endpoint.
19
+ *
20
+ * The unit here is the **endpoint**, not the profile. One `lanes link start`
21
+ * serves every profile in the workspace from one URL under one token, so
22
+ * printing a registration per profile — which this did while the ports were
23
+ * per profile — now emits commands that authenticate against tokens the server
24
+ * does not accept.
25
+ *
26
+ * Deliberately not a registration command that runs: nothing here writes to any
27
+ * agent's config file, because which file that is, and what belongs in it, is
28
+ * the harness's business.
29
+ */
30
+ export async function outputs(flags: OutputsFlags): Promise<void> {
31
+ const runtime = await openRuntime(flags);
32
+
33
+ try {
34
+ const { token } = await ensureProfileToken(runtime.credentials, runtime.config.auth.token_ref);
35
+ const declared = runtime.config.targets[runtime.target]?.deploy;
36
+ const deployed = await deployedUrl(declared);
37
+ const url = deployed ?? (await endpointUrl(runtime.config, runtime.target));
38
+
39
+ const live = await health(url, token);
40
+ const mine = live?.profile === runtime.resolution.profile;
41
+
42
+ // Live if it is up, otherwise what `start` would serve: the whole
43
+ // workspace, since `--only` is the exception rather than the default.
44
+ const profiles = mine
45
+ ? live.profiles
46
+ : await listProfiles(runtime.resolution.workspaceRoot);
47
+
48
+ if (flags.json) {
49
+ print(
50
+ JSON.stringify(
51
+ {
52
+ url,
53
+ running: mine,
54
+ deployed: deployed !== null,
55
+ target: runtime.target,
56
+ primary: runtime.resolution.profile,
57
+ profiles,
58
+ ...(flags.show ? { token } : {}),
59
+ },
60
+ null,
61
+ 2,
62
+ ),
63
+ );
64
+ return;
65
+ }
66
+
67
+ announce(runtime.resolution);
68
+
69
+ heading('Endpoint');
70
+ print(
71
+ ` ${url} ${mine ? style.green(deployed ? 'deployed' : 'running') : style.dim(deployed ? 'not answering' : 'not running')}`,
72
+ );
73
+ if (deployed && declared) {
74
+ // The URL is the platform's, not the configured host and port: those
75
+ // govern where `lanes link start` listens locally and mean nothing to a
76
+ // deployed revision, which listens on whatever $PORT it was given.
77
+ print(style.dim(` ${declared.platform} service "${declared.service}" for target "${runtime.target}".`));
78
+ }
79
+
80
+ if (live && !mine) {
81
+ // Two workspaces can assign the same port. Saying so beats reporting an
82
+ // endpoint as up when it belongs to something else entirely.
83
+ print(warn(`something else is serving this port: profile "${live.profile}"`));
84
+ }
85
+
86
+ heading(`Profiles reachable through it (${profiles.length})`);
87
+ for (const profile of profiles) {
88
+ print(` ${profile}${profile === runtime.resolution.profile ? style.dim(' (endpoint owner)') : ''}`);
89
+ }
90
+ print(style.dim(' Each call names one, in its `profile` argument.'));
91
+
92
+ if (flags.show) {
93
+ heading('Token');
94
+ print(` ${token}`);
95
+ }
96
+
97
+ heading('Register with your agent');
98
+ print(
99
+ style.dim(
100
+ ' Not run for you: which config file an agent reads is its business, not ours.',
101
+ ),
102
+ );
103
+ print('');
104
+
105
+ const invocation = await tokenInvocation(runtime.config.auth.token_ref, token, flags.profile);
106
+
107
+ print(
108
+ ` claude mcp add --transport http lanes-link ${url} \\\n` +
109
+ ` --header "Authorization: Bearer $(${invocation.command})"`,
110
+ );
111
+ print('');
112
+
113
+ if (!invocation.onPath) {
114
+ // The failure this prevents is nasty: an unresolvable command substitutes
115
+ // to the empty string, the header becomes "Bearer ", and the only symptom
116
+ // is a 401 that looks like a bad token rather than a missing binary.
117
+ print(warn('lanes is not on your PATH, so the short form would substitute to nothing.'));
118
+ print(style.dim(' The command above uses this checkout instead. To shorten it permanently:'));
119
+ print(` cd ${process.cwd()} && bun link`);
120
+ print('');
121
+ }
122
+
123
+ print(
124
+ style.dim(
125
+ ' One registration covers every profile above. The $(…) keeps the token out of your\n' +
126
+ ' agent\'s context and out of the transcript — but note it is resolved once, when you\n' +
127
+ ' run the command, and stored as a literal. After "lanes link token rotate" you have to\n' +
128
+ ' register again. Other harnesses take the same two facts — URL and bearer token.',
129
+ ),
130
+ );
131
+ } finally {
132
+ await runtime.close();
133
+ }
134
+ }
135
+
136
+ /**
137
+ * The command that prints this endpoint's token, verified to actually work.
138
+ *
139
+ * `$(lanes link token show --raw)` is the right shape, and useless if `lanes`
140
+ * does not resolve: the substitution yields an empty string, the header becomes
141
+ * `Bearer `, and the endpoint answers 401 — which reads as a bad token rather
142
+ * than a missing binary. So the short form is offered only after running it and
143
+ * checking it returns *this* token; otherwise the printed command names this
144
+ * checkout, which always works.
145
+ */
146
+ async function tokenInvocation(
147
+ tokenRef: string,
148
+ expected: string,
149
+ profile: string | undefined,
150
+ ): Promise<{ command: string; onPath: boolean }> {
151
+ const suffix = profile ? ` --profile ${profile}` : '';
152
+ const short = `lanes link token show --raw${suffix}`;
153
+
154
+ const resolved = Bun.which('lanes');
155
+ if (resolved) {
156
+ try {
157
+ const result = Bun.spawnSync([resolved, 'link', 'token', 'show', '--raw', ...(profile ? ['--profile', profile] : [])]);
158
+ // Compared against the token rather than merely checking it exited zero:
159
+ // a `lanes` on PATH could belong to a different workspace entirely,
160
+ // and would hand the harness a token this endpoint rejects.
161
+ if (result.success && new TextDecoder().decode(result.stdout).trim() === expected) {
162
+ return { command: short, onPath: true };
163
+ }
164
+ } catch {
165
+ // Falls through to the checkout-relative form.
166
+ }
167
+ }
168
+
169
+ // `lanes.ts`, not `main.ts`: main.ts exports `run` and no longer executes on
170
+ // import, so `bun run main.ts token show --raw` prints nothing and exits 0 —
171
+ // which is the empty-token failure this whole function exists to avoid,
172
+ // reintroduced by the fallback meant to prevent it.
173
+ const entry = fileURLToPath(new URL('../../lanes.ts', import.meta.url));
174
+ return { command: `bun run ${entry} link token show --raw${suffix}`, onPath: false };
175
+ }
176
+
177
+ /**
178
+ * Ask the endpoint who it is.
179
+ *
180
+ * The profile name is checked by the caller, not just the port: two workspaces
181
+ * can assign the same port, and reporting "running" because something
182
+ * unrelated answers would send someone to register an endpoint serving another
183
+ * workspace's accounts.
184
+ *
185
+ * The token is sent because `/health` names profiles only to a caller that
186
+ * holds one. Anonymously it answers `{status: "ok"}` and nothing else — that
187
+ * list is what this endpoint holds, and a deployed URL is readable by anyone.
188
+ * An endpoint that answers without naming itself is therefore reported as
189
+ * something else's, which is the honest reading: this token does not open it.
190
+ */
191
+ async function health(url: string, token: string): Promise<Health | null> {
192
+ try {
193
+ const probe = new URL(url);
194
+ probe.pathname = '/health';
195
+ const response = await fetch(probe, {
196
+ headers: { authorization: `Bearer ${token}` },
197
+ signal: AbortSignal.timeout(700),
198
+ });
199
+ if (!response.ok) return null;
200
+
201
+ const body = (await response.json()) as Partial<Health>;
202
+ return body.profile ? { profile: body.profile, profiles: body.profiles ?? [body.profile] } : null;
203
+ } catch {
204
+ return null;
205
+ }
206
+ }
@@ -0,0 +1,80 @@
1
+ import { loadConfigFile } from '#profile';
2
+ import { ConfigDocument } from '../../config-edit.ts';
3
+ import { announce, heading, ok, print, style, table } from '../../output.ts';
4
+ import { resolveProfile, type GlobalFlags } from '../../runtime.ts';
5
+ import { nextAfterEdit, publishProfileEdit } from '../../publish.ts';
6
+
7
+ /**
8
+ * `lanes link policy` and `lanes link config show` — reading and editing what is granted.
9
+ *
10
+ * Both are control-plane operations under ADR-007: a policy change authorises
11
+ * future agent behaviour, so it originates here and never over MCP.
12
+ */
13
+
14
+ export async function policyList(flags: GlobalFlags): Promise<void> {
15
+ const { resolution, config } = await resolveProfile(flags);
16
+ announce(resolution);
17
+
18
+ if (config.policy.allow.length === 0 && config.policy.deny.length === 0) {
19
+ print(style.dim('No rules. Default deny is in effect: nothing is reachable.'));
20
+ return;
21
+ }
22
+
23
+ const expiry = (rule: { capability: string; expires_at?: string | undefined }) =>
24
+ rule.expires_at ? style.dim(`until ${rule.expires_at}`) : '';
25
+
26
+ if (config.policy.allow.length > 0) {
27
+ heading('Allow');
28
+ table(config.policy.allow.map((rule) => [` ${style.green('+')}`, rule.capability, expiry(rule)]));
29
+ }
30
+
31
+ if (config.policy.deny.length > 0) {
32
+ heading('Deny');
33
+ print(style.dim(' A deny beats any allow, whatever the order in the file.'));
34
+ table(config.policy.deny.map((rule) => [` ${style.red('-')}`, rule.capability, expiry(rule)]));
35
+ }
36
+
37
+ print('');
38
+ print(
39
+ style.dim('Rules cover every account of a provider. For different grants, use a second profile.'),
40
+ );
41
+ }
42
+
43
+ export async function policyRule(
44
+ effect: 'allow' | 'deny',
45
+ capability: string,
46
+ flags: GlobalFlags,
47
+ ): Promise<void> {
48
+ const { resolution, config, target } = await resolveProfile(flags);
49
+ const document = await ConfigDocument.open(resolution.workspaceRoot, resolution.profile);
50
+
51
+ if (config.policy[effect].some((rule) => rule.capability === capability)) {
52
+ print(style.dim(`${effect} ${capability} is already declared.`));
53
+ return;
54
+ }
55
+
56
+ // A bare string, because that is what the file should read like. The object
57
+ // form exists for `expires_at` and is not worth writing by default.
58
+ document.addTo(['policy', effect], capability, { inline: true });
59
+ // Validation runs before the write, so a rule naming a provider with no
60
+ // connection fails here rather than silently granting nothing at runtime.
61
+ await document.save();
62
+
63
+ announce(resolution);
64
+ print(ok(`${effect} ${style.bold(capability)}`));
65
+
66
+ if (effect === 'deny' && config.policy.allow.some((rule) => rule.capability === '*')) {
67
+ print(style.dim(' This narrows the catch-all allow; a deny always wins.'));
68
+ }
69
+
70
+ // A deny the endpoint has not heard about is still granting what it names, so
71
+ // a policy edit publishes itself exactly as connecting does (ADR-029).
72
+ print(style.dim(` ${nextAfterEdit(await publishProfileEdit({ resolution, config, target }))}`));
73
+ }
74
+
75
+ export async function configShow(flags: GlobalFlags): Promise<void> {
76
+ const { resolution } = await resolveProfile(flags);
77
+ announce(resolution);
78
+ const { config } = await loadConfigFile(resolution.profilePath);
79
+ print(JSON.stringify(config, null, 2));
80
+ }
@@ -0,0 +1,55 @@
1
+ import { startEndpoint } from '#server/endpoint.ts';
2
+ import { streamLogger } from '#server/logging.ts';
3
+ import { announce, ok, print, style, warn } from '../../output.ts';
4
+ import { resolveProfile, type GlobalFlags } from '../../runtime.ts';
5
+
6
+ /** `lanes link start` — reconcile, then serve every profile on one endpoint. */
7
+
8
+ export async function start(
9
+ flags: GlobalFlags & {
10
+ port?: number | undefined;
11
+ /** `--only` serves just the resolved profile, rather than the whole workspace. */
12
+ only?: boolean | undefined;
13
+ },
14
+ ): Promise<void> {
15
+ const { resolution } = await resolveProfile(flags);
16
+ announce(resolution);
17
+
18
+ // The bootstrap itself lives in `endpoint.ts`, shared with the container
19
+ // entrypoint. What stays here is what a terminal wants: the plan, printed as
20
+ // it is applied, and the endpoint at the end.
21
+ const endpoint = await startEndpoint({
22
+ flags,
23
+ port: flags.port,
24
+ only: flags.only,
25
+ mintToken: true,
26
+ // Stderr, not stdout: `--json` and `--raw` callers parse the other stream.
27
+ // A refused credential is the event worth seeing while this runs in the
28
+ // foreground, and until now nothing printed it.
29
+ log: streamLogger((line) => process.stderr.write(`${line}\n`)),
30
+ reporter: {
31
+ reconciled({ profile, plan, ofMany }) {
32
+ if (ofMany) print(style.dim(` ${profile}`));
33
+ print(plan);
34
+ print(ok(`reconciled ${ofMany ? profile : ''}`.trim()));
35
+ },
36
+ tokenMinted({ target }) {
37
+ print(warn(`minted a token — run: lanes link outputs --show --target ${target}`));
38
+ },
39
+ },
40
+ });
41
+
42
+ print(ok(`serving ${style.bold(endpoint.url)}`));
43
+ print(style.dim(` profiles: ${endpoint.profiles.join(', ')}`));
44
+ print(style.dim('Ctrl-C to stop.'));
45
+
46
+ const shutdown = async (): Promise<void> => {
47
+ await endpoint.stop();
48
+ process.exit(0);
49
+ };
50
+
51
+ process.on('SIGINT', () => void shutdown());
52
+ process.on('SIGTERM', () => void shutdown());
53
+
54
+ await new Promise(() => {});
55
+ }
@@ -0,0 +1,133 @@
1
+ import { listProfiles } from '#profile';
2
+ import { oneProfile, visibleCapabilities } from '#server/mcp';
3
+ import { toPolicyDocument } from '#registry';
4
+ import { announce, emit, heading, print, style, table } from '../../output.ts';
5
+ import { openRuntime, ownerPrincipal, type GlobalFlags } from '../../runtime.ts';
6
+ import { deploymentIdentity } from '../../endpoint-url.ts';
7
+
8
+ /** `lanes link status` — connections, what is reachable through them, and where. */
9
+
10
+ export interface StatusFlags extends GlobalFlags {
11
+ readonly json?: boolean | undefined;
12
+ }
13
+
14
+ /**
15
+ * Deliberately no `running` field, unlike `outputs`.
16
+ *
17
+ * `status` answers from config, the database, and policy — no network call. A
18
+ * health probe would make the command that tells you what is configured depend
19
+ * on something being up, and `outputs --json` already reports liveness.
20
+ *
21
+ * Which is why a deployed target prints its *identity* rather than its address.
22
+ * This used to print `http://<host>:<port>/mcp` unconditionally, so
23
+ * `status --target cloud` named a loopback port with nothing behind it — the
24
+ * bug `endpoint-url.ts` records having already fixed in `mcp add`. Reaching for
25
+ * `endpointUrl` here would fix the lie by surrendering the property above: it
26
+ * shells out to `gcloud`, costs seconds, needs the CLI installed and
27
+ * authenticated, and swallows every failure back into loopback — so on a fresh
28
+ * machine or in CI it would print the same wrong answer, more slowly.
29
+ *
30
+ * A deployment's address is the platform's to assign and `outputs --target` is
31
+ * the command that asks. Which service it is, though, is in the config, and a
32
+ * config-only command can say that much honestly.
33
+ */
34
+ export async function status(flags: StatusFlags): Promise<void> {
35
+ const runtime = await openRuntime(flags);
36
+ try {
37
+ const records = await runtime.state.connections.list();
38
+ const byKey = new Map(records.map((record) => [`${record.provider}.${record.id}`, record]));
39
+
40
+ const connections = runtime.config.connections.map((connection) => {
41
+ const key = `${connection.provider}.${connection.id}`;
42
+ return {
43
+ key,
44
+ provider: connection.provider,
45
+ id: connection.id,
46
+ account: connection.account,
47
+ state: byKey.get(key)?.status ?? 'not reconciled',
48
+ };
49
+ });
50
+
51
+ const capabilities = visibleCapabilities({
52
+ profiles: oneProfile(runtime.resolution.profile, {
53
+ config: runtime.config,
54
+ registry: runtime.registry,
55
+ dispatcher: runtime.dispatcher,
56
+ policy: toPolicyDocument(runtime.config),
57
+ }),
58
+ principal: ownerPrincipal(runtime.config.instance.profile),
59
+ });
60
+
61
+ // What is registered but has no connection yet — the other half of "what is
62
+ // set up", and the half an agent needs to answer "what else could I use?".
63
+ const connected = new Set(connections.map((connection) => connection.provider));
64
+ const notConnected = runtime.registry
65
+ .list()
66
+ .map((entry) => entry.manifest.id)
67
+ .filter((id) => !connected.has(id))
68
+ .sort();
69
+
70
+ // Null rather than a different shape: `endpoint` stays a string-or-null so a
71
+ // consumer that reads it keeps working, and `deployment` carries what
72
+ // replaces it. Silently turning a string into an object would be a cost
73
+ // paid by every reader for the benefit of none.
74
+ const local = `http://${runtime.config.instance.host}:${runtime.config.instance.port}/mcp`;
75
+ const deployment = deploymentIdentity(runtime.config.targets[runtime.target]?.deploy);
76
+ const endpoint = deployment ? null : local;
77
+
78
+ return emit(
79
+ flags.json,
80
+ {
81
+ profile: runtime.resolution.profile,
82
+ profiles: await listProfiles(runtime.resolution.workspaceRoot),
83
+ target: runtime.target,
84
+ endpoint,
85
+ deployment,
86
+ connections,
87
+ capabilities,
88
+ notConnected,
89
+ },
90
+ () => {
91
+ announce(runtime.resolution);
92
+
93
+ heading('Connections');
94
+ if (connections.length === 0) {
95
+ print(style.dim(' none — run: lanes link connect example'));
96
+ } else {
97
+ table(
98
+ connections.map((connection) => [
99
+ ` ${style.bold(connection.key)}`,
100
+ connection.state === 'active'
101
+ ? style.green(connection.state)
102
+ : style.yellow(connection.state),
103
+ style.dim(connection.account),
104
+ ]),
105
+ );
106
+ }
107
+
108
+ heading('Reachable capabilities');
109
+ if (capabilities.length === 0) {
110
+ print(style.dim(' none — default deny is in effect and nothing is granted'));
111
+ } else {
112
+ for (const capability of capabilities) print(` ${capability}`);
113
+ }
114
+
115
+ heading('Endpoint');
116
+ if (!deployment) {
117
+ print(` ${local} ${style.dim('(when running)')}`);
118
+ } else {
119
+ const { platform, service, region } = deployment;
120
+ print(` ${platform} service ${style.bold(service)} in ${region}`);
121
+ print(
122
+ style.dim(
123
+ ' the address is the platform\'s to assign — run: ' +
124
+ `lanes link outputs --target ${runtime.target}`,
125
+ ),
126
+ );
127
+ }
128
+ },
129
+ );
130
+ } finally {
131
+ await runtime.close();
132
+ }
133
+ }
@@ -0,0 +1,70 @@
1
+ import { announce, ok, print, style, warn } from '../../output.ts';
2
+ import { ensureProfileToken, openRuntime, type GlobalFlags } from '../../runtime.ts';
3
+
4
+ /** `lanes link token` — the one bearer token this endpoint accepts. */
5
+
6
+ /** The token, or enough of it to recognise. One shape, so the two commands agree. */
7
+ function show(token: string, reveal: boolean | undefined): string {
8
+ return reveal ? token : `${token.slice(0, 8)}… ${style.dim('(--show to reveal)')}`;
9
+ }
10
+
11
+ export async function tokenShow(
12
+ flags: GlobalFlags & { show?: boolean | undefined; raw?: boolean | undefined },
13
+ ): Promise<void> {
14
+ const runtime = await openRuntime(flags);
15
+ try {
16
+ const { token, created } = await ensureProfileToken(
17
+ runtime.credentials,
18
+ runtime.config.auth.token_ref,
19
+ );
20
+
21
+ // `--raw` prints the token and nothing else, for command substitution:
22
+ //
23
+ // claude mcp add … --header "Authorization: Bearer $(lanes link token show --raw)"
24
+ //
25
+ // That is the recommended way to register an instance, and the reason is
26
+ // not convenience. A token pasted from `--show` passes through the agent's
27
+ // context and into its transcript; substituted by the shell it goes from
28
+ // this process to the harness and is never seen by the model at all.
29
+ if (flags.raw) {
30
+ process.stdout.write(`${token}\n`);
31
+ return;
32
+ }
33
+
34
+ announce(runtime.resolution);
35
+ if (created) print(warn('no token existed; a new one was minted'));
36
+ print(show(token, flags.show));
37
+ } finally {
38
+ await runtime.close();
39
+ }
40
+ }
41
+
42
+ export async function tokenRotate(
43
+ flags: GlobalFlags & { show?: boolean | undefined },
44
+ ): Promise<void> {
45
+ const runtime = await openRuntime(flags);
46
+ try {
47
+ announce(runtime.resolution);
48
+
49
+ const { generateProfileToken } = await import('#auth');
50
+ const token = generateProfileToken();
51
+ await runtime.credentials.set(runtime.config.auth.token_ref, token);
52
+
53
+ print(ok('token rotated'));
54
+ // Gated the way `tokenShow` gates it, and for the reason given above: a
55
+ // token printed here goes into the transcript of whatever ran the command.
56
+ // Rotating is what an operator does *because* a token leaked, so printing
57
+ // the replacement unasked is the one moment it costs the most.
58
+ print(` ${show(token, flags.show)}`);
59
+ print();
60
+ // Rotating invalidates every agent using this endpoint, which is the cost
61
+ // of one token per endpoint rather than one per agent. Say so plainly —
62
+ // and say how, because `claude mcp add` stores the substituted value rather
63
+ // than the command, so nothing re-reads this on its own.
64
+ print(warn('every agent configured with the old token must be re-registered'));
65
+ print(style.dim(' A harness stores the token it was given, not the command that produced it.'));
66
+ print(style.dim(' Run: lanes link outputs for the command to re-run.'));
67
+ } finally {
68
+ await runtime.close();
69
+ }
70
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Running an instance and looking at it — everything that is neither
3
+ * `connect` nor the owner layer.
4
+ *
5
+ * Seven files, one per verb group, because every private helper here served
6
+ * exactly one command and nothing crossed between them:
7
+ *
8
+ * inspect.ts check, plan, doctor — the gate order, cheapest failure first
9
+ * status.ts connections, reachable capabilities, endpoint
10
+ * outputs.ts what an agent harness needs, and proving the short form works
11
+ * serve.ts start
12
+ * audit.ts audit tail and verify, and the Markdown rendering of tail
13
+ * token.ts token show, token rotate
14
+ * policy.ts policy list/allow/deny, config show
15
+ *
16
+ * This file stays a barrel because `./operate.ts` is the spelling `main.ts` and
17
+ * `operate.test.ts` bind, and Bun will not resolve that to `operate/index.ts`.
18
+ */
19
+
20
+ export { check, doctor, plan } from './operate/inspect.ts';
21
+ export { status } from './operate/status.ts';
22
+ export { outputs, type OutputsFlags } from './operate/outputs.ts';
23
+ export { start } from './operate/serve.ts';
24
+ export { auditTail, auditVerify, markdownCell } from './operate/audit.ts';
25
+ export { attachFile } from './operate/attach.ts';
26
+ export { tokenRotate, tokenShow } from './operate/token.ts';
27
+ export { configShow, policyList, policyRule } from './operate/policy.ts';
@@ -0,0 +1,110 @@
1
+ import { ConfigError } from '#profile';
2
+ import { scopeNamespace } from '#dispatch';
3
+ import { scopeBlobStore, type BlobStore } from '#stores/blobs';
4
+ import { memoryStorage } from '#providers/owner.ts';
5
+ import { heading, ok, print, style, table } from '../../output.ts';
6
+ import type { Runtime } from '../../runtime.ts';
7
+ import {
8
+ agreed,
9
+ ownerConnection,
10
+ readStdin,
11
+ required,
12
+ withRuntime,
13
+ type OwnerFlags,
14
+ } from './shared.ts';
15
+
16
+ /** `lanes link memory` — the owner's persistent, accumulated knowledge. */
17
+
18
+ export async function memoryList(flags: OwnerFlags): Promise<void> {
19
+ await withRuntime(flags, async (runtime) => {
20
+ const store = memoryStore(runtime, flags);
21
+ const entries = (await memoryStorage.all(store)).filter(
22
+ (entry) => !flags.tag || entry.tags.includes(flags.tag),
23
+ );
24
+
25
+ heading(`Memory (${entries.length})`);
26
+ if (entries.length === 0) {
27
+ print(style.dim(' none — write one with: lanes link memory write <id> --title <title>'));
28
+ return;
29
+ }
30
+
31
+ table(
32
+ entries.map((entry) => [
33
+ ` ${entry.id}`,
34
+ entry.title,
35
+ entry.tags.length > 0 ? style.dim(entry.tags.join(', ')) : '',
36
+ style.dim(entry.updatedAt.slice(0, 10)),
37
+ ]),
38
+ );
39
+ });
40
+ }
41
+
42
+ export async function memoryGet(id: string | undefined, flags: OwnerFlags): Promise<void> {
43
+ const entryId = required(id, 'lanes link memory get <id>');
44
+
45
+ await withRuntime(flags, async (runtime) => {
46
+ const entry = await memoryStorage.read(memoryStore(runtime, flags), entryId);
47
+ if (!entry) throw new ConfigError(`No memory entry "${entryId}" in this profile.`);
48
+
49
+ print('');
50
+ print(entry.body);
51
+ });
52
+ }
53
+
54
+ export async function memoryWrite(id: string | undefined, flags: OwnerFlags): Promise<void> {
55
+ const given = required(id, 'lanes link memory write <id> --title <title> (body on stdin)');
56
+ const text = await readStdin(
57
+ `lanes link memory write ${given} --title "<title>"`,
58
+ 'the entry body',
59
+ );
60
+
61
+ await withRuntime(flags, async (runtime) => {
62
+ const store = memoryStore(runtime, flags);
63
+ const existing = await memoryStorage.read(store, given);
64
+
65
+ // The title is what a listing and a search show, so it is worth keeping
66
+ // when a rewrite does not supply one — losing it silently would leave an
67
+ // entry titled after its own id for no reason the owner asked for.
68
+ const title = flags.title ?? existing?.title ?? given;
69
+ const document = memoryStorage.serialise({
70
+ title,
71
+ tags: flags.tag ? [flags.tag] : (existing?.tags ?? []),
72
+ updatedAt: new Date().toISOString(),
73
+ body: text,
74
+ });
75
+
76
+ await store.put(memoryStorage.key(given), new TextEncoder().encode(document), {
77
+ contentType: 'text/markdown',
78
+ });
79
+
80
+ print(ok(`${existing ? 'replaced' : 'wrote'} memory entry ${style.bold(given)}`));
81
+ });
82
+ }
83
+
84
+ export async function memoryForget(id: string | undefined, flags: OwnerFlags): Promise<void> {
85
+ const entryId = required(id, 'lanes link memory forget <id>');
86
+
87
+ await withRuntime(flags, async (runtime) => {
88
+ const store = memoryStore(runtime, flags);
89
+ const entry = await memoryStorage.read(store, entryId);
90
+ if (!entry) throw new ConfigError(`No memory entry "${entryId}" in this profile.`);
91
+
92
+ print(` ${style.bold(entry.id)} ${entry.title}`);
93
+ if (!(await agreed(flags, 'Forget this entry?'))) return;
94
+
95
+ await store.delete(memoryStorage.key(entryId));
96
+ print(ok(`forgot memory entry ${style.bold(entryId)}`));
97
+ });
98
+ }
99
+
100
+ /**
101
+ * The blob namespace core would scope this provider to.
102
+ *
103
+ * Built here from `scopeNamespace` and `scopeBlobStore` — the same two
104
+ * functions `buildProviderContext` uses — rather than from a path spelled out
105
+ * again, so the CLI cannot address a different directory from the provider.
106
+ */
107
+ export function memoryStore(runtime: Runtime, flags: OwnerFlags): BlobStore {
108
+ const connection = ownerConnection(runtime.config, 'memory', flags);
109
+ return scopeBlobStore(runtime.storage, scopeNamespace('memory', connection));
110
+ }