@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,125 @@
1
+ import { ConfigError, type Config } from '#profile';
2
+ import { announce, print, style } from '../../output.ts';
3
+ import { confirm } from '../../prompt.ts';
4
+ import { openRuntime, type GlobalFlags, type Runtime } from '../../runtime.ts';
5
+
6
+ /**
7
+ * What `lanes link memory`, `lanes link skills` and `lanes link vault` all need: the flag shape, the
8
+ * runtime wrapper, connection resolution, and the two prompts.
9
+ *
10
+ * All four commands are the same shape — open a runtime, announce, act, close —
11
+ * so the wrapper lives here rather than three times over.
12
+ */
13
+
14
+ export interface OwnerFlags extends GlobalFlags {
15
+ /** Which connection of this provider, when a profile has more than one. */
16
+ readonly connection?: string | undefined;
17
+ readonly title?: string | undefined;
18
+ readonly tag?: string | undefined;
19
+ readonly description?: string | undefined;
20
+ readonly file?: string | undefined;
21
+ /** Reveal a vault value on a terminal. */
22
+ readonly show?: boolean | undefined;
23
+ /** Print only the value, for `$(…)`. */
24
+ readonly raw?: boolean | undefined;
25
+ /** Skip the confirmation a destructive command would otherwise ask for. */
26
+ readonly yes?: boolean | undefined;
27
+ }
28
+
29
+ export async function withRuntime(
30
+ flags: OwnerFlags,
31
+ body: (runtime: Runtime) => Promise<void>,
32
+ ): Promise<void> {
33
+ const runtime = await openRuntime(flags);
34
+ try {
35
+ // Every command announces its profile and target before acting — except
36
+ // under `--raw`, whose entire purpose is that `$(…)` captures the value and
37
+ // nothing else. The same exception `lanes link token show --raw` makes.
38
+ if (!flags.raw) announce(runtime.resolution);
39
+ await body(runtime);
40
+ } finally {
41
+ await runtime.close();
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Which connection of an owner provider to act on.
47
+ *
48
+ * A profile usually has exactly one of each, and typing `--connection` to reach
49
+ * your only one would be pure ceremony — the same trade ADR-012 §1 made for
50
+ * prompt routing. Two or more is genuinely ambiguous, so it refuses and names
51
+ * the candidates rather than picking.
52
+ */
53
+ export function ownerConnection(config: Config, provider: string, flags: OwnerFlags): string {
54
+ const candidates = config.connections.filter((connection) => connection.provider === provider);
55
+
56
+ if (flags.connection) {
57
+ const match = candidates.find((connection) => connection.id === flags.connection);
58
+ if (!match) {
59
+ throw new ConfigError(
60
+ `No ${provider} connection "${flags.connection}" in this profile` +
61
+ (candidates.length > 0
62
+ ? ` (have: ${candidates.map((connection) => connection.id).join(', ')})`
63
+ : ''),
64
+ );
65
+ }
66
+ return match.id;
67
+ }
68
+
69
+ if (candidates.length === 0) {
70
+ throw new ConfigError(
71
+ `This profile has no ${provider} connection. Add one with: lanes link connect ${provider}`,
72
+ );
73
+ }
74
+ if (candidates.length > 1) {
75
+ throw new ConfigError(
76
+ `This profile has ${candidates.length} ${provider} connections ` +
77
+ `(${candidates.map((connection) => connection.id).join(', ')}). Name one with --connection.`,
78
+ );
79
+ }
80
+
81
+ return candidates[0]!.id;
82
+ }
83
+
84
+ /**
85
+ * Read a value from stdin, refusing a terminal.
86
+ *
87
+ * The same shape as `lanes link secrets set`, for the same two reasons: a value on argv
88
+ * lands in shell history, and a command that silently waits on a terminal that
89
+ * will never send anything is a hang rather than an error.
90
+ */
91
+ export async function readStdin(usage: string, what: string): Promise<string> {
92
+ if (process.stdin.isTTY) {
93
+ throw new ConfigError(
94
+ `${usage} reads ${what} from stdin, and stdin is a terminal.\n` +
95
+ ` printf %s "<value>" | ${usage}\n` +
96
+ ` or: ${usage} < file.md`,
97
+ );
98
+ }
99
+
100
+ const text = (await Bun.stdin.text()).replace(/\n$/, '');
101
+ if (!text) throw new ConfigError(`Nothing on stdin. Pipe ${what} in: ${usage} < file`);
102
+ return text;
103
+ }
104
+
105
+ /** Confirm a destructive action, unless `--yes` already answered. */
106
+ export async function agreed(flags: OwnerFlags, question: string): Promise<boolean> {
107
+ if (flags.yes) return true;
108
+
109
+ if (!process.stdin.isTTY) {
110
+ throw new ConfigError(
111
+ `${question} — stdin is not a terminal, so there is nobody to ask. Pass --yes to proceed.`,
112
+ );
113
+ }
114
+
115
+ // Defaulting to no: the same choice `connect` makes where a prompt precedes
116
+ // something that cannot be undone.
117
+ const yes = await confirm(question, false);
118
+ if (!yes) print(style.dim(' cancelled'));
119
+ return yes;
120
+ }
121
+
122
+ export function required(value: string | undefined, usage: string): string {
123
+ if (!value) throw new ConfigError(`Usage: ${usage}`);
124
+ return value;
125
+ }
@@ -0,0 +1,92 @@
1
+ import { ConfigError } from '#profile';
2
+ import {
3
+ loadProfileSkills,
4
+ readSkill,
5
+ removeSkill,
6
+ writeSkill,
7
+ } from '#providers/skills/store.ts';
8
+ import { heading, ok, print, style, table } from '../../output.ts';
9
+ import { agreed, readStdin, required, withRuntime, type OwnerFlags } from './shared.ts';
10
+
11
+ /** `lanes link skills` — the reusable procedures agents can invoke. */
12
+
13
+ export async function skillsList(flags: OwnerFlags): Promise<void> {
14
+ await withRuntime(flags, async (runtime) => {
15
+ const skills = await loadProfileSkills(runtime.skills);
16
+
17
+ heading(`Skills (${skills.length})`);
18
+ if (skills.length === 0) {
19
+ print(style.dim(' none — add one with: lanes link skills add <name>'));
20
+ return;
21
+ }
22
+
23
+ table(
24
+ skills.map((skill) => [
25
+ ` ${skill.name}`,
26
+ skill.arguments.length > 0
27
+ ? style.dim(
28
+ skill.arguments
29
+ .map((argument) => (argument.required ? argument.name : `${argument.name}?`))
30
+ .join(', '),
31
+ )
32
+ : '',
33
+ skill.description,
34
+ ]),
35
+ );
36
+
37
+ print('');
38
+ print(style.dim(' Each is the MCP prompt "skills_<name>", where policy allows it.'));
39
+ });
40
+ }
41
+
42
+ export async function skillsShow(name: string | undefined, flags: OwnerFlags): Promise<void> {
43
+ const skillName = required(name, 'lanes link skills show <name>');
44
+
45
+ await withRuntime(flags, async (runtime) => {
46
+ const skill = await readSkill(runtime.skills, skillName);
47
+ if (!skill) throw new ConfigError(`No skill "${skillName}" in this workspace.`);
48
+
49
+ const bytes = await runtime.skills.get(skill.path);
50
+ print('');
51
+ print(bytes ? new TextDecoder().decode(bytes) : skill.body);
52
+ });
53
+ }
54
+
55
+ export async function skillsAdd(name: string | undefined, flags: OwnerFlags): Promise<void> {
56
+ const skillName = required(name, 'lanes link skills add <name> (document on stdin, or --file)');
57
+
58
+ const text = flags.file
59
+ ? await Bun.file(flags.file).text()
60
+ : await readStdin(
61
+ `lanes link skills add ${skillName}`,
62
+ 'the skill document, frontmatter included',
63
+ );
64
+
65
+ await withRuntime(flags, async (runtime) => {
66
+ const existing = await readSkill(runtime.skills, skillName);
67
+ const skill = await writeSkill(runtime.skills, skillName, text);
68
+
69
+ print(ok(`${existing ? 'replaced' : 'added'} skill ${style.bold(skill.name)}`));
70
+ print(
71
+ style.dim(
72
+ ` Available as the prompt "skills_${skill.name}". A running endpoint picks it up within a few seconds.`,
73
+ ),
74
+ );
75
+ });
76
+ }
77
+
78
+ export async function skillsRemove(name: string | undefined, flags: OwnerFlags): Promise<void> {
79
+ const skillName = required(name, 'lanes link skills remove <name>');
80
+
81
+ await withRuntime(flags, async (runtime) => {
82
+ const skill = await readSkill(runtime.skills, skillName);
83
+ if (!skill) throw new ConfigError(`No skill "${skillName}" in this workspace.`);
84
+
85
+ print(` ${style.bold(skill.name)} ${skill.description}`);
86
+ print(style.dim(` ${skill.path}`));
87
+ if (!(await agreed(flags, 'Remove this skill?'))) return;
88
+
89
+ await removeSkill(runtime.skills, skillName);
90
+ print(ok(`removed skill ${style.bold(skillName)}`));
91
+ });
92
+ }
@@ -0,0 +1,140 @@
1
+ import { ConfigError } from '#profile';
2
+ import { assertItemId, generateVaultKey } from '#providers/owner.ts';
3
+ import { heading, ok, print, style, table } from '../../output.ts';
4
+ import {
5
+ agreed,
6
+ ownerConnection,
7
+ readStdin,
8
+ required,
9
+ withRuntime,
10
+ type OwnerFlags,
11
+ } from './shared.ts';
12
+
13
+ /**
14
+ * `lanes link vault` — the owner's own secret material.
15
+ *
16
+ * `lanes link vault get` prints a value, which `lanes link secrets` deliberately never does.
17
+ * That is the two-kinds-of-secret distinction doing its job
18
+ * (`docs/detailed/security.md`): a credential authorises the system and is never
19
+ * disclosed, and a vault the owner cannot read without an agent is not a vault.
20
+ */
21
+
22
+ export async function vaultList(flags: OwnerFlags): Promise<void> {
23
+ await withRuntime(flags, async (runtime) => {
24
+ const items = await runtime.vault.ids();
25
+
26
+ heading(`Vault items (${items.length})`);
27
+ if (items.length === 0) {
28
+ print(style.dim(' none — store one with: lanes link vault set <id>'));
29
+ return;
30
+ }
31
+
32
+ table(
33
+ items.map((item) => [
34
+ ` ${item.id}`,
35
+ style.dim(item.connectionId),
36
+ item.description ?? '',
37
+ ]),
38
+ );
39
+
40
+ print('');
41
+ print(
42
+ style.dim(
43
+ ' Names only. Each is readable over MCP as "vault.get.<id>", one grant at a time.',
44
+ ),
45
+ );
46
+ });
47
+ }
48
+
49
+ export async function vaultGet(id: string | undefined, flags: OwnerFlags): Promise<void> {
50
+ const itemId = required(id, 'lanes link vault get <id> [--show]');
51
+
52
+ await withRuntime(flags, async (runtime) => {
53
+ const connection = ownerConnection(runtime.config, 'vault', flags);
54
+ const item = await runtime.vault.get(connection, itemId);
55
+ if (!item) throw new ConfigError(`No vault item "${itemId}" in this profile.`);
56
+
57
+ // `--raw` writes the value and nothing else, so `$(…)` is not contaminated
58
+ // by the announce line every other command prints. Same shape as
59
+ // `lanes link token show --raw`.
60
+ if (flags.raw) {
61
+ process.stdout.write(`${item.value}\n`);
62
+ return;
63
+ }
64
+
65
+ print(
66
+ flags.show
67
+ ? item.value
68
+ : `${item.value.slice(0, 3)}… ${style.dim(`(${item.value.length} chars — --show to reveal)`)}`,
69
+ );
70
+ });
71
+ }
72
+
73
+ export async function vaultSet(id: string | undefined, flags: OwnerFlags): Promise<void> {
74
+ const itemId = required(id, 'lanes link vault set <id> (value on stdin)');
75
+ assertItemId(itemId);
76
+
77
+ const value = await readStdin(`lanes link vault set ${itemId}`, 'the secret');
78
+
79
+ await withRuntime(flags, async (runtime) => {
80
+ const connection = ownerConnection(runtime.config, 'vault', flags);
81
+ const replacing = (await runtime.vault.get(connection, itemId)) !== null;
82
+
83
+ await runtime.vault.put(connection, {
84
+ id: itemId,
85
+ value,
86
+ ...(flags.description ? { description: flags.description } : {}),
87
+ });
88
+
89
+ print(ok(`${replacing ? 'replaced' : 'stored'} vault item ${style.bold(itemId)}`));
90
+ if (!replacing) {
91
+ // Not a limitation being apologised for: the item list is read when the
92
+ // runtime is built, so a write cannot hand itself a read and granting a
93
+ // new secret is a deliberate act between two runs (ADR-012 §3).
94
+ print(
95
+ style.dim(
96
+ ` Not readable over MCP yet. Allow "vault.get.${itemId}" in policy, then restart the endpoint.`,
97
+ ),
98
+ );
99
+ }
100
+ });
101
+ }
102
+
103
+ export async function vaultRemove(id: string | undefined, flags: OwnerFlags): Promise<void> {
104
+ const itemId = required(id, 'lanes link vault remove <id>');
105
+
106
+ await withRuntime(flags, async (runtime) => {
107
+ const connection = ownerConnection(runtime.config, 'vault', flags);
108
+ const item = await runtime.vault.get(connection, itemId);
109
+ if (!item) throw new ConfigError(`No vault item "${itemId}" in this profile.`);
110
+
111
+ print(` ${style.bold(itemId)} ${item.description ?? style.dim('no description')}`);
112
+ if (!(await agreed(flags, 'Remove this item? The value cannot be recovered.'))) return;
113
+
114
+ await runtime.vault.delete(connection, itemId);
115
+ print(ok(`removed vault item ${style.bold(itemId)}`));
116
+ print(style.dim(' Its read capability disappears when the endpoint restarts.'));
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Mint a vault key.
122
+ *
123
+ * Deliberately not written anywhere: a deployment's key belongs in that
124
+ * deployment's secret manager, and a command that helpfully stored it beside
125
+ * the ciphertext it protects would encrypt nothing.
126
+ */
127
+ export function vaultKeyGenerate(flags: OwnerFlags): void {
128
+ const key = generateVaultKey();
129
+
130
+ if (flags.raw) {
131
+ process.stdout.write(`${key}\n`);
132
+ return;
133
+ }
134
+
135
+ print(key);
136
+ print('');
137
+ print(style.dim(' Store it as LANES_LINK_VAULT_KEY. It is not written anywhere by this command:'));
138
+ print(style.dim(' a key kept beside the document it protects protects nothing.'));
139
+ print(style.dim(' Separate from the credential-store key, deliberately — docs/detailed/security.md.'));
140
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * `lanes link memory`, `lanes link skills`, `lanes link vault` — the owner layer's control plane.
3
+ *
4
+ * The layer shipped in M4 with no CLI at all, so the two stores holding the
5
+ * owner's *own* data were reachable only by an agent, and the one thing that
6
+ * shapes agent behaviour was reachable only by editing a file and restarting.
7
+ * Putting a password into the vault meant asking a language model to type it.
8
+ * ADR-014.
9
+ *
10
+ * **These commands reach the same bytes the providers do**, through the same
11
+ * stores — `runtime.skills`, `runtime.vault`, and the blob namespace core
12
+ * scopes memory to. Nothing here reimplements a storage layout, because two
13
+ * spellings of one layout is exactly how a control plane and its data plane
14
+ * drift apart.
15
+ *
16
+ * This does not widen ADR-007. Its exclusion list is policy, tokens,
17
+ * credentials, connections, config, and audit — the things that authorise
18
+ * *future agent behaviour*. A memory entry and a vault item are the owner's own
19
+ * data. Skills are the interesting case and are deliberately reachable from
20
+ * both sides now: from here because this is the owner's control plane, and over
21
+ * MCP because ADR-014 §1 decided a policy-gated grant beats a missing path.
22
+ *
23
+ * One noun per file — `memory.ts`, `skills.ts`, `vault.ts` — over the shape all
24
+ * three share in `shared.ts`: the flag type, the open-announce-act-close
25
+ * wrapper, connection resolution, and the two prompts.
26
+ */
27
+
28
+ export {
29
+ memoryForget,
30
+ memoryGet,
31
+ memoryList,
32
+ memoryStore,
33
+ memoryWrite,
34
+ } from './owner/memory.ts';
35
+
36
+ export { skillsAdd, skillsList, skillsRemove, skillsShow } from './owner/skills.ts';
37
+
38
+ export {
39
+ vaultGet,
40
+ vaultKeyGenerate,
41
+ vaultList,
42
+ vaultRemove,
43
+ vaultSet,
44
+ } from './owner/vault.ts';
45
+
46
+ export { ownerConnection, type OwnerFlags } from './owner/shared.ts';
@@ -0,0 +1,278 @@
1
+ import { layout, profilePath, workspacePath, type Config, type TargetConfig } from '#profile';
2
+ import type { SecretStore } from '#secrets';
3
+ import type { BlobStore } from '#stores/blobs';
4
+ import { credentialRefFor, ownClientRefsFor, type ProviderRegistry } from '#registry';
5
+ import { print, style, warn } from '../../output.ts';
6
+
7
+ /**
8
+ * What a profile's removal is allowed to delete, worked out before any of it
9
+ * happens.
10
+ *
11
+ * Everything here is a read. The plan is a value, and the preview an operator
12
+ * confirms from is that value rendered — which is what stops what they were
13
+ * shown and what runs from drifting apart. `../../../deployments/driver.ts`
14
+ * gives the same reason for the same shape.
15
+ */
16
+
17
+ /**
18
+ * The credential references this profile declares, and only those.
19
+ *
20
+ * Locally the profile is the boundary: its credentials are a file inside its own
21
+ * directory, and deleting the directory is the whole operation. In Secret
22
+ * Manager they are flat names in one project, so two profiles deployed to the
23
+ * same project share a namespace and `list()` hands back the other one's as
24
+ * readily as its own. Deriving from what this profile declares is the only
25
+ * answer that cannot delete something that was never ours, and a secret deleted
26
+ * in the wrong project is not recoverable.
27
+ *
28
+ * The cost is that a genuinely orphaned ref — one whose connection was removed
29
+ * from config long ago — is not derivable and so is not deleted. The caller
30
+ * reports those rather than guessing, which is the honest position: a guess
31
+ * here is indistinguishable from another profile's credential.
32
+ */
33
+ export function declaredRefs(
34
+ config: Config,
35
+ registry: ProviderRegistry,
36
+ declared: TargetConfig,
37
+ ): string[] {
38
+ const refs = new Set<string>([config.auth.token_ref]);
39
+
40
+ // Read off the *target*, not the profile. `vaultTargetSchema` sits inside
41
+ // `targetSchema`, so two targets may seal the same profile's items in
42
+ // different places; taking it from the profile would attach one target's
43
+ // vault to another target's removal.
44
+ if (declared.vault?.adapter === 'secret') {
45
+ refs.add(declared.vault.ref ?? 'vault/document');
46
+ }
47
+
48
+ for (const connection of config.connections) {
49
+ const manifest = registry.manifest(connection.provider);
50
+
51
+ // `credentialRefFor` rather than `credentialRefForConnection`: it is the
52
+ // single authority on where a connection's credential lives, and it exists
53
+ // because two callers once derived the answer differently and disagreed.
54
+ // Asking the lower-level one here would open that gap again from a third
55
+ // side — and it would miss a `local` provider, whose only ref is the one
56
+ // the connection declares itself.
57
+ const ref = credentialRefFor(connection, manifest);
58
+ if (ref) refs.add(ref);
59
+
60
+ if (manifest) {
61
+ for (const own of ownClientRefsFor(manifest, config.oauth_apps)) refs.add(own);
62
+ }
63
+ }
64
+
65
+ return [...refs];
66
+ }
67
+
68
+ export interface RemovalItem {
69
+ /** `null` for a workspace-level item — the config file, the default-profile key. */
70
+ readonly target: string | null;
71
+ readonly kind: 'secret' | 'blob' | 'file' | 'config' | 'workspace-key';
72
+ readonly id: string;
73
+ readonly note?: string;
74
+ }
75
+
76
+ export interface RemovalPlan {
77
+ readonly profile: string;
78
+ readonly items: readonly RemovalItem[];
79
+ /** Present in a target's store, not declared by this profile. Left alone. */
80
+ readonly untouched: readonly { readonly target: string; readonly refs: readonly string[] }[];
81
+ readonly warnings: readonly string[];
82
+ }
83
+
84
+ export interface PlanOptions {
85
+ /** Restrict to one target. The profile itself then survives. */
86
+ readonly target?: string | undefined;
87
+ readonly openSecrets: (target: string) => Promise<SecretStore>;
88
+ readonly openBlobs: (target: string, area?: string) => Promise<BlobStore>;
89
+ readonly readDefaultProfile?: (() => Promise<string | undefined>) | undefined;
90
+ }
91
+
92
+ const reason = (cause: unknown): string => (cause instanceof Error ? cause.message : String(cause));
93
+
94
+ /**
95
+ * Everything removing this profile would delete, before any of it is deleted.
96
+ *
97
+ * Read-only. A store that will not open becomes a warning rather than a throw,
98
+ * because a target whose project is gone must not be able to strand a profile
99
+ * on the machine forever — and the operator sees that warning in the preview,
100
+ * before they confirm, rather than discovering it half way through.
101
+ */
102
+ export async function removalPlan(
103
+ config: Config,
104
+ root: string,
105
+ profile: string,
106
+ registry: ProviderRegistry,
107
+ options: PlanOptions,
108
+ ): Promise<RemovalPlan> {
109
+ const items: RemovalItem[] = [];
110
+ const untouched: { target: string; refs: string[] }[] = [];
111
+ const warnings: string[] = [];
112
+
113
+ const names = options.target ? [options.target] : Object.keys(config.targets);
114
+
115
+ for (const name of names) {
116
+ const declared = config.targets[name];
117
+ if (!declared) {
118
+ warnings.push(`Target "${name}" is not declared by this profile, so nothing was planned.`);
119
+ continue;
120
+ }
121
+
122
+ // Secrets first, and this ordering is load-bearing rather than tidy:
123
+ // `layout.credentials(p)` is `data/<p>/credentials.enc`, *inside* the blob
124
+ // root `data/<p>`. For the file adapter the credential store is itself a
125
+ // blob, so blobs-first would delete the store the secret deletions read
126
+ // through and turn every one of them into a failure.
127
+ try {
128
+ const secrets = await options.openSecrets(name);
129
+ const present = await secrets.list();
130
+ const mine = new Set(declaredRefs(config, registry, declared));
131
+
132
+ for (const ref of present) if (mine.has(ref)) items.push({ target: name, kind: 'secret', id: ref });
133
+
134
+ const theirs = present.filter((ref) => !mine.has(ref));
135
+ if (theirs.length > 0) untouched.push({ target: name, refs: theirs });
136
+ } catch (cause) {
137
+ warnings.push(
138
+ `Target "${name}": its credential store could not be opened (${reason(cause)}), so nothing in it will be removed.`,
139
+ );
140
+ }
141
+
142
+ try {
143
+ const blobs = await options.openBlobs(name);
144
+ for (const blob of await blobs.list()) items.push({ target: name, kind: 'blob', id: blob.key });
145
+ } catch (cause) {
146
+ warnings.push(
147
+ `Target "${name}": its storage could not be opened (${reason(cause)}), so nothing in it will be removed.`,
148
+ );
149
+ }
150
+
151
+ // The blob store's root is the profile's own directory, and an adapter must
152
+ // never delete the root it was configured with — so emptying it leaves the
153
+ // directory. `layout.ts` frames that directory as the unit ("rm -r
154
+ // data/work is exactly remove the work profile's data"), and one left
155
+ // behind is silently reused by a later `profile add` of the same name.
156
+ if (declared.storage.adapter === 'filesystem') {
157
+ const blobRoot = declared.storage.path ?? layout.blobs(profile);
158
+ items.push({
159
+ target: name,
160
+ kind: 'file',
161
+ id: blobRoot,
162
+ note: 'the profile directory, once emptied',
163
+ });
164
+
165
+ // Skills and manifests sit at fixed areas under `data/<profile>/`, the
166
+ // same way state and the log do — an explicit area, so a profile that
167
+ // declares its own `storage.path` moves its provider blobs and leaves
168
+ // these where `layout` says they are. The sweep above walks the blob root
169
+ // and would then walk straight past them, so they are named. Ordinary
170
+ // case: both are already inside `blobRoot` and the sweep has them, which
171
+ // is why this only fires when the two have been pointed apart.
172
+ //
173
+ // Resolved before comparing, because they are not compared as paths
174
+ // otherwise: `newProfileTemplate` writes `./data/<profile>` and
175
+ // `layout.blobs` returns `data/<profile>`, which is one directory spelled
176
+ // two ways — the same leading `./` that `profile/layout.ts` has a
177
+ // paragraph about. Comparing the strings names every default profile's
178
+ // skills twice in the preview an operator confirms.
179
+ if (workspacePath(root, blobRoot) !== workspacePath(root, layout.blobs(profile))) {
180
+ for (const area of [layout.skills(profile), layout.providers(profile)]) {
181
+ items.push({
182
+ target: name,
183
+ kind: 'file',
184
+ id: area,
185
+ note: 'outside the declared storage path, so not swept with it',
186
+ });
187
+ }
188
+ }
189
+ }
190
+
191
+ // A deployed revision reads its config from the bucket rather than the
192
+ // image (ADR-023), so that copy is the profile too — and it is outside the
193
+ // profile's blob tree, which is why it needs its own area.
194
+ if (declared.storage.adapter === 'gcs' || declared.storage.adapter === 's3') {
195
+ items.push({
196
+ target: name,
197
+ kind: 'config',
198
+ id: `profiles/${profile}.yaml`,
199
+ note: 'the copy a deployed revision reads',
200
+ });
201
+ }
202
+
203
+ // `deploy` only: the schema folds the deprecated `cloudrun` block into it,
204
+ // so the resolved config has one answer rather than two that could differ.
205
+ if (declared.deploy) {
206
+ warnings.push(
207
+ `Target "${name}" is deployed. The service will keep answering, and every call will fail, because what it served is gone. Tearing it down is not part of this.`,
208
+ );
209
+ }
210
+ }
211
+
212
+ // Only when the whole profile is going. With `--target` it still exists.
213
+ if (!options.target) {
214
+ const defaultProfile = await options.readDefaultProfile?.();
215
+ if (defaultProfile === profile) {
216
+ items.push({
217
+ target: null,
218
+ kind: 'workspace-key',
219
+ id: 'default_profile',
220
+ note: 'cleared, not repointed at whatever remains',
221
+ });
222
+ }
223
+
224
+ // Last. It is the only record of where everything else lives, so a failure
225
+ // before this point leaves data a later run can still find.
226
+ items.push({ target: null, kind: 'config', id: profilePath(root, profile) });
227
+ }
228
+
229
+ return { profile, items, untouched, warnings };
230
+ }
231
+
232
+ const KIND_LABEL: Record<RemovalItem['kind'], string> = {
233
+ secret: 'credential',
234
+ blob: 'object',
235
+ file: 'file',
236
+ config: 'config',
237
+ 'workspace-key': 'workspace',
238
+ };
239
+
240
+ /**
241
+ * The plan, as the thing an operator decides from.
242
+ *
243
+ * Rendered from the same value that is executed, so there is no second
244
+ * description of the work to fall out of step with the first. It prints
245
+ * references and keys and never a value — the plan holds no values to print.
246
+ */
247
+ export function renderPlan(plan: RemovalPlan): void {
248
+ print();
249
+ print(`Removing profile ${style.bold(plan.profile)} would delete:`);
250
+ print();
251
+
252
+ if (plan.items.length === 0) {
253
+ print(style.dim(' nothing — there is no trace of this profile left to remove.'));
254
+ }
255
+
256
+ const targets = [...new Set(plan.items.map((item) => item.target))];
257
+ for (const target of targets) {
258
+ const items = plan.items.filter((item) => item.target === target);
259
+ print(` ${style.bold(target ?? 'workspace')}`);
260
+ for (const item of items) {
261
+ const note = item.note ? style.dim(` — ${item.note}`) : '';
262
+ print(` ${KIND_LABEL[item.kind].padEnd(10)} ${item.id}${note}`);
263
+ }
264
+ print();
265
+ }
266
+
267
+ for (const { target, refs } of plan.untouched) {
268
+ // Named rather than counted, because the operator is the only one who can
269
+ // tell an orphan from another profile's live credential — and this command
270
+ // deliberately will not guess.
271
+ print(` ${style.bold(target)}: present but not declared by this profile, so left alone`);
272
+ for (const ref of refs) print(style.dim(` ${ref}`));
273
+ print();
274
+ }
275
+
276
+ for (const warning of plan.warnings) print(warn(warning));
277
+ if (plan.warnings.length > 0) print();
278
+ }