@lanes-sh/link 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +20 -10
  2. package/instructions/agents/lanes-link-scout.md +2 -2
  3. package/instructions/skills/lanes-link/SKILL.md +148 -73
  4. package/package.json +2 -1
  5. package/src/audit/index.ts +8 -1
  6. package/src/auth/index.ts +58 -2
  7. package/src/auth/lanes/assertion.ts +256 -0
  8. package/src/auth/lanes/callback.ts +135 -0
  9. package/src/auth/lanes/federation.ts +50 -0
  10. package/src/auth/lanes/login.ts +294 -0
  11. package/src/auth/lanes/members.ts +103 -0
  12. package/src/auth/lanes/session.ts +97 -0
  13. package/src/auth/oauth/grant.ts +183 -0
  14. package/src/auth/oauth/result.ts +27 -0
  15. package/src/auth/oauth/server.ts +176 -203
  16. package/src/auth/oauth/store.ts +85 -4
  17. package/src/auth/remote.ts +32 -9
  18. package/src/cli/accepts.ts +109 -0
  19. package/src/cli/argv.ts +57 -3
  20. package/src/cli/audit-change.ts +140 -0
  21. package/src/cli/callback-page.ts +36 -115
  22. package/src/cli/commands/auth-dispatch.ts +48 -0
  23. package/src/cli/commands/auth.ts +229 -0
  24. package/src/cli/commands/connect/accounts.ts +4 -4
  25. package/src/cli/commands/connect/authorise.ts +4 -4
  26. package/src/cli/commands/connect/bind-credential.ts +2 -1
  27. package/src/cli/commands/connect/custom/index.ts +1 -1
  28. package/src/cli/commands/connect/custom/write.ts +2 -2
  29. package/src/cli/commands/connect/grant.ts +29 -14
  30. package/src/cli/commands/connect/index.ts +90 -88
  31. package/src/cli/commands/connect/options.ts +83 -0
  32. package/src/cli/commands/connect/registration.ts +50 -0
  33. package/src/cli/commands/connect/requirements.ts +1 -1
  34. package/src/cli/commands/connect/settle.ts +16 -6
  35. package/src/cli/commands/connect/target-note.ts +7 -2
  36. package/src/cli/commands/connect/unknown.ts +1 -1
  37. package/src/cli/commands/connect/variables.ts +3 -2
  38. package/src/cli/commands/connection-list.ts +116 -0
  39. package/src/cli/commands/connection.ts +183 -165
  40. package/src/cli/commands/grant.ts +140 -0
  41. package/src/cli/commands/identity.ts +24 -12
  42. package/src/cli/commands/knowledge/index.ts +49 -89
  43. package/src/cli/commands/knowledge/migrate.ts +79 -13
  44. package/src/cli/commands/knowledge/show.ts +97 -0
  45. package/src/cli/commands/knowledge.ts +2 -1
  46. package/src/cli/commands/mcp/harnesses.ts +30 -8
  47. package/src/cli/commands/mcp/onboarding.ts +86 -0
  48. package/src/cli/commands/mcp/register.ts +16 -2
  49. package/src/cli/commands/mcp.ts +1 -0
  50. package/src/cli/commands/members.ts +288 -0
  51. package/src/cli/commands/operate/attach.ts +3 -3
  52. package/src/cli/commands/operate/audit.ts +11 -7
  53. package/src/cli/commands/operate/auth.ts +28 -11
  54. package/src/cli/commands/operate/findings.ts +2 -1
  55. package/src/cli/commands/operate/inspect.ts +37 -19
  56. package/src/cli/commands/operate/migrate.ts +33 -13
  57. package/src/cli/commands/operate/outputs.ts +3 -3
  58. package/src/cli/commands/operate/pair-certificate.ts +141 -0
  59. package/src/cli/commands/operate/pair.ts +324 -0
  60. package/src/cli/commands/operate/policy.ts +73 -22
  61. package/src/cli/commands/operate/serve.ts +53 -5
  62. package/src/cli/commands/operate/status.ts +18 -10
  63. package/src/cli/commands/operate/tools.ts +2 -2
  64. package/src/cli/commands/operate.ts +2 -0
  65. package/src/cli/commands/owner/assets.ts +2 -2
  66. package/src/cli/commands/owner/entities.ts +2 -2
  67. package/src/cli/commands/owner/memory.ts +2 -2
  68. package/src/cli/commands/owner/shared.ts +13 -2
  69. package/src/cli/commands/owner/skills.ts +28 -8
  70. package/src/cli/commands/owner/tasks.ts +2 -2
  71. package/src/cli/commands/owner/vault.ts +3 -3
  72. package/src/cli/commands/profile/disposition.ts +236 -0
  73. package/src/cli/commands/profile/removal.ts +154 -64
  74. package/src/cli/commands/profile/remove.ts +83 -7
  75. package/src/cli/commands/profile.ts +79 -16
  76. package/src/cli/commands/relabel.ts +112 -0
  77. package/src/cli/commands/secrets.ts +39 -17
  78. package/src/cli/commands/set-workspace.ts +96 -0
  79. package/src/cli/commands/setup.ts +2 -2
  80. package/src/cli/commands/sync.ts +8 -8
  81. package/src/cli/commands/target.ts +9 -7
  82. package/src/cli/commands/update-migration.ts +54 -0
  83. package/src/cli/commands/update.ts +78 -24
  84. package/src/cli/config-edit.ts +99 -143
  85. package/src/cli/config-migrate.ts +82 -64
  86. package/src/cli/config-repair-sweep.ts +119 -0
  87. package/src/cli/config-repair.ts +131 -125
  88. package/src/cli/config-templates.ts +200 -0
  89. package/src/cli/contract3-credentials.ts +294 -0
  90. package/src/cli/contract3-data.ts +262 -0
  91. package/src/cli/contract3-layout.ts +46 -0
  92. package/src/cli/contract3-shape.ts +212 -0
  93. package/src/cli/contract3.ts +399 -0
  94. package/src/cli/contract4-credentials.ts +207 -0
  95. package/src/cli/contract4-data.ts +399 -0
  96. package/src/cli/contract4-rename.ts +73 -0
  97. package/src/cli/contract4-yaml.ts +223 -0
  98. package/src/cli/contract4.ts +342 -0
  99. package/src/cli/endpoint-url.ts +1 -1
  100. package/src/cli/identity.ts +44 -26
  101. package/src/cli/lanes.ts +25 -1
  102. package/src/cli/main.ts +94 -14
  103. package/src/cli/migrate-move.ts +166 -0
  104. package/src/cli/migrate-plan.ts +12 -6
  105. package/src/cli/output.ts +34 -1
  106. package/src/cli/publish.ts +6 -7
  107. package/src/cli/runtime/open.ts +64 -99
  108. package/src/cli/runtime/registry.ts +6 -7
  109. package/src/cli/runtime/select.ts +2 -11
  110. package/src/cli/runtime/stores.ts +58 -0
  111. package/src/cli/runtime/types.ts +106 -0
  112. package/src/cli/runtime/vault.ts +19 -4
  113. package/src/cli/runtime/workspace.ts +60 -0
  114. package/src/cli/runtime.ts +2 -1
  115. package/src/cli/selection-require.ts +44 -13
  116. package/src/cli/selection.ts +127 -145
  117. package/src/cli/usage.ts +40 -20
  118. package/src/cli/workspace-migrate.ts +152 -22
  119. package/src/connectivity/manifest/provider.ts +34 -13
  120. package/src/connectivity/manifest/requirements.ts +1 -1
  121. package/src/connectivity/transports/imap/parser.ts +70 -9
  122. package/src/deployments/adapters/filesystem.ts +18 -3
  123. package/src/deployments/bind.ts +1 -1
  124. package/src/deployments/deploy.ts +38 -29
  125. package/src/deployments/gcp/bucket.ts +58 -11
  126. package/src/deployments/gcp/provision.ts +7 -7
  127. package/src/deployments/knowledge.ts +9 -4
  128. package/src/deployments/prepare.ts +72 -24
  129. package/src/deployments/record.ts +1 -1
  130. package/src/deployments/report.ts +2 -2
  131. package/src/deployments/serving.ts +15 -74
  132. package/src/deployments/target.ts +34 -13
  133. package/src/deployments/upload.ts +60 -27
  134. package/src/dispatch/deps.ts +88 -0
  135. package/src/dispatch/dispatch.ts +21 -62
  136. package/src/policy/index.ts +47 -15
  137. package/src/profile/connections.ts +195 -0
  138. package/src/profile/deployments.ts +86 -8
  139. package/src/profile/index.ts +35 -6
  140. package/src/profile/knowledge.ts +18 -5
  141. package/src/profile/layout.ts +163 -90
  142. package/src/profile/load.ts +133 -64
  143. package/src/profile/pairing.ts +32 -0
  144. package/src/profile/primitives.ts +35 -1
  145. package/src/profile/registry.ts +6 -6
  146. package/src/profile/schema.ts +181 -21
  147. package/src/profile/targets.ts +21 -9
  148. package/src/profile/testing.ts +104 -2
  149. package/src/profile/workspace.ts +124 -33
  150. package/src/providers/assets/provider.ts +6 -6
  151. package/src/providers/custom/index.ts +1 -1
  152. package/src/providers/custom/load.ts +2 -3
  153. package/src/providers/entities/provider.ts +6 -6
  154. package/src/providers/entities/writes.ts +1 -1
  155. package/src/providers/identity/provider.ts +2 -2
  156. package/src/providers/memory/provider.ts +26 -8
  157. package/src/providers/setup/plan.ts +1 -1
  158. package/src/providers/setup/provider.ts +3 -3
  159. package/src/providers/skills/provider.ts +2 -2
  160. package/src/providers/slack/index.ts +2 -2
  161. package/src/providers/tasks/provider.ts +6 -6
  162. package/src/providers/vault/provider.ts +1 -1
  163. package/src/registry/policy-bridge.ts +33 -11
  164. package/src/registry/reconcile.ts +4 -4
  165. package/src/registry/registry.ts +1 -1
  166. package/src/server/authorization.ts +94 -0
  167. package/src/server/edge.ts +14 -1
  168. package/src/server/endpoint.ts +89 -104
  169. package/src/server/generation.ts +10 -1
  170. package/src/server/harness.ts +71 -13
  171. package/src/server/index.ts +31 -0
  172. package/src/server/mcp/build.ts +20 -1
  173. package/src/server/mcp/client-info.ts +54 -0
  174. package/src/server/mcp/guide.ts +120 -0
  175. package/src/server/mcp/instructions.ts +22 -22
  176. package/src/server/mcp/prompts.ts +7 -3
  177. package/src/server/mcp/resources.ts +16 -8
  178. package/src/server/mcp/routing.ts +3 -3
  179. package/src/server/mcp/tools.ts +25 -6
  180. package/src/server/mcp/visibility.ts +74 -7
  181. package/src/server/oauth.ts +29 -109
  182. package/src/server/read/credential.ts +134 -0
  183. package/src/server/read/deployed.ts +56 -0
  184. package/src/server/read/listener.ts +54 -0
  185. package/src/server/read/open.ts +101 -0
  186. package/src/server/read/routes.ts +247 -0
  187. package/src/server/read/state.ts +171 -0
  188. package/src/stores/blobs/conformance.ts +19 -0
  189. package/src/stores/state/index.ts +76 -10
  190. package/src/stores/state/testing.ts +5 -1
