@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
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { clearSession, isFresh, readSession } from '#auth/lanes/session.ts';
|
|
2
|
+
import { DEFAULT_API_URL, currentIdToken, login } from '#auth/lanes/login.ts';
|
|
3
|
+
import { completionPage } from '../callback-page.ts';
|
|
4
|
+
import { print, ok, style, warn } from '../output.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `lanes auth` — who this machine is signed in as.
|
|
8
|
+
*
|
|
9
|
+
* A separate area from `lanes link auth`, which is a per-connection credential
|
|
10
|
+
* diagnostic and answers a different question entirely. Neither is renamed and
|
|
11
|
+
* neither gains an alias: one spelling per command, and the help text on each
|
|
12
|
+
* says which is which.
|
|
13
|
+
*
|
|
14
|
+
* Sign-in is required to consume a profile, local endpoints included (ADR-060).
|
|
15
|
+
* That is a real dependency on lanes.sh for a self-hostable tool, and the shape
|
|
16
|
+
* of it is worth stating plainly: **the network is needed to sign in and to
|
|
17
|
+
* refresh, not per request.** A machine offline for a day keeps serving, and
|
|
18
|
+
* `status` below is the command that says how long that has left to run — a
|
|
19
|
+
* session that lapses silently would present as an endpoint that worked
|
|
20
|
+
* yesterday and does not today, which is the worst failure to debug from the
|
|
21
|
+
* outside.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export interface AuthFlags {
|
|
25
|
+
readonly json?: boolean | undefined;
|
|
26
|
+
readonly apiUrl?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function authLogin(flags: AuthFlags = {}): Promise<void> {
|
|
30
|
+
const opened: string[] = [];
|
|
31
|
+
|
|
32
|
+
const session = await login({
|
|
33
|
+
apiUrl: flags.apiUrl,
|
|
34
|
+
// The same card a provider connect flow ends on. For a few seconds this
|
|
35
|
+
// page is the whole product, and it used to be an unstyled sentence on a
|
|
36
|
+
// white rectangle — in a browser set to dark, on a sign-in that had just
|
|
37
|
+
// worked. `brand.ts` carries the palette for both modes.
|
|
38
|
+
page: (outcome) =>
|
|
39
|
+
completionPage(
|
|
40
|
+
outcome.ok
|
|
41
|
+
? { label: 'Lanes', heading: 'Signed in', detail: outcome.detail, ok: true }
|
|
42
|
+
: { label: 'Lanes', heading: 'That did not work', detail: outcome.detail, ok: false },
|
|
43
|
+
),
|
|
44
|
+
onUrl: (url) => opened.push(url),
|
|
45
|
+
open: async (url) => {
|
|
46
|
+
// Printed as well as opened. A machine with no browser — a container, an
|
|
47
|
+
// SSH session — gets a URL it can paste somewhere that has one, which is
|
|
48
|
+
// the whole difference between "this does not work here" and "this takes
|
|
49
|
+
// one more step here".
|
|
50
|
+
print(style.dim(' Opening your browser to sign in. If nothing opens, use this:'));
|
|
51
|
+
print(` ${url}`);
|
|
52
|
+
print('');
|
|
53
|
+
await openInBrowser(url);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (flags.json === true) {
|
|
58
|
+
print(JSON.stringify({ subject: session.subject, email: session.email }, null, 2));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
print(ok(`signed in as ${style.bold(session.email ?? session.subject)}`));
|
|
63
|
+
print(style.dim(` subject ${session.subject}`));
|
|
64
|
+
print('');
|
|
65
|
+
print(
|
|
66
|
+
style.dim(
|
|
67
|
+
' A profile reaches you only where its members list that subject:\n' +
|
|
68
|
+
' lanes link profile members add <subject> --profile <name>',
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
void opened;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function authLogout(flags: AuthFlags = {}): Promise<void> {
|
|
75
|
+
const session = await readSession();
|
|
76
|
+
await clearSession();
|
|
77
|
+
|
|
78
|
+
if (flags.json === true) {
|
|
79
|
+
print(JSON.stringify({ signedOut: session !== null }, null, 2));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
print(
|
|
84
|
+
session === null
|
|
85
|
+
? style.dim('Not signed in; nothing to do.')
|
|
86
|
+
: ok(`signed out ${style.bold(session.email ?? session.subject)}`),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Said out loud, because it is the surprising half. Signing out removes the
|
|
90
|
+
// identity this machine presents; it does not touch a profile's members list,
|
|
91
|
+
// and it does not revoke a token an agent already holds.
|
|
92
|
+
print(
|
|
93
|
+
style.dim(
|
|
94
|
+
' Profiles still list you, and a client holding a token still holds it.\n' +
|
|
95
|
+
' To take a token back: lanes link token rotate --workspace <name>',
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function authStatus(flags: AuthFlags = {}): Promise<void> {
|
|
101
|
+
const session = await readSession();
|
|
102
|
+
|
|
103
|
+
if (session === null) {
|
|
104
|
+
if (flags.json === true) {
|
|
105
|
+
print(JSON.stringify({ signedIn: false }, null, 2));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
print(warn('not signed in'));
|
|
109
|
+
print(style.dim(' Run: lanes auth login'));
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fresh = isFresh(session);
|
|
115
|
+
|
|
116
|
+
if (flags.json === true) {
|
|
117
|
+
print(
|
|
118
|
+
JSON.stringify(
|
|
119
|
+
{
|
|
120
|
+
signedIn: true,
|
|
121
|
+
subject: session.subject,
|
|
122
|
+
email: session.email,
|
|
123
|
+
expiresAt: session.expiresAt,
|
|
124
|
+
fresh,
|
|
125
|
+
apiUrl: session.apiUrl,
|
|
126
|
+
},
|
|
127
|
+
null,
|
|
128
|
+
2,
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
print(ok(`signed in as ${style.bold(session.email ?? session.subject)}`));
|
|
135
|
+
print(` subject ${style.dim(session.subject)}`);
|
|
136
|
+
print(` api ${style.dim(session.apiUrl)}`);
|
|
137
|
+
print(
|
|
138
|
+
` token ${
|
|
139
|
+
fresh
|
|
140
|
+
? style.dim(`valid until ${session.expiresAt}`)
|
|
141
|
+
: style.dim('expired — the next command refreshes it')
|
|
142
|
+
}`,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (session.apiUrl !== DEFAULT_API_URL) {
|
|
146
|
+
print(style.dim(` This session was issued by ${session.apiUrl}, not the default.`));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Every workspace this identity can reach, and what it may do there.
|
|
152
|
+
*
|
|
153
|
+
* `GET /v1/me` is the source, and it is the server that decides — the client
|
|
154
|
+
* sends no uid, and the answer is derived from the token. That is what makes
|
|
155
|
+
* this listing something an operator can trust rather than a local guess.
|
|
156
|
+
*/
|
|
157
|
+
export async function authWorkspaces(flags: AuthFlags = {}): Promise<void> {
|
|
158
|
+
const session = await readSession();
|
|
159
|
+
if (session === null) {
|
|
160
|
+
print(warn('not signed in'));
|
|
161
|
+
print(style.dim(' Run: lanes auth login'));
|
|
162
|
+
process.exitCode = 1;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const token = await currentIdToken();
|
|
167
|
+
if (token === null) {
|
|
168
|
+
print(warn('the session could not be refreshed'));
|
|
169
|
+
print(style.dim(' Run: lanes auth login'));
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const response = await fetch(`${session.apiUrl}/v1/me`, {
|
|
175
|
+
headers: { authorization: `Bearer ${token}` },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
print(warn(`${session.apiUrl} answered ${response.status}`));
|
|
180
|
+
process.exitCode = 1;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const body = (await response.json()) as {
|
|
185
|
+
memberships?: { workspace_id: string; workspace_name: string; role: string }[];
|
|
186
|
+
};
|
|
187
|
+
const memberships = body.memberships ?? [];
|
|
188
|
+
|
|
189
|
+
if (flags.json === true) {
|
|
190
|
+
print(JSON.stringify({ memberships }, null, 2));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
print(style.dim(`signed in as ${session.email ?? session.subject}`));
|
|
195
|
+
print('');
|
|
196
|
+
|
|
197
|
+
if (memberships.length === 0) {
|
|
198
|
+
print(style.dim('You are a member of no Lanes workspace yet.'));
|
|
199
|
+
} else {
|
|
200
|
+
for (const one of memberships) {
|
|
201
|
+
print(` ${style.bold(one.workspace_name)} ${style.dim(one.role)}`);
|
|
202
|
+
print(` ${style.dim(one.workspace_id)}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
print('');
|
|
207
|
+
print(
|
|
208
|
+
style.dim(
|
|
209
|
+
'A remote lanes link workspace binds to one of these with `lanes_workspace:`\n' +
|
|
210
|
+
'in lanes-link.yaml, which is whose members a profile may delegate to.',
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Opens a URL, and says nothing if it cannot. The URL is printed either way. */
|
|
216
|
+
async function openInBrowser(url: string): Promise<void> {
|
|
217
|
+
const command =
|
|
218
|
+
process.platform === 'darwin'
|
|
219
|
+
? ['open', url]
|
|
220
|
+
: process.platform === 'win32'
|
|
221
|
+
? ['cmd', '/c', 'start', '', url]
|
|
222
|
+
: ['xdg-open', url];
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
await Bun.spawn(command, { stdout: 'ignore', stderr: 'ignore' }).exited;
|
|
226
|
+
} catch {
|
|
227
|
+
// Nothing to report: the URL is on screen above, which is the fallback.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -32,12 +32,12 @@ export function credentialApp(manifest: ProviderManifest): string | undefined {
|
|
|
32
32
|
*/
|
|
33
33
|
export function accountSiblings(
|
|
34
34
|
manifest: ProviderManifest,
|
|
35
|
-
|
|
35
|
+
connections: readonly ConnectionConfig[],
|
|
36
36
|
registry: { manifest(id: string): ProviderManifest | undefined },
|
|
37
37
|
): ConnectionConfig[] {
|
|
38
38
|
const app = credentialApp(manifest);
|
|
39
39
|
|
|
40
|
-
return
|
|
40
|
+
return connections.filter((connection) => {
|
|
41
41
|
if (connection.provider === manifest.id) return true;
|
|
42
42
|
if (!app) return false;
|
|
43
43
|
const sibling = registry.manifest(connection.provider);
|
|
@@ -55,7 +55,7 @@ export function accountSiblings(
|
|
|
55
55
|
*/
|
|
56
56
|
export function siblingAccountId(
|
|
57
57
|
manifest: ProviderManifest,
|
|
58
|
-
|
|
58
|
+
connections: readonly ConnectionConfig[],
|
|
59
59
|
registry: { manifest(id: string): ProviderManifest | undefined },
|
|
60
60
|
): string | undefined {
|
|
61
61
|
if (!credentialApp(manifest)) return undefined;
|
|
@@ -63,7 +63,7 @@ export function siblingAccountId(
|
|
|
63
63
|
// Asked of the registry, not inferred from the id: `app` is a manifest field,
|
|
64
64
|
// and a provider is free to declare `app: icloud` under any name it likes.
|
|
65
65
|
const ids = new Set(
|
|
66
|
-
accountSiblings(manifest,
|
|
66
|
+
accountSiblings(manifest, connections, registry)
|
|
67
67
|
.filter((connection) => connection.provider !== manifest.id)
|
|
68
68
|
.map((connection) => connection.id),
|
|
69
69
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registering } from './registration.ts';
|
|
1
2
|
import { auth, discoverOAuthProtectedResourceMetadata } from '@modelcontextprotocol/client';
|
|
2
3
|
import { CredentialOAuthProvider } from '#connectivity/auth/index.ts';
|
|
3
4
|
import type { SecretStore } from '#secrets';
|
|
@@ -146,10 +147,9 @@ export async function authorise(input: {
|
|
|
146
147
|
|
|
147
148
|
const scope = manifest.auth.scopes.length > 0 ? manifest.auth.scopes.join(' ') : undefined;
|
|
148
149
|
|
|
149
|
-
const first = await
|
|
150
|
-
serverUrl,
|
|
151
|
-
|
|
152
|
-
});
|
|
150
|
+
const first = await registering(manifest, serverUrl, () =>
|
|
151
|
+
auth(provider as never, { serverUrl, ...(scope ? { scope } : {}) }),
|
|
152
|
+
);
|
|
153
153
|
|
|
154
154
|
if (first === 'AUTHORIZED') {
|
|
155
155
|
progress(ok('already authorised'));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { grantedConnections } from '../../runtime.ts';
|
|
1
2
|
import { bindConnectionCredentials, type BindOutcome } from '#deployments/bind.ts';
|
|
2
3
|
import type { Runtime } from '../../runtime.ts';
|
|
3
4
|
|
|
@@ -35,7 +36,7 @@ export function bindNewCredential(
|
|
|
35
36
|
connectionId: string,
|
|
36
37
|
account: string,
|
|
37
38
|
): Promise<BindOutcome> {
|
|
38
|
-
const declared = runtime.
|
|
39
|
+
const declared = grantedConnections(runtime).find(
|
|
39
40
|
(candidate) => candidate.provider === providerId && candidate.id === connectionId,
|
|
40
41
|
);
|
|
41
42
|
|
|
@@ -92,7 +92,7 @@ export async function connectCustom(
|
|
|
92
92
|
if (options.json !== true) announce(resolution);
|
|
93
93
|
|
|
94
94
|
const path = manifestPath(workspaceRoot, profile, providerId);
|
|
95
|
-
const rerun = `${PROGRAM} connect custom ${providerId} --profile ${profile} --
|
|
95
|
+
const rerun = `${PROGRAM} connect custom ${providerId} --profile ${profile} --workspace ${target}`;
|
|
96
96
|
|
|
97
97
|
const prompter =
|
|
98
98
|
options.prompter ??
|
|
@@ -17,7 +17,7 @@ import { parseManifest } from '#providers/custom/index.ts';
|
|
|
17
17
|
|
|
18
18
|
/** Where this profile keeps its own declarations. */
|
|
19
19
|
export function manifestPath(workspaceRoot: string, profile: string, id: string): string {
|
|
20
|
-
return join(workspaceRoot, layout.providers(
|
|
20
|
+
return join(workspaceRoot, layout.providers(), `${id}.yaml`);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -142,7 +142,7 @@ export async function checkOpenapiReachable(
|
|
|
142
142
|
|
|
143
143
|
const beside = isAbsolute(value)
|
|
144
144
|
? value
|
|
145
|
-
: resolve(join(workspaceRoot, layout.providers(
|
|
145
|
+
: resolve(join(workspaceRoot, layout.providers()), value);
|
|
146
146
|
|
|
147
147
|
if (await exists(beside)) return;
|
|
148
148
|
|
|
@@ -1,27 +1,42 @@
|
|
|
1
|
+
import type { Config } from '#profile';
|
|
1
2
|
import type { ConfigDocument } from '../../config-edit.ts';
|
|
2
3
|
import { matchesRule } from './accounts.ts';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* What connecting grants.
|
|
6
7
|
*
|
|
7
|
-
* One
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* connected.
|
|
8
|
+
* One row naming the connection, carrying the wildcard for its provider — not
|
|
9
|
+
* one rule per capability. The pinned-per-tool form this replaced was 85 lines
|
|
10
|
+
* for four providers and unreadable, and what it bought — a vendor cannot widen
|
|
11
|
+
* your policy by shipping a new tool — is preserved instead by `doctor`, which
|
|
12
|
+
* reports capabilities that appeared after you connected.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* A row rather than a rule, since contract 3. The connection and the permission
|
|
15
|
+
* used to be two writes into two blocks, which is what made "a row with no rule"
|
|
16
|
+
* and "a rule with no row" both expressible and both silent: the first served
|
|
17
|
+
* nothing, the second was refused at load. A grant is one thing now (ADR-058),
|
|
18
|
+
* so neither state exists to be repaired.
|
|
19
|
+
*
|
|
20
|
+
* Idempotent, and by matching rather than by equality: a profile whose row for
|
|
21
|
+
* this connection already holds a broader rule is not widened, and a second
|
|
22
|
+
* connect to the same account adds nothing.
|
|
16
23
|
*/
|
|
17
|
-
export function
|
|
24
|
+
export function grantConnection(
|
|
18
25
|
document: ConfigDocument,
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
config: Config,
|
|
27
|
+
connection: string,
|
|
21
28
|
): string[] {
|
|
22
|
-
const rule = `${
|
|
23
|
-
|
|
29
|
+
const rule = `${connection.split('.')[0] ?? ''}.*`;
|
|
30
|
+
const index = config.grants.findIndex((grant) => grant.connection === connection);
|
|
31
|
+
|
|
32
|
+
if (index === -1) {
|
|
33
|
+
document.addTo(['grants'], { connection, allow: [rule], deny: [] });
|
|
34
|
+
return [rule];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const existing = config.grants[index]!;
|
|
38
|
+
if (existing.allow.some((held) => matchesRule(held.capability, rule))) return [];
|
|
24
39
|
|
|
25
|
-
document.addTo(['
|
|
40
|
+
document.addTo(['grants', index, 'allow'], rule, { inline: true });
|
|
26
41
|
return [rule];
|
|
27
42
|
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
import { CONNECTIONS_FILE } from '#profile';
|
|
1
2
|
import { credentialRefForConnection, WRITE_BUNDLE } from '#connectivity';
|
|
3
|
+
import { recordConfigChange } from '../../audit-change.ts';
|
|
2
4
|
import { ConfigDocument } from '../../config-edit.ts';
|
|
3
5
|
import { ensureOwnerLayer, repaired } from '../../config-repair.ts';
|
|
4
6
|
import { emit, print } from '../../output.ts';
|
|
5
7
|
import { nonInteractivePrompter, terminalPrompter, type Prompter } from '../../prompt.ts';
|
|
6
|
-
import {
|
|
8
|
+
import type { ConnectOptions } from './options.ts';
|
|
9
|
+
|
|
10
|
+
export type { ConnectOptions } from './options.ts';
|
|
11
|
+
import {
|
|
12
|
+
grantedConnections,
|
|
13
|
+
openRuntime,
|
|
14
|
+
primaryProfile,
|
|
15
|
+
type GlobalFlags,
|
|
16
|
+
} from '../../runtime.ts';
|
|
7
17
|
import { moveCredential, siblingAccountId } from './accounts.ts';
|
|
8
|
-
import {
|
|
18
|
+
import { grantConnection } from './grant.ts';
|
|
9
19
|
import { acquireCredential } from './acquire.ts';
|
|
10
20
|
import { declareConnection } from './declare.ts';
|
|
11
21
|
import { resolveConnectionAddress, type ResolvedAddress } from './variables.ts';
|
|
@@ -25,80 +35,8 @@ import { settleIdentity } from './settle.ts';
|
|
|
25
35
|
import { runStrategySetup } from './strategy.ts';
|
|
26
36
|
import { announceConnectTarget } from './target-note.ts';
|
|
27
37
|
import { unknownProvider } from './unknown.ts';
|
|
38
|
+
import { DISCOVERY_NAMESPACE } from '#stores/state';
|
|
28
39
|
|
|
29
|
-
/**
|
|
30
|
-
* `lanes link connect <provider>` — the one command that adds an account.
|
|
31
|
-
*
|
|
32
|
-
* The same command regardless of connectivity. A local provider declares a
|
|
33
|
-
* connection and stops; an MCP provider authorises, asks the upstream server
|
|
34
|
-
* what it exposes, and grants it. Core learns nothing about any vendor: the
|
|
35
|
-
* manifest says how to reach them and what to ask the operator for.
|
|
36
|
-
*
|
|
37
|
-
* The five numbered steps below are the whole command, and each one that grew
|
|
38
|
-
* past a paragraph moved out: `authorise.ts` gets the token, `setup.ts` asks the
|
|
39
|
-
* operator for what the vendor's console produced, `settle.ts` works out whose
|
|
40
|
-
* account it was, and `accounts.ts` knows which connections are siblings.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
export interface ConnectOptions extends GlobalFlags {
|
|
44
|
-
readonly id?: string | undefined;
|
|
45
|
-
readonly displayName?: string | undefined;
|
|
46
|
-
/** `--set key=value`, repeatable: where this connection's service is. */
|
|
47
|
-
readonly set?: readonly string[] | string | undefined;
|
|
48
|
-
/**
|
|
49
|
-
* `--label`: what to call this connection, instead of being asked.
|
|
50
|
-
*
|
|
51
|
-
* Distinct from `--display-name`, which answers *whose account this is* for a
|
|
52
|
-
* provider that cannot report it. This one never touches the identity, so it
|
|
53
|
-
* is safe to pass anything a person would say out loud.
|
|
54
|
-
*/
|
|
55
|
-
readonly label?: string | undefined;
|
|
56
|
-
/** Ask for the stored credential again — a key rotated, or a password revoked. */
|
|
57
|
-
readonly replace?: boolean | undefined;
|
|
58
|
-
/**
|
|
59
|
-
* Answer nothing from a terminal: resolve every declared value from the
|
|
60
|
-
* credential store, and refuse with instructions where one is missing.
|
|
61
|
-
*/
|
|
62
|
-
readonly nonInteractive?: boolean | undefined;
|
|
63
|
-
/** The operator has already agreed to scopes broader than the provider needs. */
|
|
64
|
-
readonly acceptBroadScopes?: boolean | undefined;
|
|
65
|
-
/**
|
|
66
|
-
* Register an OAuth client of your own rather than using a hosted one.
|
|
67
|
-
*
|
|
68
|
-
* Sticky by consequence rather than by flag: it writes the `oauth_apps` entry,
|
|
69
|
-
* and a profile that declares one is never moved off it. So this is typed once
|
|
70
|
-
* and then forgotten, which is the right shape for a decision about a client
|
|
71
|
-
* that is shared by every connection of that vendor.
|
|
72
|
-
*/
|
|
73
|
-
/**
|
|
74
|
-
* `--own-client`, the older spelling of one of the routes `--auth` now names.
|
|
75
|
-
*
|
|
76
|
-
* Kept because it is in scripts and in a year of documentation, and because
|
|
77
|
-
* it still says something true. It resolves to `--auth own_client`.
|
|
78
|
-
*/
|
|
79
|
-
readonly ownClient?: boolean | undefined;
|
|
80
|
-
/**
|
|
81
|
-
* `--auth <method>`: which way in, for a provider that offers more than one.
|
|
82
|
-
*
|
|
83
|
-
* Unset means ask, where there is somebody to ask and something to ask
|
|
84
|
-
* about. It is not sticky the way `--own-client` is: `--own-client` writes an
|
|
85
|
-
* `oauth_apps` entry that every connection of the vendor then reads, whereas
|
|
86
|
-
* this decides one connection's credential and is recorded by that credential
|
|
87
|
-
* existing. Two accounts on the same profile may honestly differ.
|
|
88
|
-
*/
|
|
89
|
-
readonly auth?: string | undefined;
|
|
90
|
-
/** Injected for tests. The broker is the only thing `connect` fetches. */
|
|
91
|
-
readonly fetch?: typeof globalThis.fetch | undefined;
|
|
92
|
-
readonly json?: boolean | undefined;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* The connection id used before the provider has said whose account this is.
|
|
98
|
-
*
|
|
99
|
-
* It is a placeholder rather than a name, and `setup.ts` reads it as one: a
|
|
100
|
-
* credential filed under it belongs to a `connect` that did not finish.
|
|
101
|
-
*/
|
|
102
40
|
const PROVISIONAL_ID = 'pending';
|
|
103
41
|
|
|
104
42
|
export async function connect(target: string, options: ConnectOptions): Promise<void> {
|
|
@@ -128,7 +66,13 @@ export async function runConnect(
|
|
|
128
66
|
|
|
129
67
|
// The runtime is opened first because its registry includes workspace
|
|
130
68
|
// manifests — a custom provider must be as connectable as a built-in.
|
|
131
|
-
|
|
69
|
+
// A connection belongs to the workspace, so connecting does not need a
|
|
70
|
+
// profile (ADR-057). One is still resolved, because the registry that reads
|
|
71
|
+
// `providers.d/` is opened through a runtime and a runtime carries one — but
|
|
72
|
+
// it is not the subject, and nothing is written to it unless `--profile` was
|
|
73
|
+
// actually given.
|
|
74
|
+
const granting = options.profile !== undefined;
|
|
75
|
+
const runtime = await openRuntime({ ...options, profile: await primaryProfile(options) });
|
|
132
76
|
|
|
133
77
|
// Declared out here so the `finally` can close whatever it built.
|
|
134
78
|
let address: ResolvedAddress | undefined;
|
|
@@ -139,7 +83,7 @@ export async function runConnect(
|
|
|
139
83
|
// `gs://` workspace is a second network read of the same YAML. Still long
|
|
140
84
|
// before the browser opens, which is the part that matters. Inside the
|
|
141
85
|
// `try` so the `finally` closes the runtime if the rendering throws.
|
|
142
|
-
if (!announced) announceConnectTarget(runtime, options.json);
|
|
86
|
+
if (!announced) announceConnectTarget(runtime, options.json, granting);
|
|
143
87
|
|
|
144
88
|
const registry = runtime.registry;
|
|
145
89
|
const entry = registry.get(providerId);
|
|
@@ -171,7 +115,17 @@ export async function runConnect(
|
|
|
171
115
|
}
|
|
172
116
|
|
|
173
117
|
const manifest = entry.manifest;
|
|
174
|
-
|
|
118
|
+
// Two documents, because a connection and a grant live in two files
|
|
119
|
+
// (ADR-057). Both opened here, so a failure to open either happens before
|
|
120
|
+
// anything is written to the other.
|
|
121
|
+
const document = await ConfigDocument.open(
|
|
122
|
+
runtime.resolution.workspaceRoot,
|
|
123
|
+
runtime.resolution.profile,
|
|
124
|
+
);
|
|
125
|
+
const connectionsDocument = await ConfigDocument.openKey(
|
|
126
|
+
runtime.resolution.workspaceRoot,
|
|
127
|
+
CONNECTIONS_FILE,
|
|
128
|
+
);
|
|
175
129
|
const changes: string[] = [];
|
|
176
130
|
|
|
177
131
|
// 1. Authorise, if this provider needs it.
|
|
@@ -183,7 +137,7 @@ export async function runConnect(
|
|
|
183
137
|
// Unless a sibling already knows: when several providers of one vendor
|
|
184
138
|
// share a per-account credential, the second one should adopt the first's
|
|
185
139
|
// id rather than invent `pending` and ask for a password already held.
|
|
186
|
-
const adopted = siblingAccountId(manifest, runtime.
|
|
140
|
+
const adopted = siblingAccountId(manifest, runtime.workspaceConnections, registry);
|
|
187
141
|
const named = options.id ?? namedId;
|
|
188
142
|
const provisionalId = named ?? adopted ?? PROVISIONAL_ID;
|
|
189
143
|
|
|
@@ -252,10 +206,17 @@ export async function runConnect(
|
|
|
252
206
|
manifest,
|
|
253
207
|
provisionalId,
|
|
254
208
|
credentials: runtime.credentials,
|
|
255
|
-
|
|
209
|
+
// The connections file, not the profile. `oauth_apps` moved there in
|
|
210
|
+
// contract 3 — `configSchema` does not declare it any more and only
|
|
211
|
+
// `connectionsFileSchema` does — so `--own-client` was writing the switch
|
|
212
|
+
// into a key nothing reads. With no `--profile` the profile document is
|
|
213
|
+
// not even saved, so it was reported and discarded; with one, it landed in
|
|
214
|
+
// a block the schema drops, and dispatch fell back to the broker client
|
|
215
|
+
// while `deploy` bound none of the operator's own refs.
|
|
216
|
+
document: connectionsDocument,
|
|
256
217
|
changes,
|
|
257
218
|
providerId,
|
|
258
|
-
connections: runtime
|
|
219
|
+
connections: grantedConnections(runtime),
|
|
259
220
|
target,
|
|
260
221
|
profile,
|
|
261
222
|
prompter,
|
|
@@ -305,7 +266,7 @@ export async function runConnect(
|
|
|
305
266
|
credentials: runtime.credentials,
|
|
306
267
|
connectorFor: address.connectorFor,
|
|
307
268
|
remember: async (found) => {
|
|
308
|
-
await runtime.state.kv.set(
|
|
269
|
+
await runtime.state.kv.set(DISCOVERY_NAMESPACE, providerId, JSON.stringify(found));
|
|
309
270
|
registry.setDiscovered(providerId, found);
|
|
310
271
|
},
|
|
311
272
|
});
|
|
@@ -313,8 +274,8 @@ export async function runConnect(
|
|
|
313
274
|
// 4. Declare the connection, or update the one this account already has.
|
|
314
275
|
changes.push(
|
|
315
276
|
...declareConnection({
|
|
316
|
-
document,
|
|
317
|
-
connections: runtime.
|
|
277
|
+
document: connectionsDocument,
|
|
278
|
+
connections: runtime.workspaceConnections,
|
|
318
279
|
providerId,
|
|
319
280
|
connectionId,
|
|
320
281
|
account,
|
|
@@ -324,8 +285,17 @@ export async function runConnect(
|
|
|
324
285
|
}),
|
|
325
286
|
);
|
|
326
287
|
|
|
327
|
-
// 5. Grant it — one
|
|
328
|
-
|
|
288
|
+
// 5. Grant it — one row naming the connection, carrying its provider's
|
|
289
|
+
// wildcard (ADR-058). The row *is* the grant, so neither half can be
|
|
290
|
+
// written without the other.
|
|
291
|
+
// Only when a profile was named. Connecting authorises an account into
|
|
292
|
+
// the workspace; deciding what may be done with it belongs to a profile,
|
|
293
|
+
// and those are two acts — so `connect` without `--profile` leaves the
|
|
294
|
+
// account granted to nobody and says how to grant it. Writing a grant
|
|
295
|
+
// into a profile the operator did not name would be choosing for them.
|
|
296
|
+
const granted = granting
|
|
297
|
+
? grantConnection(document, runtime.config, `${providerId}.${connectionId}`)
|
|
298
|
+
: [];
|
|
329
299
|
|
|
330
300
|
// 6. Repair the owner layer if this profile predates it.
|
|
331
301
|
//
|
|
@@ -342,7 +312,7 @@ export async function runConnect(
|
|
|
342
312
|
// sentence in it is something a caller counting edits has to recognise
|
|
343
313
|
// and skip, and `granted` is the field that answers "what did this widen"
|
|
344
314
|
// — an audit reading it would have missed `setup.*` entirely.
|
|
345
|
-
const repair = ensureOwnerLayer(document);
|
|
315
|
+
const repair = ensureOwnerLayer(connectionsDocument, document, { grants: granting });
|
|
346
316
|
changes.push(...repair.changes);
|
|
347
317
|
granted.push(...repair.granted);
|
|
348
318
|
|
|
@@ -369,7 +339,39 @@ export async function runConnect(
|
|
|
369
339
|
};
|
|
370
340
|
}
|
|
371
341
|
|
|
372
|
-
|
|
342
|
+
// The connections file first, always, and the profile second.
|
|
343
|
+
//
|
|
344
|
+
// First because a grant naming a connection the workspace does not hold is
|
|
345
|
+
// refused at load by `assertGrantsResolve` — so if only one of these two
|
|
346
|
+
// writes lands, it has to be this one. The other order leaves a workspace
|
|
347
|
+
// that will not open.
|
|
348
|
+
//
|
|
349
|
+
// Always because this is where the connection is *declared*, and until this
|
|
350
|
+
// release it was never written at all: `declareConnection` edited a document
|
|
351
|
+
// in memory and nothing saved it, so a connect stored the credential, wrote
|
|
352
|
+
// the grant, and left no row. The account was authorised, invisible, and
|
|
353
|
+
// unusable.
|
|
354
|
+
await connectionsDocument.save();
|
|
355
|
+
|
|
356
|
+
// Untouched when no profile was named, and `save()` on an unchanged document
|
|
357
|
+
// still rewrites the file — which would restamp a profile nobody asked to
|
|
358
|
+
// edit.
|
|
359
|
+
if (granting) await document.save();
|
|
360
|
+
|
|
361
|
+
// A connection belongs to the workspace, so the row is scoped to the
|
|
362
|
+
// workspace even when a profile was granted it in the same breath — the
|
|
363
|
+
// grant is its own event, written by `grant`.
|
|
364
|
+
await recordConfigChange(
|
|
365
|
+
runtime.config,
|
|
366
|
+
runtime.resolution.workspaceRoot,
|
|
367
|
+
runtime.target,
|
|
368
|
+
{
|
|
369
|
+
capability: 'config.connection.create',
|
|
370
|
+
scope: runtime.target,
|
|
371
|
+
connection: connectionKey,
|
|
372
|
+
arguments: { account, ...(label ? { label } : {}) },
|
|
373
|
+
},
|
|
374
|
+
);
|
|
373
375
|
|
|
374
376
|
// Where the endpoint that has to serve this reads its config, and then a
|
|
375
377
|
// nudge to re-read it. Neither is a deploy (ADR-029).
|