@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.
- package/README.md +20 -10
- package/instructions/agents/lanes-link-scout.md +2 -2
- package/instructions/skills/lanes-link/SKILL.md +148 -73
- package/package.json +2 -1
- package/src/audit/index.ts +8 -1
- package/src/auth/index.ts +58 -2
- package/src/auth/lanes/assertion.ts +256 -0
- package/src/auth/lanes/callback.ts +135 -0
- package/src/auth/lanes/federation.ts +50 -0
- package/src/auth/lanes/login.ts +294 -0
- package/src/auth/lanes/members.ts +103 -0
- package/src/auth/lanes/session.ts +97 -0
- package/src/auth/oauth/grant.ts +183 -0
- package/src/auth/oauth/result.ts +27 -0
- package/src/auth/oauth/server.ts +176 -203
- package/src/auth/oauth/store.ts +85 -4
- package/src/auth/remote.ts +32 -9
- package/src/cli/accepts.ts +109 -0
- package/src/cli/argv.ts +57 -3
- package/src/cli/audit-change.ts +140 -0
- package/src/cli/callback-page.ts +36 -115
- package/src/cli/commands/auth-dispatch.ts +48 -0
- package/src/cli/commands/auth.ts +229 -0
- package/src/cli/commands/connect/accounts.ts +4 -4
- package/src/cli/commands/connect/authorise.ts +4 -4
- package/src/cli/commands/connect/bind-credential.ts +2 -1
- package/src/cli/commands/connect/custom/index.ts +1 -1
- package/src/cli/commands/connect/custom/write.ts +2 -2
- package/src/cli/commands/connect/grant.ts +29 -14
- package/src/cli/commands/connect/index.ts +90 -88
- package/src/cli/commands/connect/options.ts +83 -0
- package/src/cli/commands/connect/registration.ts +50 -0
- package/src/cli/commands/connect/requirements.ts +1 -1
- package/src/cli/commands/connect/settle.ts +16 -6
- package/src/cli/commands/connect/target-note.ts +7 -2
- package/src/cli/commands/connect/unknown.ts +1 -1
- package/src/cli/commands/connect/variables.ts +3 -2
- package/src/cli/commands/connection-list.ts +116 -0
- package/src/cli/commands/connection.ts +183 -165
- package/src/cli/commands/grant.ts +140 -0
- package/src/cli/commands/identity.ts +24 -12
- package/src/cli/commands/knowledge/index.ts +49 -89
- package/src/cli/commands/knowledge/migrate.ts +79 -13
- package/src/cli/commands/knowledge/show.ts +97 -0
- package/src/cli/commands/knowledge.ts +2 -1
- package/src/cli/commands/mcp/harnesses.ts +30 -8
- package/src/cli/commands/mcp/onboarding.ts +86 -0
- package/src/cli/commands/mcp/register.ts +16 -2
- package/src/cli/commands/mcp.ts +1 -0
- package/src/cli/commands/members.ts +288 -0
- package/src/cli/commands/operate/attach.ts +3 -3
- package/src/cli/commands/operate/audit.ts +11 -7
- package/src/cli/commands/operate/auth.ts +28 -11
- package/src/cli/commands/operate/findings.ts +2 -1
- package/src/cli/commands/operate/inspect.ts +37 -19
- package/src/cli/commands/operate/migrate.ts +33 -13
- package/src/cli/commands/operate/outputs.ts +3 -3
- package/src/cli/commands/operate/pair-certificate.ts +141 -0
- package/src/cli/commands/operate/pair.ts +324 -0
- package/src/cli/commands/operate/policy.ts +73 -22
- package/src/cli/commands/operate/serve.ts +53 -5
- package/src/cli/commands/operate/status.ts +18 -10
- package/src/cli/commands/operate/tools.ts +2 -2
- package/src/cli/commands/operate.ts +2 -0
- package/src/cli/commands/owner/assets.ts +2 -2
- package/src/cli/commands/owner/entities.ts +2 -2
- package/src/cli/commands/owner/memory.ts +2 -2
- package/src/cli/commands/owner/shared.ts +13 -2
- package/src/cli/commands/owner/skills.ts +28 -8
- package/src/cli/commands/owner/tasks.ts +2 -2
- package/src/cli/commands/owner/vault.ts +3 -3
- package/src/cli/commands/profile/disposition.ts +236 -0
- package/src/cli/commands/profile/removal.ts +154 -64
- package/src/cli/commands/profile/remove.ts +83 -7
- package/src/cli/commands/profile.ts +79 -16
- package/src/cli/commands/relabel.ts +112 -0
- package/src/cli/commands/secrets.ts +39 -17
- package/src/cli/commands/set-workspace.ts +96 -0
- package/src/cli/commands/setup.ts +2 -2
- package/src/cli/commands/sync.ts +8 -8
- package/src/cli/commands/target.ts +9 -7
- package/src/cli/commands/update-migration.ts +54 -0
- package/src/cli/commands/update.ts +78 -24
- package/src/cli/config-edit.ts +99 -143
- package/src/cli/config-migrate.ts +82 -64
- package/src/cli/config-repair-sweep.ts +119 -0
- package/src/cli/config-repair.ts +131 -125
- package/src/cli/config-templates.ts +200 -0
- package/src/cli/contract3-credentials.ts +294 -0
- package/src/cli/contract3-data.ts +262 -0
- package/src/cli/contract3-layout.ts +46 -0
- package/src/cli/contract3-shape.ts +212 -0
- package/src/cli/contract3.ts +399 -0
- package/src/cli/contract4-credentials.ts +207 -0
- package/src/cli/contract4-data.ts +399 -0
- package/src/cli/contract4-rename.ts +73 -0
- package/src/cli/contract4-yaml.ts +223 -0
- package/src/cli/contract4.ts +342 -0
- package/src/cli/endpoint-url.ts +1 -1
- package/src/cli/identity.ts +44 -26
- package/src/cli/lanes.ts +25 -1
- package/src/cli/main.ts +94 -14
- package/src/cli/migrate-move.ts +166 -0
- package/src/cli/migrate-plan.ts +12 -6
- package/src/cli/output.ts +34 -1
- package/src/cli/publish.ts +6 -7
- package/src/cli/runtime/open.ts +64 -99
- package/src/cli/runtime/registry.ts +6 -7
- package/src/cli/runtime/select.ts +2 -11
- package/src/cli/runtime/stores.ts +58 -0
- package/src/cli/runtime/types.ts +106 -0
- package/src/cli/runtime/vault.ts +19 -4
- package/src/cli/runtime/workspace.ts +60 -0
- package/src/cli/runtime.ts +2 -1
- package/src/cli/selection-require.ts +44 -13
- package/src/cli/selection.ts +127 -145
- package/src/cli/usage.ts +40 -20
- package/src/cli/workspace-migrate.ts +152 -22
- package/src/connectivity/manifest/provider.ts +34 -13
- package/src/connectivity/manifest/requirements.ts +1 -1
- package/src/connectivity/transports/imap/parser.ts +70 -9
- package/src/deployments/adapters/filesystem.ts +18 -3
- package/src/deployments/bind.ts +1 -1
- package/src/deployments/deploy.ts +38 -29
- package/src/deployments/gcp/bucket.ts +58 -11
- package/src/deployments/gcp/provision.ts +7 -7
- package/src/deployments/knowledge.ts +9 -4
- package/src/deployments/prepare.ts +72 -24
- package/src/deployments/record.ts +1 -1
- package/src/deployments/report.ts +2 -2
- package/src/deployments/serving.ts +15 -74
- package/src/deployments/target.ts +34 -13
- package/src/deployments/upload.ts +60 -27
- package/src/dispatch/deps.ts +88 -0
- package/src/dispatch/dispatch.ts +21 -62
- package/src/policy/index.ts +47 -15
- package/src/profile/connections.ts +195 -0
- package/src/profile/deployments.ts +86 -8
- package/src/profile/index.ts +35 -6
- package/src/profile/knowledge.ts +18 -5
- package/src/profile/layout.ts +163 -90
- package/src/profile/load.ts +133 -64
- package/src/profile/pairing.ts +32 -0
- package/src/profile/primitives.ts +35 -1
- package/src/profile/registry.ts +6 -6
- package/src/profile/schema.ts +181 -21
- package/src/profile/targets.ts +21 -9
- package/src/profile/testing.ts +104 -2
- package/src/profile/workspace.ts +124 -33
- package/src/providers/assets/provider.ts +6 -6
- package/src/providers/custom/index.ts +1 -1
- package/src/providers/custom/load.ts +2 -3
- package/src/providers/entities/provider.ts +6 -6
- package/src/providers/entities/writes.ts +1 -1
- package/src/providers/identity/provider.ts +2 -2
- package/src/providers/memory/provider.ts +26 -8
- package/src/providers/setup/plan.ts +1 -1
- package/src/providers/setup/provider.ts +3 -3
- package/src/providers/skills/provider.ts +2 -2
- package/src/providers/slack/index.ts +2 -2
- package/src/providers/tasks/provider.ts +6 -6
- package/src/providers/vault/provider.ts +1 -1
- package/src/registry/policy-bridge.ts +33 -11
- package/src/registry/reconcile.ts +4 -4
- package/src/registry/registry.ts +1 -1
- package/src/server/authorization.ts +94 -0
- package/src/server/edge.ts +14 -1
- package/src/server/endpoint.ts +89 -104
- package/src/server/generation.ts +10 -1
- package/src/server/harness.ts +71 -13
- package/src/server/index.ts +31 -0
- package/src/server/mcp/build.ts +20 -1
- package/src/server/mcp/client-info.ts +54 -0
- package/src/server/mcp/guide.ts +120 -0
- package/src/server/mcp/instructions.ts +22 -22
- package/src/server/mcp/prompts.ts +7 -3
- package/src/server/mcp/resources.ts +16 -8
- package/src/server/mcp/routing.ts +3 -3
- package/src/server/mcp/tools.ts +25 -6
- package/src/server/mcp/visibility.ts +74 -7
- package/src/server/oauth.ts +29 -109
- package/src/server/read/credential.ts +134 -0
- package/src/server/read/deployed.ts +56 -0
- package/src/server/read/listener.ts +54 -0
- package/src/server/read/open.ts +101 -0
- package/src/server/read/routes.ts +247 -0
- package/src/server/read/state.ts +171 -0
- package/src/stores/blobs/conformance.ts +19 -0
- package/src/stores/state/index.ts +76 -10
- package/src/stores/state/testing.ts +5 -1
package/src/cli/config-repair.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { listProfiles } from '#profile';
|
|
2
1
|
import { ConfigDocument } from './config-edit.ts';
|
|
3
|
-
import {
|
|
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
|
-
/**
|
|
20
|
+
/** Lanes' own provider ids, and the label each row carries. */
|
|
22
21
|
const RESERVED_SURFACES = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
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
|
-
|
|
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
|
|
109
|
-
|
|
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.
|
|
119
|
-
//
|
|
120
|
-
//
|
|
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(
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
209
|
+
{ id, provider, account: RESERVED_SURFACES[provider] },
|
|
143
210
|
{ inline: true },
|
|
144
211
|
);
|
|
145
|
-
changes.push(`connections += ${
|
|
212
|
+
changes.push(`connections.yaml += ${key}`);
|
|
146
213
|
}
|
|
147
214
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
|
|
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) => `
|
|
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(
|
|
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(
|
|
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(
|
|
245
|
-
|
|
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
|
+
|