@@ -1,6 +1,5 @@
1
- import { listProfiles } from '#profile';
2
1
  import { ConfigDocument } from './config-edit.ts';
3
- import { ok, print, style, warn } from './output.ts';
2
+ import { nextConnectionId } from './identity.ts';
4
3
 
5
4
  /**
6
5
  * Giving a profile a reserved provider it is missing, without undoing a choice.
@@ -18,16 +17,16 @@ import { ok, print, style, warn } from './output.ts';
18
17
  * than several that look alike.
19
18
  */
20
19
 
21
- /** The reserved provider ids that hold no account, and the label each row carries. */
20
+ /** Lanes' own provider ids, and the label each row carries. */
22
21
  const RESERVED_SURFACES = {
23
- memory: 'Memory',
24
- tasks: 'Tasks',
25
- assets: 'Assets',
26
- skills: 'Skills',
27
- vault: 'Vault',
28
- setup: 'Setup',
29
- identity: 'Identity',
30
- entities: 'Entities',
22
+ lanes_memory: 'Memory',
23
+ lanes_tasks: 'Tasks',
24
+ lanes_assets: 'Assets',
25
+ lanes_skills: 'Skills',
26
+ lanes_vault: 'Vault',
27
+ lanes_setup: 'Setup',
28
+ lanes_identity: 'Identity',
29
+ lanes_entities: 'Entities',
31
30
  } as const;
