@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,290 @@
1
+ import { ConfigError, resolveDeployTarget, type DeployConfig } from '#profile';
2
+ import { announce, fail, heading, ok, print, style, warn } from '#cli/output.ts';
3
+ import { confirm, isInteractive } from '#cli/prompt.ts';
4
+ import { openSecretStoreFor, resolveProfile, type GlobalFlags } from '#cli/runtime.ts';
5
+ import { resolveTarget, vaultEnv } from './bootstrap.ts';
6
+ import { printSteps, runSteps } from './steps.ts';
7
+ import { driverFor } from './drivers.ts';
8
+ import { prepareSecrets, readableRefs, rotatableRefs } from './prepare.ts';
9
+ import { deployedWorkspace, repairSetupSurface, uploadWorkspace } from './upload.ts';
10
+
11
+ /**
12
+ * `lanes link deploy` — set up what is missing, build the image, roll a revision,
13
+ * print the URL.
14
+ *
15
+ * A thin wrapper, deliberately. `docs/detailed/init.md` is explicit that this must not
16
+ * grow plan-and-apply state files, deploy leases, drift reconciliation, or a
17
+ * rollback manifest: revisions already are the rollback, and that machinery
18
+ * belongs to a multi-target org deployment tool rather than to a single-user
19
+ * instance. What is here is the ordered list of things that have to happen, the
20
+ * questions whose answers it cannot derive, and refusals early enough to be
21
+ * cheap.
22
+ *
23
+ * It knows no vendor. Which platform a target deploys to is a field in its
24
+ * config, and everything vendor-shaped behind `driverFor`.
25
+ */
26
+
27
+ export interface DeployFlags extends GlobalFlags {
28
+ /** Print the commands and the checks without running any of them. */
29
+ readonly dryRun?: boolean | undefined;
30
+ /** Override the declared `access` for this run. */
31
+ readonly access?: string | undefined;
32
+ readonly serviceAccount?: string | undefined;
33
+ readonly tag?: string | undefined;
34
+ /** Skip the confirm before creating cloud resources. */
35
+ readonly yes?: boolean | undefined;
36
+ /**
37
+ * Never prompt: take the setup answers from the config and assume the confirm.
38
+ *
39
+ * Implied when stdin is not a terminal, so a scripted deploy needs no flag —
40
+ * this is for a terminal attached to a job nobody is watching.
41
+ */
42
+ readonly nonInteractive?: boolean | undefined;
43
+ }
44
+
45
+ export async function deploy(flags: DeployFlags): Promise<void> {
46
+ // The one command allowed to name a target that does not exist yet: creating
47
+ // it is what a first deploy is for.
48
+ const { resolution, config } = await resolveProfile(flags, { allowUndeclaredTarget: true });
49
+
50
+ // And the one command that can work out which target it meant. `resolveProfile`
51
+ // falls back to `instance.default_target`, which is the target commands *run*
52
+ // against — `local`, and never the answer to "deploy what".
53
+ const { target, source } = resolveDeployTarget(config, flags.target);
54
+ announce({ ...resolution, target, targetSource: source });
55
+
56
+ // `check` before anything external, per the gate order: a config that will be
57
+ // rejected on boot should be rejected here, not after a five-minute build.
58
+ print(ok(`${resolution.profilePath} is valid`));
59
+
60
+ const declared = await resolveTarget({
61
+ config,
62
+ profilePath: resolution.profilePath,
63
+ workspaceRoot: resolution.workspaceRoot,
64
+ profile: resolution.profile,
65
+ target,
66
+ flags,
67
+ });
68
+
69
+ const deployConfig = declared.deploy!;
70
+ const driver = await driverFor(deployConfig.platform);
71
+
72
+ const tag = flags.tag ?? new Date().toISOString().replace(/[^0-9]/g, '').slice(0, 14);
73
+
74
+ // Which credentials the revision will rewrite, so the steps below can grant
75
+ // each one. Scoped by `flags.profile` exactly as the upload and the repair
76
+ // below are, and read from config and manifests before anything opens a
77
+ // store — `--dry-run` must reach the printed step list without touching a
78
+ // credential.
79
+ const rotatable = await rotatableRefs(resolution.workspaceRoot, flags.profile);
80
+ const readable = await readableRefs(resolution.workspaceRoot, flags.profile, declared);
81
+ const provision = await driver.provision({
82
+ deploy: deployConfig,
83
+ declared,
84
+ target,
85
+ rotatable,
86
+ readable,
87
+ });
88
+
89
+ // Where the running instance will read its config. The bucket the target
90
+ // already declares for everything else — the workspace is not baked into the
91
+ // image any more (ADR-023), so this has to be passed at rollout.
92
+ const workspace = deployedWorkspace(declared);
93
+ const secretEnv = vaultEnv(declared);
94
+
95
+ const rollout = driver.plan({
96
+ deploy: deployConfig,
97
+ tag,
98
+ target,
99
+ ...(flags.profile !== undefined ? { profile: flags.profile } : {}),
100
+ ...(workspace !== undefined ? { workspace } : {}),
101
+ ...(secretEnv ? { secretEnv } : {}),
102
+ });
103
+
104
+ heading(
105
+ `Deploying ${style.bold(deployConfig.service)} to ${deployConfig.region}` +
106
+ (deployConfig.project ? ` (${deployConfig.project})` : ''),
107
+ );
108
+ print(style.dim(` platform: ${deployConfig.platform}`));
109
+ print(reachability(deployConfig.access));
110
+
111
+ if (flags.dryRun) {
112
+ printSteps(driver, [...provision, ...rollout]);
113
+ print('');
114
+ if (workspace) print(style.dim(` the workspace would be uploaded to ${workspace}`));
115
+ print(style.dim(' --dry-run: nothing was run, and no credential was read or written.'));
116
+ return;
117
+ }
118
+
119
+ const missing = driver.preflight();
120
+ if (missing) throw new ConfigError(missing);
121
+
122
+ // First, and before anything reaches for what they create.
123
+ //
124
+ // These used to run after the credential check and the workspace upload,
125
+ // which meant a genuinely first deploy asked Secret Manager for a token in a
126
+ // project where that API was not enabled yet, and wrote config into a bucket
127
+ // that did not exist — failing, both times, several steps before the step
128
+ // that would have fixed it. The ordering was invisible because the second
129
+ // deploy onwards finds everything present and works.
130
+ if (provision.length > 0) {
131
+ heading(`First-run setup (${provision.length} steps)`);
132
+ printSteps(driver, provision);
133
+ print('');
134
+ print(
135
+ style.dim(
136
+ ' Each of these already existing is the expected case on every deploy after\n' +
137
+ ' the first, and is not treated as a failure.',
138
+ ),
139
+ );
140
+ // `--non-interactive` assumes it: a confirm nobody can answer is a hang, and
141
+ // the same run already took its setup answers from the config rather than
142
+ // asking. `--yes` remains the way to skip it with a terminal attached.
143
+ const assumed = flags.yes === true || flags.nonInteractive === true || !isInteractive();
144
+ if (!assumed && !(await confirm(' Create these now?'))) {
145
+ throw new ConfigError('Stopped before creating anything. Nothing was changed.');
146
+ }
147
+ await runSteps(driver, provision);
148
+ }
149
+
150
+ const credentials = await openSecretStoreFor(config, resolution.workspaceRoot, target);
151
+ const prepared = await prepareSecrets({
152
+ config,
153
+ credentials,
154
+ root: resolution.workspaceRoot,
155
+ target,
156
+ readOnly: false,
157
+ });
158
+
159
+ if (prepared.blocking.length > 0) {
160
+ heading('Still missing');
161
+ for (const problem of prepared.blocking) print(fail(problem));
162
+ throw new ConfigError(
163
+ 'The deployed instance cannot start without these. Store them with ' +
164
+ `lanes link secrets set <ref> --target ${target}, or copy a local setup with ` +
165
+ `lanes link secrets push --from local --to ${target}.`,
166
+ );
167
+ }
168
+
169
+ // Repair the local config before it is copied up, not the copy in the bucket.
170
+ //
171
+ // The bucket copy is the wrong end: config flows one way, local CLI to remote
172
+ // instance (ADR-023), and the deployed revision holds `objectViewer` on
173
+ // `profiles/` so it cannot write there anyway. Fixing the local file keeps
174
+ // the two agreeing, which is the property that makes the one-way flow safe.
175
+ //
176
+ // This is also the only place the gap gets noticed. `doctor` reports a
177
+ // profile with no `setup` connection, but the operator of a deployed endpoint
178
+ // has no reason to run it — and the symptom appears in a chat client, days
179
+ // later, as an agent guessing at a command that does not exist.
180
+ // Behind the same guard as the upload, and reading the same scope.
181
+ //
182
+ // The guard because a target with no bucket uploads nothing: repairing for it
183
+ // would rewrite the operator's config as a side effect of a command that
184
+ // copied their config nowhere. The shared scope because a profile that gets
185
+ // uploaded is a profile that gets served, and repairing a narrower set than
186
+ // the upload sends would leave a served profile without the surface — this
187
+ // bug again, one profile over.
188
+ if (workspace) {
189
+ await repairSetupSurface(resolution.workspaceRoot, flags.profile);
190
+
191
+ // Before the rollout, so the revision that comes up finds a config to read.
192
+ // Uploading after would leave a window where the service is serving and the
193
+ // workspace it was told to read is not there yet.
194
+ await uploadWorkspace(resolution.workspaceRoot, workspace, flags.profile);
195
+ }
196
+
197
+ heading('Rolling out');
198
+ await runSteps(driver, rollout);
199
+
200
+ const url = await driver.url(deployConfig);
201
+ if (!url) {
202
+ print(
203
+ warn(
204
+ `deployed, but the platform reported no URL yet — run: lanes link outputs --target ${target}`,
205
+ ),
206
+ );
207
+ return;
208
+ }
209
+
210
+ heading('Endpoint');
211
+ print(` ${url}/mcp`);
212
+ print(await healthLine(url));
213
+ print('');
214
+ print(style.dim(` Register it with: lanes link outputs --target ${target}`));
215
+
216
+ reportUnauthorised(prepared.warnings, target);
217
+ }
218
+
219
+ /**
220
+ * The accounts a browser still has to authorise, and the step after them.
221
+ *
222
+ * Printed last rather than before the build, because this is the only thing
223
+ * left to do and a list eight steps up the scrollback is a list nobody reads.
224
+ *
225
+ * There is no second deploy at the end of it any more, and the reason the old
226
+ * one existed is worth keeping. Connection *credentials* are read live on every
227
+ * call, so a fresh `connect` looked like it should be picked up — but whether a
228
+ * connection was usable at all was decided by a reconcile that ran once per
229
+ * process, so a revision that came up with an account unauthorised went on
230
+ * refusing it, naming the connection rather than the staleness. Reconcile now
231
+ * runs again on every reload, and `connect` asks for one (ADR-029).
232
+ */
233
+ function reportUnauthorised(warnings: readonly string[], target: string): void {
234
+ if (warnings.length === 0) return;
235
+
236
+ heading('Not authorised yet');
237
+ for (const problem of warnings) print(warn(problem));
238
+ print('');
239
+ print(
240
+ style.dim(
241
+ ' A browser consent per account is the one step this cannot take for you:\n' +
242
+ ` lanes link connect <provider> --target ${target}\n` +
243
+ ' Each is served as soon as it is authorised. There is no second deploy —\n' +
244
+ ' deploying is how code gets here, and authorising an account changes none.',
245
+ ),
246
+ );
247
+ }
248
+
249
+ /**
250
+ * Who can reach the service once this lands.
251
+ *
252
+ * Printed on every deploy rather than only when it changes, because it is the
253
+ * one property of a deployment that is invisible from the outside until someone
254
+ * either cannot get in or should not have been able to.
255
+ */
256
+ function reachability(access: DeployConfig['access']): string {
257
+ return access === 'iam'
258
+ ? style.dim(
259
+ ' access: iam — the platform admits only callers holding its own identity\n' +
260
+ ' token. No agent harness can mint one; use --access public with an\n' +
261
+ ' authorization block if a remote MCP client has to reach this.',
262
+ )
263
+ : style.dim(
264
+ ' access: public — the platform lets requests through and this endpoint\n' +
265
+ ' authenticates them. The bearer token is what protects it.',
266
+ );
267
+ }
268
+
269
+ /** Ask the deployed endpoint whether it came up. */
270
+ async function healthLine(url: string): Promise<string> {
271
+ try {
272
+ const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(10_000) });
273
+ if (!response.ok) return warn(`the endpoint answered /health with ${response.status}`);
274
+
275
+ // Asked anonymously, so it reports that the revision is up and nothing
276
+ // about what it serves — the profile list is behind the token now.
277
+ // `lanes link outputs` holds one and prints the rest.
278
+ const body = (await response.json()) as { profiles?: string[] };
279
+ return ok(
280
+ body.profiles
281
+ ? `healthy — serving ${body.profiles.join(', ')}`
282
+ : 'healthy — run `lanes link outputs` for what it serves',
283
+ );
284
+ } catch {
285
+ // A cold start plus a database connect can outrun a short probe, and
286
+ // `access: iam` makes /health unreachable from here by design. Neither is a
287
+ // failed deploy.
288
+ return warn('could not reach /health from here — with access: iam that is expected');
289
+ }
290
+ }
@@ -0,0 +1,157 @@
1
+ import type { DeployConfig, TargetConfig } from '#profile';
2
+
3
+ /**
4
+ * What a deployment has to be able to do to be rolled out.
5
+ *
6
+ * `lanes link deploy` used to *be* the Cloud Run driver — the CLI imported it by
7
+ * path, so the command's grammar was generic while its behaviour was one
8
+ * vendor's. This interface is the seam that makes the README's existing claim
9
+ * true: adding a host is a folder here and a case in `drivers.ts`, and nothing
10
+ * else in the codebase learns about it.
11
+ *
12
+ * Everything a driver produces is **data**, not effects. `plan` and `provision`
13
+ * return steps rather than running them, which is what lets `--dry-run` show a
14
+ * deploy exactly as it will happen, and what lets the argv be asserted in a test
15
+ * with no cloud project anywhere near it. `run` is the only method that touches
16
+ * the world, and it takes one already-built step.
17
+ */
18
+
19
+ export interface CommandResult {
20
+ readonly ok: boolean;
21
+ readonly stdout: string;
22
+ readonly stderr: string;
23
+ }
24
+
25
+ export interface DeployStep {
26
+ readonly title: string;
27
+ /**
28
+ * Arguments to the driver's own tool, one element each.
29
+ *
30
+ * Never a shell string. A service name carrying whitespace and a flag has to
31
+ * arrive as one argument the tool rejects, not as an extra option it obeys.
32
+ */
33
+ readonly argv: readonly string[];
34
+ /** A step whose failure is expected when the thing already exists. */
35
+ readonly tolerateFailure?: boolean;
36
+ }
37
+
38
+ export interface PlanInput {
39
+ readonly deploy: DeployConfig;
40
+ readonly tag: string;
41
+ readonly target: string;
42
+ readonly profile?: string | undefined;
43
+ /**
44
+ * Where the running instance reads its config, as a store URL.
45
+ *
46
+ * Passed at rollout rather than baked into the image, because the bucket
47
+ * belongs to the operator and the image is meant to serve any workspace
48
+ * (ADR-023). Absent for a target that keeps its workspace on a filesystem,
49
+ * which no deployment does.
50
+ */
51
+ readonly workspace?: string | undefined;
52
+ /**
53
+ * Environment the revision must read from its own credential store, as
54
+ * variable name → credential reference.
55
+ *
56
+ * A reference rather than a value, and never a value: this argv is printed by
57
+ * `--dry-run`, echoed by the platform, and kept in a revision's description
58
+ * forever. The driver turns a reference into whatever its host calls a secret
59
+ * mount — which for Cloud Run means knowing the id encoding, and is why that
60
+ * translation lives behind this interface rather than in the generic command.
61
+ */
62
+ readonly secretEnv?: Readonly<Record<string, string>> | undefined;
63
+ }
64
+
65
+ export interface ProvisionInput {
66
+ readonly deploy: DeployConfig;
67
+ /** The whole target, because what needs creating follows from its adapters. */
68
+ readonly declared: TargetConfig;
69
+ readonly target: string;
70
+ /**
71
+ * Credential references the running revision rewrites, so provision can grant
72
+ * each one and leave the rest of the store read-only.
73
+ *
74
+ * Passed in rather than derived here: it comes from the profile's connections
75
+ * and their manifests, which `deployments` cannot reach on its own — see
76
+ * `rotatableRefs` in `./prepare.ts`.
77
+ */
78
+ readonly rotatable?: readonly string[];
79
+ /**
80
+ * Credential references the running revision reads, so provision can bind
81
+ * read on each one rather than across the whole project.
82
+ *
83
+ * Same provenance as `rotatable` and for the same reason — see `readableRefs`
84
+ * in `./prepare.ts`. Empty or absent means the project-wide fallback, which is
85
+ * what a target with no profile to walk still needs.
86
+ */
87
+ readonly readable?: readonly string[];
88
+ }
89
+
90
+ export interface SurveyInput {
91
+ /** Whatever the config already says, so a re-run confirms rather than re-asks. */
92
+ readonly current: Partial<DeployConfig>;
93
+ readonly profile: string;
94
+ /**
95
+ * Whether this profile gates requests itself.
96
+ *
97
+ * It decides which `access` to propose: a profile with its own gate wants the
98
+ * platform door open, because platform IAM in front of it admits only callers
99
+ * that can mint the host's identity token — which no agent harness can.
100
+ */
101
+ readonly gated: boolean;
102
+ /**
103
+ * Whether the target exists at all, or only its `deploy` block is missing.
104
+ *
105
+ * A target that is already declared has its adapters chosen; asking again
106
+ * would be offering to overwrite them. A target that does not exist needs all
107
+ * of it, and every answer but the bucket name follows from the platform.
108
+ */
109
+ readonly adapters: boolean;
110
+ }
111
+
112
+ /**
113
+ * A surveyed target, plus the profile-level settings its answers imply.
114
+ *
115
+ * `authorization` is separate because it does not live under `targets:` — it is
116
+ * `auth.authorization`, and it applies to every target. It comes back from here
117
+ * anyway because the question that decides it is a deployment question: whether
118
+ * a remote client has to reach this endpoint. Nothing else in the config knows
119
+ * to ask, which is why an endpoint that could not be added to a phone was the
120
+ * default outcome of a first deploy.
121
+ */
122
+ export interface SurveyResult {
123
+ readonly target: TargetConfig;
124
+ readonly authorization?: { readonly mode: 'self' } | undefined;
125
+ }
126
+
127
+ export interface DeployDriver {
128
+ readonly platform: DeployConfig['platform'];
129
+ /** The command line this driver shells out to, for printing a plan. */
130
+ readonly tool: string;
131
+
132
+ /** What is missing before this driver can run anything, or null. */
133
+ preflight(): string | null;
134
+
135
+ /** Ask for what the config does not say yet. Interactive. */
136
+ survey(input: SurveyInput): Promise<SurveyResult>;
137
+
138
+ /** Resources this driver can create for itself, in the order they must happen. */
139
+ provision(input: ProvisionInput): Promise<DeployStep[]>;
140
+
141
+ /** The rollout itself. */
142
+ plan(input: PlanInput): DeployStep[];
143
+
144
+ /** The public URL, or null if the service has none yet. */
145
+ url(deploy: DeployConfig): Promise<string | null>;
146
+
147
+ /**
148
+ * Run one step's argv.
149
+ *
150
+ * Streamed by default, because a build takes minutes and silence for the
151
+ * duration is indistinguishable from a hang. `quiet` captures instead, for the
152
+ * short steps whose output is a wall of IAM policy nobody reads and whose
153
+ * failure is usually "already there" — the caller decides what to say about
154
+ * those rather than letting the tool say it.
155
+ */
156
+ run(argv: readonly string[], options?: { quiet?: boolean }): Promise<CommandResult>;
157
+ }
@@ -0,0 +1,35 @@
1
+ import { ConfigError, type DeployConfig } from '#profile';
2
+ import type { DeployDriver } from './driver.ts';
3
+
4
+ /**
5
+ * Turning a declared platform into the driver that rolls it out.
6
+ *
7
+ * **This is the only place the mapping exists**, exactly as `target.ts` is the
8
+ * only place an adapter name becomes an open backend. Before this, the mapping
9
+ * was two static imports in the CLI — `#deployments/gcp/deploy.ts` in `main.ts`
10
+ * and `#deployments/gcp/gcloud.ts` in `outputs.ts` — which meant the CLI knew
11
+ * what Cloud Run was and a second host would have had to be threaded through
12
+ * both.
13
+ *
14
+ * The driver is imported *inside* its branch, for the same reason the cloud
15
+ * adapters are: a `lanes link outputs` against a local target must not load a
16
+ * cloud SDK, and a missing cloud credential must not fail a command that was
17
+ * never going to talk to that cloud.
18
+ */
19
+ export async function driverFor(platform: DeployConfig['platform']): Promise<DeployDriver> {
20
+ switch (platform) {
21
+ case 'cloudrun': {
22
+ const { cloudRunDriver } = await import('./gcp/driver.ts');
23
+ return cloudRunDriver;
24
+ }
25
+ }
26
+
27
+ // Unreachable while `platform` is a closed enum, and kept anyway: the enum
28
+ // and this switch are edited by different hands at different times, and a
29
+ // platform that parses but does not dispatch should say so rather than
30
+ // return undefined into a call chain.
31
+ throw new ConfigError(
32
+ `No deployment driver for platform "${platform as string}". ` +
33
+ 'Declare one of: cloudrun.',
34
+ );
35
+ }
@@ -0,0 +1,70 @@
1
+ # syntax=docker/dockerfile:1
2
+ #
3
+ # The deployed target's image. Built from the repository root:
4
+ #
5
+ # docker build -f src/deployments/gcp/Dockerfile -t lanes-link .
6
+ #
7
+ # `lanes link deploy` runs the equivalent through Cloud Build. See
8
+ # docs/deployment-cloudrun.md for the whole loop, and for why the config is
9
+ # baked in rather than mounted.
10
+ #
11
+ # There is no build step: Bun runs TypeScript directly, so the image holds
12
+ # source, and `bun install` only fetches the third-party dependencies.
13
+
14
+ FROM oven/bun:1.3.11-slim
15
+
16
+ WORKDIR /app
17
+
18
+ # The manifest first, so a source-only change reuses the install layer. One
19
+ # package now, rather than fifteen manifests copied ahead of the install to
20
+ # satisfy workspace resolution.
21
+ COPY package.json bun.lock bunfig.toml ./
22
+
23
+ RUN bun install --frozen-lockfile
24
+
25
+ COPY src/ src/
26
+
27
+ # The workspace is NOT copied in.
28
+ #
29
+ # It used to be: `COPY lanes-link.yaml` and `COPY profiles/`, on the reasoning
30
+ # that the image *is* the config, so a revision fully described what it served
31
+ # and rollback was a revision switch. The cost was a rebuild per config change,
32
+ # which the comment here called "the tradeoff docs/init.md accepts until it
33
+ # becomes annoying".
34
+ #
35
+ # It became annoying, and it had a second cost that was easier to miss: those
36
+ # paths are gitignored, so the image could not be built from a clean checkout
37
+ # at all. That is what stopped there ever being one image that serves any
38
+ # workspace.
39
+ #
40
+ # Config now lives wherever LANES_LINK_HOME points — a bucket, for a
41
+ # deployment — and `lanes link deploy` uploads it there. See ADR-023 for what
42
+ # that gives up and how the guarantee it kept is kept.
43
+ #
44
+ # What must still NOT arrive here is `data/`: the encrypted credential store
45
+ # and its key live there, and the deployed target reads credentials from Secret
46
+ # Manager instead. The root `.dockerignore` excludes it, and that exclusion is
47
+ # load bearing rather than an optimisation.
48
+
49
+ # Set at deploy time, not here: a bucket URL is the operator's, not the image's.
50
+ # `lanes link deploy` passes it as `LANES_LINK_HOME=gs://<bucket>`. Left unset,
51
+ # `resolveWorkspaceRoot` would walk ancestors for `lanes-link.yaml` and fall
52
+ # back to `~/.lanes-link`, which in a container is a directory nobody wrote —
53
+ # so the endpoint refuses rather than serving an empty workspace.
54
+ # Which adapter set to open. The whole point of the milestone is that this is
55
+ # the only thing that differs from a local run.
56
+ ENV LANES_LINK_TARGET=cloud
57
+ # Cloud Run injects its own PORT; this is the default it also uses, so the
58
+ # image is runnable unchanged with `docker run -p 8080:8080`.
59
+ ENV PORT=8080
60
+
61
+ # No `data/` directory: the container writes nothing to its own filesystem.
62
+ # State, the log, memory, skills and attachments are all objects in the bucket,
63
+ # and credentials come from Secret Manager. The image can run read-only.
64
+
65
+ USER bun
66
+ EXPOSE 8080
67
+
68
+ # Not `lanes link start`: that command is a terminal UI that mints a token when
69
+ # none exists. See the note at the top of src/server/container.ts.
70
+ CMD ["bun", "run", "src/server/container.ts"]
@@ -0,0 +1,31 @@
1
+ # Cloud Build config for the deployed target's image.
2
+ #
3
+ # It exists so `gcloud builds submit` can name a Dockerfile that is not at the
4
+ # context root — `--tag` alone always builds `./Dockerfile`, and moving the
5
+ # Dockerfile to the repository root to satisfy that would put deployment
6
+ # machinery in front of everyone who only ever runs locally.
7
+ #
8
+ # Invoked by `lanes link deploy`, which supplies _IMAGE:
9
+ #
10
+ # gcloud builds submit \
11
+ # --config src/deployments/gcp/cloudbuild.yaml \
12
+ # --substitutions _IMAGE=<region>-docker.pkg.dev/<project>/lanes-link/<service>:<tag> .
13
+ #
14
+ # The build context is the repository root, and `.dockerignore` there decides
15
+ # what reaches the builder. `data/` must not — see the note in that file.
16
+
17
+ steps:
18
+ - name: gcr.io/cloud-builders/docker
19
+ args:
20
+ - build
21
+ - --file
22
+ - src/deployments/gcp/Dockerfile
23
+ - --tag
24
+ - ${_IMAGE}
25
+ - .
26
+
27
+ images:
28
+ - ${_IMAGE}
29
+
30
+ options:
31
+ logging: CLOUD_LOGGING_ONLY