32
31
 
33
32
  type ReservedSurface = keyof typeof RESERVED_SURFACES;
@@ -55,13 +54,13 @@ type ReservedSurface = keyof typeof RESERVED_SURFACES;
55
54
  * template writes and a diff between the two reads as a diff.
56
55
  */
57
56
  export const DEFAULT_SURFACES: readonly ReservedSurface[] = [
58
- 'memory',
59
- 'tasks',
60
- 'assets',
61
- 'skills',
62
- 'vault',
63
- 'setup',
64
- 'entities',
57
+ 'lanes_memory',
58
+ 'lanes_tasks',
59
+ 'lanes_assets',
60
+ 'lanes_skills',
61
+ 'lanes_vault',
62
+ 'lanes_setup',
63
+ 'lanes_entities',
65
64
  ];
66
65
 
67
66
  /**
@@ -100,55 +99,131 @@ export interface SurfaceRepair {
100
99
  * (ADR-023) so it could not write this even if the code let it.
101
100
  */
102
101
  export function ensureReservedConnection(
103
- document: ConfigDocument,
102
+ connections: ConfigDocument,
103
+ profile: ConfigDocument,
104
104
  provider: ReservedSurface,
105
+ options: { grants?: boolean } = {},
105
106
  ): SurfaceRepair {
107
+ // Whether the profile half is written at all. `connect` without `--profile`
108
+ // repairs the workspace's connection rows and touches no profile, because it
109
+ // was not told which one to touch (ADR-057).
110
+ const grants = options.grants ?? true;
106
111
  // Raw YAML, so nothing here has been through a schema: this runs over sibling
107
112
  // profiles that were never validated, and every field is whatever was typed.
108
- const config = document.toJSON() as {
109
- connections?: unknown;
110
- policy?: { allow?: unknown; deny?: unknown };
111
- } | null;
113
+ const workspace = connections.toJSON() as { connections?: unknown } | null;
114
+ const config = profile.toJSON() as { grants?: unknown } | null;
112
115
 
113
116
  const rule = `${provider}.*`;
114
117
  const covers = (pattern: string): boolean => pattern === '*' || pattern === rule;
115
118
 
119
+ const rows = Array.isArray(workspace?.connections) ? workspace.connections : [];
120
+ const held_grants = Array.isArray(config?.grants) ? config.grants : [];
121
+
122
+ // **This profile's own grant comes first.** Any instance of the provider will
123
+ // do, and the *first* one is taken rather than `main` specifically: an
124
+ // operator who renamed theirs should not get a second one bolted on beside
125
+ // it. That reasoning was right and the lookup implementing it was not — it
126
+ // read the workspace's first row, which under contract 2 was this profile's
127
+ // because a profile carried its own connections, and under contract 3 is
128
+ // whichever profile sorts first.
129
+ //
130
+ // So the repair asked whether `personal` granted `memory.main` — demo's — saw
131
+ // that it did not, and set about adding it. For `vault` and `skills` the
132
+ // schema refuses a second grant and the save failed loudly, which is how this
133
+ // was found. For `memory`, `tasks`, `assets`, `setup` and `entities` nothing
134
+ // refuses it, and the profile would quietly have been granted another
135
+ // profile's notes and another profile's task list — the outcome ADR-059
136
+ // exists to prevent, arriving from the repair rather than the migration.
137
+ //
138
+ // **Every** instance, not the first one matched. A profile may grant two —
139
+ // `memory.team` and `memory.personal` — and reading only the first let a deny
140
+ // on the other be stepped around: the repair widened the undenied row and
141
+ // reported the surface granted, which is the one thing the paragraph below
142
+ // says it must never do.
143
+ //
144
+ // `startsWith` rather than slicing at the dot, because these rows are raw
145
+ // unvalidated YAML: `indexOf` answers -1 for a value with no dot at all, and
146
+ // `'vaults'.slice(0, -1)` is `'vault'` — so a typo'd `connection: vaults`
147
+ // matched the vault surface and was widened while the real one stayed
148
+ // unreachable.
149
+ const mine = held_grants.filter((row) => {
150
+ const connection = (row as { connection?: unknown } | null)?.connection;
151
+ return typeof connection === 'string' && connection.startsWith(`${provider}.`);
152
+ }) as { connection?: string; allow?: unknown; deny?: unknown }[];
153
+
116
154
  // Denied on purpose, and a deny beats an allow — so writing the rule would
117
155
  // widen nothing while announcing that an agent can now read the surface,
118
- // which would be false. For `setup`, deleting the two lines no longer removes
119
- // it either, because the next `connect` or `deploy` puts them back; a deny is
120
- // the way it stays off, so it is the one thing this must not undo. The same
121
- // holds for `identity`, where the next `identity add` is what would put them
122
- // back.
156
+ // which would be false. Deleting the row no longer removes the surface
157
+ // either, because the next `connect` or `deploy` puts it back; a deny is the
158
+ // way it stays off, so it is the one thing this must not undo.
123
159
  //
124
160
  // Only a rule covering the whole surface counts. `deny: [setup.provider]` is
125
161
  // an operator narrowing it, not switching it off, and that narrowing survives
126
162
  // the repair untouched — which is the point of denying one capability.
127
- if (patternsIn(config?.policy?.deny).some(covers)) return { changes: [], granted: [] };
163
+ if (mine.some((row) => patternsIn(row.deny).some(covers))) return { changes: [], granted: [] };
164
+
165
+ // The instance already carrying the rule, if any. Not an early return: the
166
+ // workspace row can be missing while the grant is present — a profile written
167
+ // before the surface existed, repaired once against a `connections.yaml` that
168
+ // has since been hand-edited — and repairing one half only is what this
169
+ // function exists to prevent.
170
+ const allowed = mine.find((row) => patternsIn(row.allow).some(covers));
171
+
172
+ // Otherwise the first row that exists and grants nothing is the one to widen.
173
+ const owned = allowed ?? mine[0];
174
+
175
+ // Only when this profile grants no instance of the provider at all is the
176
+ // workspace consulted, which is the case this was written for: a surface that
177
+ // did not exist when the profile was written.
178
+ const declared = rows.find(
179
+ (row) => (row as { provider?: unknown } | null)?.provider === provider,
180
+ ) as { id?: unknown } | undefined;
181
+
182
+ // The id an existing row already has, else the next free one. Opaque and
183
+ // allocated across the whole workspace, so `lan3` names exactly one row
184
+ // however many surfaces a repair adds in one pass.
185
+ const taken = rows.flatMap((row) => {
186
+ const id = (row as { id?: unknown } | null)?.id;
187
+ return typeof id === 'string' ? [id] : [];
188
+ });
189
+ const id = typeof declared?.id === 'string' ? declared.id : nextConnectionId(taken, true);
190
+
191
+ const key = owned?.connection ?? `${provider}.${id}`;
192
+
193
+ // Whether the *workspace* needs a row, which is a separate question from
194
+ // which one this profile grants: a profile cannot grant a connection that is
195
+ // not declared, so `owned` implies `declared`.
196
+ const existing = declared;
197
+ const held = owned;
128
198
 
129
199
  const changes: string[] = [];
130
200
  const granted: string[] = [];
131
201
 
132
- const connections = Array.isArray(config?.connections) ? config.connections : [];
133
- const declared = (row: unknown): boolean =>
134
- (row as { provider?: unknown } | null)?.provider === provider;
135
-
136
- if (!connections.some(declared)) {
137
- // Inline, and `main` for the id, so a repaired profile is spelled exactly
138
- // like `newProfileTemplate` writes a fresh one. Two spellings of one row is
139
- // how a template and its repair drift apart.
140
- document.addTo(
202
+ if (existing === undefined) {
203
+ // Inline, and the allocated id, so a repaired workspace is spelled exactly
204
+ // like `newConnectionsTemplate` writes a fresh one which is what
205
+ // `config-edit.test.ts` asserts by checking a fresh profile needs no repair.
206
+ // Two spellings of one row is how a template and its repair drift apart.
207
+ connections.addTo(
141
208
  ['connections'],
142
- { id: 'main', provider, account: RESERVED_SURFACES[provider] },
209
+ { id, provider, account: RESERVED_SURFACES[provider] },
143
210
  { inline: true },
144
211
  );
145
- changes.push(`connections += ${provider}.main`);
212
+ changes.push(`connections.yaml += ${key}`);
146
213
  }
147
214
 
148
- // `*` already covers it. Re-stating the rule under a blanket allow would be
149
- // noise in the file and a diff the operator did not ask for.
150
- if (!patternsIn(config?.policy?.allow).some(covers)) {
151
- document.addTo(['policy', 'allow'], rule, { inline: true });
215
+ if (!grants) return { changes, granted };
216
+
217
+ if (held === undefined) {
218
+ profile.addTo(['grants'], { connection: key, allow: [rule], deny: [] }, { inline: true });
219
+ changes.push(`grants += ${key}`);
220
+ granted.push(rule);
221
+ } else if (allowed === undefined) {
222
+ // A row that exists and grants nothing is a surface that is present and
223
+ // silent. Widening it back is the repair; the deny check above is what stops
224
+ // this undoing a deliberate narrowing.
225
+ const at = held_grants.indexOf(held as never);
226
+ profile.addTo(['grants', at, 'allow'], rule, { inline: true });
152
227
  granted.push(rule);
153
228
  }
154
229
 
@@ -156,20 +231,13 @@ export function ensureReservedConnection(
156
231
  }
157
232
 
158
233
  /** Whether a repair did anything, without a caller adding up two lists. */
159
- /** `memory, tasks, assets, skills, vault, setup and entities`, in repair order. */
160
- function listSurfaces(): string {
161
- const names = [...DEFAULT_SURFACES];
162
- const last = names.pop();
163
- return names.length === 0 ? String(last) : `${names.join(', ')} and ${last}`;
164
- }
165
-
166
234
  export function repaired(repair: SurfaceRepair): boolean {
167
235
  return repair.changes.length > 0 || repair.granted.length > 0;
168
236
  }
169
237
 
170
238
  /** The repair as display lines, in the order the two halves are applied. */
171
239
  export function repairLines(repair: SurfaceRepair): string[] {
172
- return [...repair.changes, ...repair.granted.map((rule) => `policy.allow += ${rule}`)];
240
+ return [...repair.changes, ...repair.granted.map((rule) => `grants[].allow += ${rule}`)];
173
241
  }
174
242
 
175
243
  /**
@@ -219,12 +287,16 @@ function patternsIn(rules: unknown, now = Date.now()): string[] {
219
287
  * Each surface is still decided independently, so a profile that denied exactly
220
288
  * one of them keeps that decision while the rest are repaired.
221
289
  */
222
- export function ensureOwnerLayer(document: ConfigDocument): SurfaceRepair {
290
+ export function ensureOwnerLayer(
291
+ connections: ConfigDocument,
292
+ profile: ConfigDocument,
293
+ options: { grants?: boolean } = {},
294
+ ): SurfaceRepair {
223
295
  const changes: string[] = [];
224
296
  const granted: string[] = [];
225
297
 
226
298
  for (const provider of DEFAULT_SURFACES) {
227
- const repair = ensureReservedConnection(document, provider);
299
+ const repair = ensureReservedConnection(connections, profile, provider, options);
228
300
  changes.push(...repair.changes);
229
301
  granted.push(...repair.granted);
230
302
  }
@@ -241,76 +313,10 @@ export function ensureOwnerLayer(document: ConfigDocument): SurfaceRepair {
241
313
  * paragraph of the instructions budget to say so. `identity add` is the only
242
314
  * caller, so the grant arrives exactly when there is something behind it.
243
315
  */
244
- export function ensureIdentityConnection(document: ConfigDocument): SurfaceRepair {
245
- return ensureReservedConnection(document, 'identity');
316
+ export function ensureIdentityConnection(
317
+ connections: ConfigDocument,
318
+ profile: ConfigDocument,
319
+ ): SurfaceRepair {
320
+ return ensureReservedConnection(connections, profile, 'lanes_identity');
246
321
  }
247
322
 
248
- /**
249
- * Apply that repair across a workspace, saving and reporting what changed.
250
- *
251
- * Here rather than in `#deployments`, where it was, because `start` needs it as
252
- * much as `deploy` does — more, in fact: `start` is the one command an existing
253
- * install runs without being asked to, so it is the path by which a profile
254
- * written before ADR-050 gets the layer at all. Two copies of a function that
255
- * widens a policy is not a thing to have.
256
- *
257
- * **The caller scopes it**, and for `deploy` that is exactly the set being
258
- * uploaded: a profile it sends is a profile the endpoint will serve, so
259
- * repairing a narrower set would leave a served profile without the surfaces.
260
- * Note what a `--profile` flag does not mean — it is the flag alone, so a
261
- * profile resolved from the environment leaves it undefined and that reads as
262
- * the whole workspace.
263
- *
264
- * *Which files are profiles* comes from `listProfiles`, never from an allowlist
265
- * of what is safe to copy: that would happily hand over a committed
266
- * `personal.example.yaml` or a nested `profiles/archive/old.yaml`, and this
267
- * opens and validates what it is given — which once turned a template into a
268
- * `ConfigError` aborting a deploy after provisioning had made cloud resources.
269
- *
270
- * A profile that cannot be read is warned about rather than fatal: the repair is
271
- * a courtesy on the way past, and the caller's real work should still happen.
272
- * Not silent, though — nothing else widens a policy without being asked.
273
- *
274
- * CLI-side by construction, like everything else in this file: a deployed
275
- * revision holds `objectViewer` on `profiles/` (ADR-023) and could not write
276
- * this even if the code let it.
277
- */
278
- export async function repairOwnerLayer(
279
- workspaceRoot: string,
280
- profiles: readonly string[] | undefined,
281
- options: { report?: (line: string) => void } = {},
282
- ): Promise<void> {
283
- // stdout by default, because every caller but one is printing a report a
284
- // person reads. `update --json` passes `progress` instead: what it produces is
285
- // a document, and a line of prose in front of it corrupts whatever is parsing.
286
- // Routed rather than silenced — nothing else here widens a policy without
287
- // saying so, and this must not be the exception.
288
- const say = options.report ?? print;
289
- const wanted = profiles === undefined ? undefined : new Set(profiles);
290
-
291
- for (const name of await listProfiles(workspaceRoot)) {
292
- if (wanted !== undefined && !wanted.has(name)) continue;
293
-
294
- try {
295
- const document = await ConfigDocument.open(workspaceRoot, name);
296
- const repair = ensureOwnerLayer(document);
297
- if (!repaired(repair)) continue;
298
-
299
- await document.save();
300
-
301
- say(ok(`gave ${style.bold(name)} its own owner layer`));
302
- for (const change of repairLines(repair)) say(` ${style.dim(change)}`);
303
- // Built from `DEFAULT_SURFACES` rather than typed out. The typed-out
304
- // version still named six after a seventh had been added, so a person
305
- // watching a deploy was told entities had arrived on the line above and
306
- // that the layer was six things on the line below.
307
- say(` ${style.dim(`${listSurfaces()} — your own material, no account behind any of them`)}`);
308
- } catch (error) {
309
- say(
310
- warn(
311
- `could not give ${name} its owner layer: ${error instanceof Error ? error.message.split('\n')[0] : String(error)}`,
312
- ),
313
- );
314
- }
315
- }
316
- }
@@ -0,0 +1,200 @@
1
+ import { SUPPORTED_CONTRACT } from '#profile';
2
+
3
+ /**
4
+ * The files a fresh workspace and a fresh profile are written from.
5
+ *
6
+ * Split out of `config-edit.ts` because they are prose rather than machinery:
7
+ * that file knows how to edit YAML without disturbing what an operator wrote,
8
+ * and these are the comments an operator reads. Keeping both in one file put it
9
+ * over the size budget, and the seam was already there.
10
+ *
11
+ * **The template and the repair must write a row in one spelling.** Two
12
+ * spellings of one row is how they drift apart, and `config-edit.test.ts`
13
+ * asserts that a fresh profile and workspace need no repair, which is the check
14
+ * that catches it (ADR-050).
15
+ */
16
+
17
+ export function newProfileTemplate(profile: string, port: number, subject?: string): string {
18
+ return `# Lanes Link profile: ${profile}
19
+ #
20
+ # A profile is a *selection*: which of the workspace's accounts this agent may
21
+ # reach, what it may do with each, and who may use it. The accounts themselves
22
+ # live in connections.yaml beside this file, because authorising an account and
23
+ # deciding what may be done with it are two different acts (ADR-057).
24
+ #
25
+ # This file never contains a credential value — only "_ref" pointers into the
26
+ # credential store, which is the workspace's and is encrypted at rest.
27
+ #
28
+ # Edit it by hand or through the CLI; both are supported, and CLI edits preserve
29
+ # your comments and ordering.
30
+ contract: 4
31
+
32
+ instance:
33
+ profile: ${profile}
34
+ port: ${port}
35
+ host: 127.0.0.1
36
+
37
+ # What this profile is for, in your own words. Members see it, and so does
38
+ # setup_overview — "reads my mail, keeps the calendar, never sends" is what
39
+ # somebody needs to know before accepting it.
40
+ # description:
41
+
42
+ # This file says nothing about where it runs, and that is the point.
43
+ #
44
+ # A profile lives in exactly one workspace, and that workspace declares its own
45
+ # adapters, once, in lanes-link.yaml beside the profiles/ directory (ADR-052).
46
+ # Moving this profile somewhere else is copying the file there.
47
+ #
48
+ # lanes link status --profile ${profile} --workspace <name>
49
+ #
50
+ # The bearer token below is for CI. People sign in instead: a client that asks
51
+ # for authorization is sent to the Lanes login, and comes back as somebody
52
+ # (ADR-062). "lanes link token show" is for a runner with no browser.
53
+ auth:
54
+ mode: bearer
55
+ token_ref: profile/token
56
+ authorization:
57
+ mode: self
58
+
59
+ limits:
60
+ requests_per_minute: 120 # per profile
61
+ upstream_calls_per_minute: 60 # per connection, protects vendor quota
62
+
63
+ # One row per connection this profile may reach, and what it may do with each.
64
+ #
65
+ # A row is the grant. There is no separate list of accounts and list of rules
66
+ # that have to agree — naming a connection here is what makes it reachable, and
67
+ # the allow list is what makes any of its capabilities callable. An account the
68
+ # workspace holds and this file does not name is simply absent: not denied, not
69
+ # advertised, not there.
70
+ #
71
+ # Rules name capabilities of that row's own provider. "gmail.*" covers
72
+ # everything Gmail offers *for that one account*, which is what lets a second
73
+ # row over a second mailbox allow something different (ADR-058).
74
+ #
75
+ # The seven below hold no account, and that is why they are here already: they
76
+ # reach your own material rather than anybody's API, so there was never anything
77
+ # for a connect step to authorise (ADR-050). What each one is:
78
+ #
79
+ # memory what you want remembered between sessions
80
+ # tasks what you have to do, each with a status
81
+ # assets files you want kept, by name
82
+ # skills procedures you have written, handed to an agent as instructions
83
+ # vault passwords and API keys, released one at a time
84
+ # setup what is connected here, and what connecting more would take
85
+ # entities the people, companies and projects you deal with, and how to
86
+ # reach each of them — so an agent looks an address up rather
87
+ # than recalling one
88
+ #
89
+ # To switch one off, add it to that row's deny — deleting the row no longer
90
+ # works, because the next connect or deploy puts it back. The three narrowings
91
+ # worth knowing:
92
+ #
93
+ # deny: [lanes_memory.write] remember nothing new
94
+ # deny: [lanes_skills.manage.*] invoke procedures, do not write them
95
+ # deny: [lanes_vault.put, lanes_vault.remove]
96
+ grants:
97
+ - { connection: lanes_memory.lan1, allow: [lanes_memory.*], deny: [] }
98
+ - { connection: lanes_tasks.lan2, allow: [lanes_tasks.*], deny: [] }
99
+ - { connection: lanes_assets.lan3, allow: [lanes_assets.*], deny: [] }
100
+ - { connection: lanes_skills.lan4, allow: [lanes_skills.*], deny: [] }
101
+ - { connection: lanes_vault.lan5, allow: [lanes_vault.*], deny: [] }
102
+ - { connection: lanes_setup.lan6, allow: [lanes_setup.*], deny: [] }
103
+ - { connection: lanes_entities.lan7, allow: [lanes_entities.*], deny: [] }
104
+
105
+ # Who may consume this profile (ADR-060).
106
+ #
107
+ # Empty is nobody, not everybody — default deny on the identity axis. A caller
108
+ # proves who they are by signing in to Lanes, and reaches this profile only if
109
+ # their subject is listed here.
110
+ #
111
+ # "owner" may edit this list. Both roles reach exactly what the grants above
112
+ # allow: a role that changed what an agent could call would be a second policy
113
+ # system beside grants, answering a question the first one already answers.
114
+ members:${
115
+ subject
116
+ ? `
117
+ - { subject: ${subject}, role: owner }`
118
+ : ' []'
119
+ }
120
+ `;
121
+ }
122
+
123
+ export function newWorkspaceTemplate(): string {
124
+ return `# Lanes Link workspace
125
+ #
126
+ # A workspace holds the accounts you have authorised (connections.yaml) and the
127
+ # profiles that select among them (profiles/). One endpoint serves all of them:
128
+ # every call names the profile it means, with --profile.
129
+ #
130
+ # "workspaces:" below says where this one's bytes go, once, for every profile in
131
+ # it — a profile says nothing about where it runs, so there is one copy of it and
132
+ # nothing to keep in step (ADR-052).
133
+ #
134
+ # A workspace somewhere else is a pointer, and "deploy" writes one:
135
+ #
136
+ # workspaces:
137
+ # cloud:
138
+ # at: gs://your-bucket
139
+ # lanes_workspace: <id> # whose members may be delegated to
140
+ #
141
+ # The workspace at that address declares its own adapters, and is the only thing
142
+ # that does. Reading it is a network call, which is why "--workspace cloud" needs
143
+ # that bucket reachable.
144
+ #
145
+ # default_workspace is used when --workspace is absent, and every command that
146
+ # uses it prints which one it got. Commands that publish or destroy — deploy,
147
+ # sync, secrets push, profile remove, disconnect, token rotate — refuse it and
148
+ # make you type the name (ADR-061).
149
+ contract: 4
150
+ default_workspace: local
151
+ workspaces:
152
+ local:
153
+ credentials: { adapter: file }
154
+ storage: { adapter: filesystem }
155
+ `;
156
+ }
157
+
158
+ /**
159
+ * `connections.yaml` for a workspace that has just been created.
160
+ *
161
+ * The owner layer arrives here rather than in the profile, because these are
162
+ * connections now (ADR-059) and a second profile should select the same stores
163
+ * rather than get its own empty ones. `ensureOwnerLayer` keeps this in one
164
+ * spelling with the repair, which `config-edit.test.ts` asserts by checking that
165
+ * a fresh workspace needs no repair.
166
+ */
167
+ export function newConnectionsTemplate(): string {
168
+ return `# Lanes Link connections
169
+ #
170
+ # Every account authorised in this workspace, in one place. A profile names the
171
+ # ones it may reach in its own "grants:" block — connecting an account and
172
+ # deciding what may be done with it are two acts, and only the second belongs to
173
+ # a profile (ADR-057).
174
+ #
175
+ # "account" is the identity the provider reports — an address, a workspace — so
176
+ # this list says whose data is reachable without having to look anything up.
177
+ # "label" is your own word for the same row, and only ever displayed.
178
+ #
179
+ # The "lanes_" rows are Lanes' own surfaces and hold no account: they reach your
180
+ # own material rather than anybody's API, so there was never anything for a
181
+ # connect step to authorise (ADR-050). Every profile grants the same ones and
182
+ # still keeps its own bytes — what you write through one profile is absent in
183
+ # another (ADR-066). A second instance is for holding two of something in one
184
+ # profile: "lanes link connect lanes_memory --id lan9".
185
+ contract: 4
186
+
187
+ connections:
188
+ - { id: lan1, provider: lanes_memory, account: Memory }
189
+ - { id: lan2, provider: lanes_tasks, account: Tasks }
190
+ - { id: lan3, provider: lanes_assets, account: Assets }
191
+ - { id: lan4, provider: lanes_skills, account: Skills }
192
+ - { id: lan5, provider: lanes_vault, account: Vault }
193
+ - { id: lan6, provider: lanes_setup, account: Setup }
194
+ - { id: lan7, provider: lanes_entities, account: Entities }
195
+
196
+ # App registrations, shared by every connection of that vendor.
197
+ oauth_apps: {}
198
+ `;
199
+ }
200
+