@lanes-sh/link 0.2.2 → 0.3.1
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 +22 -8
- package/instructions/skills/lanes-link/SKILL.md +42 -14
- package/package.json +1 -1
- package/src/cli/argv.ts +50 -0
- package/src/cli/brand.ts +178 -0
- package/src/cli/callback-page.ts +108 -128
- package/src/cli/commands/connect/accounts.ts +5 -0
- package/src/cli/commands/connect/assertion.ts +187 -0
- package/src/cli/commands/connect/authorise.ts +61 -17
- package/src/cli/commands/connect/client.ts +37 -9
- package/src/cli/commands/connect/discover.ts +94 -0
- package/src/cli/commands/connect/family.ts +72 -0
- package/src/cli/commands/connect/index.ts +113 -115
- package/src/cli/commands/connect/method.ts +237 -0
- package/src/cli/commands/connect/outcome.ts +42 -1
- package/src/cli/commands/connect/pasted-token.ts +66 -0
- package/src/cli/commands/connect/requirements.ts +60 -8
- package/src/cli/commands/connect/setup.ts +16 -5
- package/src/cli/commands/connect/target-note.ts +34 -0
- package/src/cli/commands/identity.ts +258 -0
- package/src/cli/commands/knowledge/index.ts +390 -0
- package/src/cli/commands/knowledge/migrate.ts +180 -0
- package/src/cli/commands/knowledge/setup.ts +144 -0
- package/src/cli/commands/knowledge.ts +10 -0
- package/src/cli/commands/mcp/harnesses.ts +16 -2
- package/src/cli/commands/mcp/register.ts +9 -1
- package/src/cli/commands/mcp/stdio.ts +21 -0
- package/src/cli/commands/operate/dashboard.ts +107 -0
- package/src/cli/commands/operate/findings.ts +151 -0
- package/src/cli/commands/operate/inspect.ts +63 -164
- package/src/cli/commands/operate/outputs.ts +38 -11
- package/src/cli/commands/operate/policy.ts +7 -7
- package/src/cli/commands/operate/serve.ts +3 -0
- package/src/cli/commands/operate/status.ts +108 -1
- package/src/cli/commands/operate/token.ts +1 -1
- package/src/cli/commands/operate.ts +2 -0
- package/src/cli/commands/profile/declare.ts +154 -0
- package/src/cli/commands/profile/removal.ts +17 -0
- package/src/cli/commands/profile/remove.ts +5 -5
- package/src/cli/commands/profile.ts +83 -35
- package/src/cli/commands/secrets.ts +6 -6
- package/src/cli/commands/setup.ts +22 -6
- package/src/cli/commands/sync.ts +262 -0
- package/src/cli/commands/target.ts +65 -83
- package/src/cli/config-edit.ts +53 -144
- package/src/cli/config-repair.ts +186 -0
- package/src/cli/dashboard-page.ts +284 -0
- package/src/cli/dashboard-shell.ts +125 -0
- package/src/cli/dispatch-owner.ts +93 -0
- package/src/cli/identity.ts +12 -1
- package/src/cli/main.ts +90 -61
- package/src/cli/nearest.ts +45 -0
- package/src/cli/oauth-callback.ts +187 -0
- package/src/cli/oauth-exchange.ts +57 -15
- package/src/cli/oauth.ts +67 -177
- package/src/cli/output.ts +21 -5
- package/src/cli/provider-marks.ts +45 -0
- package/src/cli/runtime/open.ts +74 -51
- package/src/cli/runtime/registry.ts +60 -2
- package/src/cli/runtime/select.ts +26 -13
- package/src/cli/runtime/vault.ts +61 -0
- package/src/cli/runtime.ts +2 -1
- package/src/cli/selection.ts +365 -0
- package/src/cli/usage.ts +42 -11
- package/src/connectivity/auth/README.md +7 -1
- package/src/connectivity/auth/basic/index.ts +1 -1
- package/src/connectivity/auth/index.ts +14 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +26 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +16 -2
- package/src/connectivity/auth/oauth-authcode/provider.ts +1 -1
- package/src/connectivity/auth/oauth-authcode/refresh.ts +3 -3
- package/src/connectivity/auth/oauth-jwt/README.md +33 -0
- package/src/connectivity/auth/oauth-jwt/index.ts +237 -0
- package/src/connectivity/auth/oauth-jwt/key.ts +148 -0
- package/src/connectivity/auth/resolve.ts +1 -1
- package/src/connectivity/auth/token.ts +11 -0
- package/src/connectivity/index.ts +2 -0
- package/src/connectivity/manifest/auth.ts +99 -2
- package/src/connectivity/manifest/identity.ts +12 -0
- package/src/connectivity/manifest/index.ts +3 -1
- package/src/connectivity/manifest/provider.ts +37 -8
- package/src/connectivity/manifest/requirements.ts +109 -6
- package/src/deployments/adapters/filesystem.ts +10 -1
- package/src/deployments/adapters/github-api.ts +106 -0
- package/src/deployments/adapters/github-commit.ts +103 -0
- package/src/deployments/adapters/github-repo.ts +356 -0
- package/src/deployments/adapters/github-testing.ts +258 -0
- package/src/deployments/adapters/github.ts +125 -0
- package/src/deployments/deploy.ts +94 -114
- package/src/deployments/discover.ts +103 -0
- package/src/deployments/driver.ts +8 -1
- package/src/deployments/gcp/driver.ts +3 -1
- package/src/deployments/knowledge.ts +119 -0
- package/src/deployments/prepare.ts +12 -6
- package/src/deployments/report.ts +117 -0
- package/src/deployments/servable.ts +82 -0
- package/src/deployments/serving.ts +165 -0
- package/src/deployments/sync-apply.ts +276 -0
- package/src/deployments/sync.ts +136 -0
- package/src/deployments/target.ts +3 -2
- package/src/deployments/upload.ts +19 -12
- package/src/dispatch/dispatch.ts +1 -1
- package/src/profile/deployments.ts +80 -0
- package/src/profile/identity.ts +60 -0
- package/src/profile/index.ts +23 -5
- package/src/profile/knowledge.ts +124 -0
- package/src/profile/load.ts +17 -5
- package/src/profile/primitives.ts +24 -1
- package/src/profile/schema.ts +81 -3
- package/src/profile/targets.ts +122 -109
- package/src/profile/workspace.ts +139 -79
- package/src/providers/google/calendar/index.ts +2 -0
- package/src/providers/google/contacts/index.ts +2 -0
- package/src/providers/google/docs/index.ts +2 -0
- package/src/providers/google/drive/index.ts +2 -0
- package/src/providers/google/gmail/index.ts +2 -0
- package/src/providers/google/gmail-imap/index.ts +125 -0
- package/src/providers/google/index.ts +2 -1
- package/src/providers/google/shared/oauth.ts +18 -6
- package/src/providers/google/shared/service-account.ts +110 -0
- package/src/providers/google/shared/setup.ts +21 -3
- package/src/providers/google/sheets/index.ts +2 -0
- package/src/providers/google/tasks/index.ts +2 -0
- package/src/providers/identity/provider.ts +166 -0
- package/src/providers/index.ts +3 -0
- package/src/providers/owner.ts +10 -2
- package/src/providers/scopes.ts +2 -0
- package/src/providers/setup/plan.ts +31 -9
- package/src/providers/setup/provider.ts +23 -0
- package/src/providers/slack/index.ts +81 -33
- package/src/providers/slack/oauth.ts +103 -0
- package/src/providers/slack/scopes.ts +37 -0
- package/src/server/container.ts +18 -1
- package/src/server/cors.ts +252 -0
- package/src/server/dashboard.ts +208 -0
- package/src/server/endpoint.ts +45 -1
- package/src/server/generations.ts +11 -2
- package/src/server/harness.ts +7 -0
- package/src/server/index.ts +45 -6
- package/src/server/mcp/index.ts +1 -0
- package/src/server/mcp/instructions.ts +28 -1
- package/src/server/mcp/visibility.ts +33 -0
- package/src/stores/blobs/route.ts +123 -0
package/src/cli/oauth.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
-
import { completionPage } from './callback-page.ts';
|
|
3
2
|
import { OAuthError } from './oauth-error.ts';
|
|
4
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
captureOAuthCallback,
|
|
5
|
+
connectedPage,
|
|
6
|
+
failedPage,
|
|
7
|
+
shutdown,
|
|
8
|
+
stateMatches,
|
|
9
|
+
withTimeout,
|
|
10
|
+
type CallbackCapture,
|
|
11
|
+
} from './oauth-callback.ts';
|
|
12
|
+
import {
|
|
13
|
+
directExchange,
|
|
14
|
+
type ExchangeCode,
|
|
15
|
+
type OAuthTokens,
|
|
16
|
+
type RefreshTokenPolicy,
|
|
17
|
+
} from './oauth-exchange.ts';
|
|
5
18
|
|
|
6
19
|
/**
|
|
7
20
|
* The OAuth authorization-code exchange, run entirely from the CLI.
|
|
@@ -36,7 +49,23 @@ export interface OAuthFlowOptions {
|
|
|
36
49
|
* state check, and the browser cannot drift apart between the two paths.
|
|
37
50
|
*/
|
|
38
51
|
readonly exchange?: ExchangeCode;
|
|
52
|
+
/** What a response carrying no refresh token means for this vendor. */
|
|
53
|
+
readonly refreshToken: RefreshTokenPolicy;
|
|
39
54
|
readonly authorizeParams?: Readonly<Record<string, string>>;
|
|
55
|
+
/**
|
|
56
|
+
* An HTTPS URL to name as the redirect, for a vendor that will take no other.
|
|
57
|
+
*
|
|
58
|
+
* Absent for every vendor that accepts a loopback callback, which is all of
|
|
59
|
+
* them but Slack — there the listener names itself and this is the whole of
|
|
60
|
+
* it. Present where the vendor's redirect must be HTTPS: the browser then
|
|
61
|
+
* lands on that URL, which bounces it down to the listener opened here, and
|
|
62
|
+
* the port it needs to bounce to travels in `state`.
|
|
63
|
+
*
|
|
64
|
+
* The listener is unchanged either way. It still binds loopback on a port the
|
|
65
|
+
* kernel picked, still serves exactly one callback, and still checks `state`
|
|
66
|
+
* on the way back. What changes is only the address the vendor is told.
|
|
67
|
+
*/
|
|
68
|
+
readonly relayRedirect?: string;
|
|
40
69
|
/** What the completion page names as connected. A provider's display name. */
|
|
41
70
|
readonly connectionLabel?: string;
|
|
42
71
|
/** How long to wait for the operator to finish in the browser. */
|
|
@@ -49,7 +78,12 @@ export interface OAuthFlowOptions {
|
|
|
49
78
|
// Re-exported so the flow stays one import for its callers even though the
|
|
50
79
|
// exchange half now lives next door.
|
|
51
80
|
export { OAuthError };
|
|
52
|
-
export type { ExchangeCode, OAuthTokens };
|
|
81
|
+
export type { ExchangeCode, OAuthTokens, RefreshTokenPolicy };
|
|
82
|
+
|
|
83
|
+
// Re-exported so a caller wanting a loopback callback still has one import to
|
|
84
|
+
// reach for. Which file it lives in is this module's business, not theirs.
|
|
85
|
+
export { captureOAuthCallback };
|
|
86
|
+
export type { CallbackCapture };
|
|
53
87
|
|
|
54
88
|
const base64url = (input: Buffer): string => input.toString('base64url');
|
|
55
89
|
|
|
@@ -60,13 +94,6 @@ export function createPkcePair(): { verifier: string; challenge: string } {
|
|
|
60
94
|
return { verifier, challenge };
|
|
61
95
|
}
|
|
62
96
|
|
|
63
|
-
/** Compare the returned state in constant time — it is a CSRF defence. */
|
|
64
|
-
function stateMatches(expected: string, received: string): boolean {
|
|
65
|
-
const a = createHash('sha256').update(expected).digest();
|
|
66
|
-
const b = createHash('sha256').update(received).digest();
|
|
67
|
-
return timingSafeEqual(a, b);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
97
|
export function defaultOpenBrowser(url: string): void {
|
|
71
98
|
const command =
|
|
72
99
|
process.platform === 'darwin'
|
|
@@ -85,24 +112,6 @@ export function defaultOpenBrowser(url: string): void {
|
|
|
85
112
|
}
|
|
86
113
|
}
|
|
87
114
|
|
|
88
|
-
/**
|
|
89
|
-
* What the browser is left looking at, in the two shapes it comes in.
|
|
90
|
-
*
|
|
91
|
-
* The label is what the caller knows and this file cannot: which provider the
|
|
92
|
-
* grant was for. Given one, the page names it under "Connected" the way the
|
|
93
|
-
* invite page names the workspace; without one it says only that the flow
|
|
94
|
-
* finished, which is what every path said before.
|
|
95
|
-
*/
|
|
96
|
-
const connectedPage = (label?: string): Response =>
|
|
97
|
-
completionPage({
|
|
98
|
-
...(label ? { label: 'Connected', heading: label } : { heading: 'Connected' }),
|
|
99
|
-
detail: 'You can close this tab and return to the terminal.',
|
|
100
|
-
ok: true,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const failedPage = (detail: string): Response =>
|
|
104
|
-
completionPage({ heading: 'Authorization failed', detail, ok: false });
|
|
105
|
-
|
|
106
115
|
/**
|
|
107
116
|
* Run the flow and return the tokens.
|
|
108
117
|
*
|
|
@@ -112,7 +121,6 @@ const failedPage = (detail: string): Response =>
|
|
|
112
121
|
*/
|
|
113
122
|
export async function runOAuthFlow(options: OAuthFlowOptions): Promise<OAuthTokens> {
|
|
114
123
|
const { verifier, challenge } = createPkcePair();
|
|
115
|
-
const state = base64url(randomBytes(24));
|
|
116
124
|
const timeoutMs = options.timeoutMs ?? 5 * 60_000;
|
|
117
125
|
|
|
118
126
|
let resolveCode: (code: string) => void;
|
|
@@ -124,7 +132,7 @@ export async function runOAuthFlow(options: OAuthFlowOptions): Promise<OAuthToke
|
|
|
124
132
|
|
|
125
133
|
const server = Bun.serve({
|
|
126
134
|
hostname: '127.0.0.1',
|
|
127
|
-
port: 0, // let the OS choose;
|
|
135
|
+
port: 0, // let the OS choose; nothing outside this machine names this port
|
|
128
136
|
fetch(request) {
|
|
129
137
|
const url = new URL(request.url);
|
|
130
138
|
if (url.pathname !== '/callback') return new Response('Not found', { status: 404 });
|
|
@@ -143,8 +151,9 @@ export async function runOAuthFlow(options: OAuthFlowOptions): Promise<OAuthToke
|
|
|
143
151
|
|
|
144
152
|
const returnedState = url.searchParams.get('state');
|
|
145
153
|
if (!returnedState || !stateMatches(state, returnedState)) {
|
|
146
|
-
// A mismatched state means this callback did not come from the
|
|
147
|
-
// we started. Refuse it rather than redeeming whatever code
|
|
154
|
+
// A mismatched state means this callback did not come from the
|
|
155
|
+
// request we started. Refuse it rather than redeeming whatever code
|
|
156
|
+
// it carries.
|
|
148
157
|
rejectCode(new OAuthError('State mismatch — ignoring an unexpected callback.'));
|
|
149
158
|
return failedPage('Unexpected callback.');
|
|
150
159
|
}
|
|
@@ -160,7 +169,25 @@ export async function runOAuthFlow(options: OAuthFlowOptions): Promise<OAuthToke
|
|
|
160
169
|
},
|
|
161
170
|
});
|
|
162
171
|
|
|
163
|
-
|
|
172
|
+
// Where the vendor is told to send the browser. The relay bounces it back
|
|
173
|
+
// here; without one it comes here directly, which is every other provider.
|
|
174
|
+
const redirectUri = options.relayRedirect ?? `http://127.0.0.1:${server.port}/callback`;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The CSRF binding, and — behind a relay — the only way home.
|
|
178
|
+
*
|
|
179
|
+
* Built after the listener because half of it is the port the kernel just
|
|
180
|
+
* chose. The relay has nowhere else to learn that from: `state` is opaque to
|
|
181
|
+
* the vendor, round-trips untouched, and is already checked on the way back,
|
|
182
|
+
* so carrying the port in it costs no extra parameter and no state held
|
|
183
|
+
* anywhere. Without a relay it stays what it always was.
|
|
184
|
+
*
|
|
185
|
+
* Safe to declare after the handler that reads it: `Bun.serve` returns
|
|
186
|
+
* synchronously and nothing can be served until this frame yields.
|
|
187
|
+
*/
|
|
188
|
+
const state = options.relayRedirect
|
|
189
|
+
? `${base64url(randomBytes(24))}.${server.port}`
|
|
190
|
+
: base64url(randomBytes(24));
|
|
164
191
|
|
|
165
192
|
try {
|
|
166
193
|
const authorizeUrl = new URL(options.authorizeUrl);
|
|
@@ -196,159 +223,22 @@ export async function runOAuthFlow(options: OAuthFlowOptions): Promise<OAuthToke
|
|
|
196
223
|
}
|
|
197
224
|
}
|
|
198
225
|
|
|
199
|
-
/**
|
|
200
|
-
* Close the listener without cutting off the response in flight.
|
|
201
|
-
*
|
|
202
|
-
* Forcing the socket shut the instant the code is read leaves the operator
|
|
203
|
-
* looking at a connection error on a flow that in fact succeeded. So: stop
|
|
204
|
-
* gracefully, wait for the in-flight callback to drain, and force only if
|
|
205
|
-
* graceful genuinely did not finish — a keep-alive connection must not be able
|
|
206
|
-
* to hold the CLI open forever either.
|
|
207
|
-
*/
|
|
208
|
-
async function shutdown(server: { stop(force?: boolean): Promise<void> }): Promise<void> {
|
|
209
|
-
let drained = false;
|
|
210
|
-
const graceful = server.stop().then(() => {
|
|
211
|
-
drained = true;
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
await withTimeout(graceful, 2_000).catch(() => {});
|
|
215
|
-
if (!drained) await server.stop(true);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Race a promise against a deadline, and **clear the timer either way**.
|
|
220
|
-
*
|
|
221
|
-
* A bare `Promise.race` with `setTimeout` leaks: when the real promise wins,
|
|
222
|
-
* the timer is still pending, and a pending timer keeps the event loop alive
|
|
223
|
-
* for its full duration. With a five-minute OAuth deadline that turns a
|
|
224
|
-
* finished `connect` into a terminal that prints "Next: lanes link start" and
|
|
225
|
-
* then sits there for five minutes — which is exactly what it did.
|
|
226
|
-
*
|
|
227
|
-
* Rejects with `OAuthError(message)` if given one, otherwise resolves to
|
|
228
|
-
* undefined at the deadline, which is what the shutdown path wants.
|
|
229
|
-
*/
|
|
230
|
-
async function withTimeout<T>(promise: Promise<T>, ms: number, message?: string): Promise<T> {
|
|
231
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
return await Promise.race([
|
|
235
|
-
promise,
|
|
236
|
-
new Promise<T>((resolve, reject) => {
|
|
237
|
-
timer = setTimeout(
|
|
238
|
-
() => (message ? reject(new OAuthError(message)) : resolve(undefined as T)),
|
|
239
|
-
ms,
|
|
240
|
-
);
|
|
241
|
-
}),
|
|
242
|
-
]);
|
|
243
|
-
} finally {
|
|
244
|
-
if (timer) clearTimeout(timer);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
226
|
function defaultExchange(options: OAuthFlowOptions): ExchangeCode {
|
|
249
|
-
|
|
227
|
+
// The secret is no longer part of this condition. A public client has none —
|
|
228
|
+
// its id ships in the manifest and PKCE is what protects the exchange — so
|
|
229
|
+
// requiring one here refused the shipped-client path before it sent anything.
|
|
230
|
+
if (!options.tokenUrl) {
|
|
250
231
|
throw new OAuthError(
|
|
251
|
-
'runOAuthFlow needs
|
|
232
|
+
'runOAuthFlow needs a tokenUrl to redeem the code at, or an exchange to redeem it through.',
|
|
252
233
|
);
|
|
253
234
|
}
|
|
254
235
|
return directExchange({
|
|
255
236
|
tokenUrl: options.tokenUrl,
|
|
256
237
|
clientId: options.clientId,
|
|
257
|
-
|
|
238
|
+
refreshToken: options.refreshToken,
|
|
239
|
+
...(options.clientSecret ? { clientSecret: options.clientSecret } : {}),
|
|
258
240
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
259
241
|
});
|
|
260
242
|
}
|
|
261
243
|
|
|
262
244
|
|
|
263
|
-
/**
|
|
264
|
-
* Capture exactly one OAuth callback on loopback.
|
|
265
|
-
*
|
|
266
|
-
* Split out from `runOAuthFlow` so the SDK can drive the protocol — discovery,
|
|
267
|
-
* registration, PKCE, exchange — while we supply only the redirect target. The
|
|
268
|
-
* listener exists for the duration of one consent and no longer.
|
|
269
|
-
*/
|
|
270
|
-
export interface CallbackCapture {
|
|
271
|
-
readonly redirectUri: string;
|
|
272
|
-
/**
|
|
273
|
-
* The `state` this listener will accept, for the provider to put on the
|
|
274
|
-
* authorization URL. The SDK asks its provider for one and sends whatever it
|
|
275
|
-
* gets; nothing generates it for us.
|
|
276
|
-
*/
|
|
277
|
-
readonly state: string;
|
|
278
|
-
wait(timeoutMs?: number): Promise<{ code: string; iss?: string }>;
|
|
279
|
-
close(): Promise<void>;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export function captureOAuthCallback(options: { label?: string } = {}): CallbackCapture {
|
|
283
|
-
const state = base64url(randomBytes(24));
|
|
284
|
-
let resolveCode: (value: { code: string; iss?: string }) => void;
|
|
285
|
-
let rejectCode: (error: Error) => void;
|
|
286
|
-
|
|
287
|
-
const received = new Promise<{ code: string; iss?: string }>((resolve, reject) => {
|
|
288
|
-
resolveCode = resolve;
|
|
289
|
-
rejectCode = reject;
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
const server = Bun.serve({
|
|
293
|
-
hostname: '127.0.0.1',
|
|
294
|
-
port: 0,
|
|
295
|
-
fetch(request) {
|
|
296
|
-
const url = new URL(request.url);
|
|
297
|
-
if (url.pathname !== '/callback') return new Response('Not found', { status: 404 });
|
|
298
|
-
|
|
299
|
-
const error = url.searchParams.get('error');
|
|
300
|
-
if (error) {
|
|
301
|
-
rejectCode(
|
|
302
|
-
new OAuthError(
|
|
303
|
-
error === 'access_denied'
|
|
304
|
-
? 'Authorization was declined in the browser.'
|
|
305
|
-
: `Authorization failed: ${error}`,
|
|
306
|
-
),
|
|
307
|
-
);
|
|
308
|
-
return failedPage('You can close this tab.');
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const code = url.searchParams.get('code');
|
|
312
|
-
if (!code) {
|
|
313
|
-
rejectCode(new OAuthError('The callback carried no authorization code.'));
|
|
314
|
-
return failedPage('No code returned.');
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Checked here because nothing else checks it. The SDK's own docs say it
|
|
318
|
-
// does not validate `state`, and it only sends one if the provider hands
|
|
319
|
-
// it over — so this listener mints the value, `CredentialOAuthProvider`
|
|
320
|
-
// returns it from `state()`, and the two are compared on the way back.
|
|
321
|
-
//
|
|
322
|
-
// Without it this resolves on the first `/callback?code=` to reach the
|
|
323
|
-
// port, whoever sent it: any local process, or a page the operator has
|
|
324
|
-
// open, can sweep loopback during the five-minute window. PKCE usually
|
|
325
|
-
// turns that into a failed exchange, but a manifest is free to name an
|
|
326
|
-
// authorization server that does not enforce it, and there the code
|
|
327
|
-
// would be redeemed and the connection bound to someone else's account.
|
|
328
|
-
const returnedState = url.searchParams.get('state');
|
|
329
|
-
if (!returnedState || !stateMatches(state, returnedState)) {
|
|
330
|
-
rejectCode(new OAuthError('State mismatch — ignoring an unexpected callback.'));
|
|
331
|
-
return failedPage('Unexpected callback.');
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const iss = url.searchParams.get('iss');
|
|
335
|
-
resolveCode({ code, ...(iss ? { iss } : {}) });
|
|
336
|
-
return connectedPage(options.label);
|
|
337
|
-
},
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
return {
|
|
341
|
-
redirectUri: `http://127.0.0.1:${server.port}/callback`,
|
|
342
|
-
state,
|
|
343
|
-
|
|
344
|
-
async wait(timeoutMs = 5 * 60_000) {
|
|
345
|
-
return withTimeout(
|
|
346
|
-
received,
|
|
347
|
-
timeoutMs,
|
|
348
|
-
`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for the browser.`,
|
|
349
|
-
);
|
|
350
|
-
},
|
|
351
|
-
|
|
352
|
-
close: () => shutdown(server),
|
|
353
|
-
};
|
|
354
|
-
}
|
package/src/cli/output.ts
CHANGED
|
@@ -106,15 +106,31 @@ export async function waiting<T>(label: string, work: () => Promise<T>): Promise
|
|
|
106
106
|
* The line every command prints before acting, read-only commands included.
|
|
107
107
|
*
|
|
108
108
|
* This is the primary guard against operating on the wrong instance, and it
|
|
109
|
-
* costs one line. It
|
|
110
|
-
*
|
|
111
|
-
*
|
|
109
|
+
* costs one line. It used to name where each value came from, which mattered
|
|
110
|
+
* while four things could supply them. Only the command line can now (ADR-037),
|
|
111
|
+
* so the parenthetical would say `(flag)` twice on every line forever — which
|
|
112
|
+
* `target.ts` already argues is how a line stops being read.
|
|
112
113
|
*/
|
|
114
|
+
/**
|
|
115
|
+
* The same line for a command that opens no target.
|
|
116
|
+
*
|
|
117
|
+
* `identity list` reads a block declared once in the YAML, so it takes no
|
|
118
|
+
* `--target` (ADR-037) and there is none to name. Printing `target undefined`
|
|
119
|
+
* or omitting the line entirely were the alternatives; the first is a lie and
|
|
120
|
+
* the second loses the guard that this line exists to be.
|
|
121
|
+
*/
|
|
122
|
+
export function announceProfile(selection: {
|
|
123
|
+
readonly profile: string;
|
|
124
|
+
readonly workspaceRoot: string;
|
|
125
|
+
}): void {
|
|
126
|
+
print(style.dim(`profile ${style.bold(selection.profile)} ${selection.workspaceRoot}`));
|
|
127
|
+
}
|
|
128
|
+
|
|
113
129
|
export function announce(resolution: Resolution): void {
|
|
114
130
|
print(
|
|
115
131
|
style.dim(
|
|
116
|
-
`profile ${style.bold(resolution.profile)}
|
|
117
|
-
`target ${style.bold(resolution.target)}
|
|
132
|
+
`profile ${style.bold(resolution.profile)} ` +
|
|
133
|
+
`target ${style.bold(resolution.target)} ` +
|
|
118
134
|
`${resolution.workspaceRoot}`,
|
|
119
135
|
),
|
|
120
136
|
);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A brand mark per provider, drawn rather than fetched.
|
|
3
|
+
*
|
|
4
|
+
* Vendored from Simple Icons 16.28.0, which dedicates its paths to the public
|
|
5
|
+
* domain under CC0-1.0. **The trademarks themselves remain their owners'** —
|
|
6
|
+
* these identify the service a connection reaches, which is what nominative use
|
|
7
|
+
* is for, and nothing here implies that any of them endorses this.
|
|
8
|
+
*
|
|
9
|
+
* Inlined because there is nowhere else to put them. The page is served under a
|
|
10
|
+
* strict `default-src 'none'`, so it fetches nothing; the alternative — a CDN —
|
|
11
|
+
* would tell a third party which dashboard the owner is looking at, every time
|
|
12
|
+
* they look. There is also no static-asset pipeline to serve them from.
|
|
13
|
+
*
|
|
14
|
+
* Every path is one shape on a `0 0 24 24` canvas, filled with `currentColor`,
|
|
15
|
+
* so a mark inherits the page's text colour and is correct in either theme
|
|
16
|
+
* without a second copy. That is also why these are monochrome rather than the
|
|
17
|
+
* full-colour originals: `callback-page.ts` reserves colour for status, and a
|
|
18
|
+
* row of brand hues would spend it on decoration.
|
|
19
|
+
*
|
|
20
|
+
* Ids sharing a mark do so deliberately — `gmail` and `gmail_mcp` reach the same
|
|
21
|
+
* mailbox by different transports, and the four `icloud_*` providers are one
|
|
22
|
+
* service. The mark names the vendor; the name beside it names the product.
|
|
23
|
+
*
|
|
24
|
+
* Absent here, `dashboard-page.ts` falls back to a monogram. That covers every
|
|
25
|
+
* provider an operator writes themselves, and the two shipped ones Simple Icons
|
|
26
|
+
* does not carry: Slack, whose mark was withdrawn at the brand's request, and
|
|
27
|
+
* Google Contacts, which it never had.
|
|
28
|
+
*/
|
|
29
|
+
export const PROVIDER_MARKS: Readonly<Record<string, string>> = {
|
|
30
|
+
calendar: 'M18.316 5.684H24v12.632h-5.684V5.684zM5.684 24h12.632v-5.684H5.684V24zM18.316 5.684V0H1.895A1.894 1.894 0 0 0 0 1.895v16.421h5.684V5.684h12.632zm-7.207 6.25v-.065c.272-.144.5-.349.687-.617s.279-.595.279-.982c0-.379-.099-.72-.3-1.025a2.05 2.05 0 0 0-.832-.714 2.703 2.703 0 0 0-1.197-.257c-.6 0-1.094.156-1.481.467-.386.311-.65.671-.793 1.078l1.085.452c.086-.249.224-.461.413-.633.189-.172.445-.257.767-.257.33 0 .602.088.816.264a.86.86 0 0 1 .322.703c0 .33-.12.589-.36.778-.24.19-.535.284-.886.284h-.567v1.085h.633c.407 0 .748.109 1.02.327.272.218.407.499.407.843 0 .336-.129.614-.387.832s-.565.327-.924.327c-.351 0-.651-.103-.897-.311-.248-.208-.422-.502-.521-.881l-1.096.452c.178.616.505 1.082.977 1.401.472.319.984.478 1.538.477a2.84 2.84 0 0 0 1.293-.291c.382-.193.684-.458.902-.794.218-.336.327-.72.327-1.149 0-.429-.115-.797-.344-1.105a2.067 2.067 0 0 0-.881-.689zm2.093-1.931l.602.913L15 10.045v5.744h1.187V8.446h-.827l-2.158 1.557zM22.105 0h-3.289v5.184H24V1.895A1.894 1.894 0 0 0 22.105 0zm-3.289 23.5l4.684-4.684h-4.684V23.5zM0 22.105C0 23.152.848 24 1.895 24h3.289v-5.184H0v3.289z',
|
|
31
|
+
docs: 'M14.727 6.727H14V0H4.91c-.905 0-1.637.732-1.637 1.636v20.728c0 .904.732 1.636 1.636 1.636h14.182c.904 0 1.636-.732 1.636-1.636V6.727h-6zm-.545 10.455H7.09v-1.364h7.09v1.364zm2.727-3.273H7.091v-1.364h9.818v1.364zm0-3.273H7.091V9.273h9.818v1.363zM14.727 6h6l-6-6v6z',
|
|
32
|
+
drive: 'M12.01 1.485c-2.082 0-3.754.02-3.743.047.01.02 1.708 3.001 3.774 6.62l3.76 6.574h3.76c2.081 0 3.753-.02 3.742-.047-.005-.02-1.708-3.001-3.775-6.62l-3.76-6.574zm-4.76 1.73a789.828 789.861 0 0 0-3.63 6.319L0 15.868l1.89 3.298 1.885 3.297 3.62-6.335 3.618-6.33-1.88-3.287C8.1 4.704 7.255 3.22 7.25 3.214zm2.259 12.653-.203.348c-.114.198-.96 1.672-1.88 3.287a423.93 423.948 0 0 1-1.698 2.97c-.01.026 3.24.042 7.222.042h7.244l1.796-3.157c.992-1.734 1.85-3.23 1.906-3.323l.104-.167h-7.249z',
|
|
33
|
+
drive_mcp: 'M12.01 1.485c-2.082 0-3.754.02-3.743.047.01.02 1.708 3.001 3.774 6.62l3.76 6.574h3.76c2.081 0 3.753-.02 3.742-.047-.005-.02-1.708-3.001-3.775-6.62l-3.76-6.574zm-4.76 1.73a789.828 789.861 0 0 0-3.63 6.319L0 15.868l1.89 3.298 1.885 3.297 3.62-6.335 3.618-6.33-1.88-3.287C8.1 4.704 7.255 3.22 7.25 3.214zm2.259 12.653-.203.348c-.114.198-.96 1.672-1.88 3.287a423.93 423.948 0 0 1-1.698 2.97c-.01.026 3.24.042 7.222.042h7.244l1.796-3.157c.992-1.734 1.85-3.23 1.906-3.323l.104-.167h-7.249z',
|
|
34
|
+
github: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
|
|
35
|
+
gmail: 'M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z',
|
|
36
|
+
gmail_mcp: 'M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z',
|
|
37
|
+
icloud_calendar: 'M13.762 4.29a6.51 6.51 0 0 0-5.669 3.332 3.571 3.571 0 0 0-1.558-.36 3.571 3.571 0 0 0-3.516 3A4.918 4.918 0 0 0 0 14.796a4.918 4.918 0 0 0 4.92 4.914 4.93 4.93 0 0 0 .617-.045h14.42c2.305-.272 4.041-2.258 4.043-4.589v-.009a4.594 4.594 0 0 0-3.727-4.508 6.51 6.51 0 0 0-6.511-6.27z',
|
|
38
|
+
icloud_contacts: 'M13.762 4.29a6.51 6.51 0 0 0-5.669 3.332 3.571 3.571 0 0 0-1.558-.36 3.571 3.571 0 0 0-3.516 3A4.918 4.918 0 0 0 0 14.796a4.918 4.918 0 0 0 4.92 4.914 4.93 4.93 0 0 0 .617-.045h14.42c2.305-.272 4.041-2.258 4.043-4.589v-.009a4.594 4.594 0 0 0-3.727-4.508 6.51 6.51 0 0 0-6.511-6.27z',
|
|
39
|
+
icloud_drive: 'M13.762 4.29a6.51 6.51 0 0 0-5.669 3.332 3.571 3.571 0 0 0-1.558-.36 3.571 3.571 0 0 0-3.516 3A4.918 4.918 0 0 0 0 14.796a4.918 4.918 0 0 0 4.92 4.914 4.93 4.93 0 0 0 .617-.045h14.42c2.305-.272 4.041-2.258 4.043-4.589v-.009a4.594 4.594 0 0 0-3.727-4.508 6.51 6.51 0 0 0-6.511-6.27z',
|
|
40
|
+
icloud_mail: 'M13.762 4.29a6.51 6.51 0 0 0-5.669 3.332 3.571 3.571 0 0 0-1.558-.36 3.571 3.571 0 0 0-3.516 3A4.918 4.918 0 0 0 0 14.796a4.918 4.918 0 0 0 4.92 4.914 4.93 4.93 0 0 0 .617-.045h14.42c2.305-.272 4.041-2.258 4.043-4.589v-.009a4.594 4.594 0 0 0-3.727-4.508 6.51 6.51 0 0 0-6.511-6.27z',
|
|
41
|
+
linear: 'M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z',
|
|
42
|
+
notion: 'M4.459 4.208c.746.606 1.026.56 2.428.466l13.215-.793c.28 0 .047-.28-.046-.326L17.86 1.968c-.42-.326-.981-.7-2.055-.607L3.01 2.295c-.466.046-.56.28-.374.466zm.793 3.08v13.904c0 .747.373 1.027 1.214.98l14.523-.84c.841-.046.935-.56.935-1.167V6.354c0-.606-.233-.933-.748-.887l-15.177.887c-.56.047-.747.327-.747.933zm14.337.745c.093.42 0 .84-.42.888l-.7.14v10.264c-.608.327-1.168.514-1.635.514-.748 0-.935-.234-1.495-.933l-4.577-7.186v6.952L12.21 19s0 .84-1.168.84l-3.222.186c-.093-.186 0-.653.327-.746l.84-.233V9.854L7.822 9.76c-.094-.42.14-1.026.793-1.073l3.456-.233 4.764 7.279v-6.44l-1.215-.139c-.093-.514.28-.887.747-.933zM1.936 1.035l13.31-.98c1.634-.14 2.055-.047 3.082.7l4.249 2.986c.7.513.934.653.934 1.213v16.378c0 1.026-.373 1.634-1.68 1.726l-15.458.934c-.98.047-1.448-.093-1.962-.747l-3.129-4.06c-.56-.747-.793-1.306-.793-1.96V2.667c0-.839.374-1.54 1.447-1.632z',
|
|
43
|
+
sheets: 'M11.318 12.545H7.91v-1.909h3.41v1.91zM14.728 0v6h6l-6-6zm1.363 10.636h-3.41v1.91h3.41v-1.91zm0 3.273h-3.41v1.91h3.41v-1.91zM20.727 6.5v15.864c0 .904-.732 1.636-1.636 1.636H4.909a1.636 1.636 0 0 1-1.636-1.636V1.636C3.273.732 4.005 0 4.909 0h9.318v6.5h6.5zm-3.273 2.773H6.545v7.909h10.91v-7.91zm-6.136 4.636H7.91v1.91h3.41v-1.91z',
|
|
44
|
+
tasks: 'M11.383.617C5.097.617 0 5.714 0 12c0 6.286 5.097 11.383 11.383 11.383 6.286 0 11.38-5.097 11.38-11.383a11.34 11.34 0 0 0-.878-4.389l-3.203 3.203c.062.387.1.782.1 1.186a7.398 7.398 0 1 1-7.4-7.398c1.499 0 2.889.448 4.054 1.214l2.857-2.857a11.325 11.325 0 0 0-6.91-2.342zm9.674.756c-.292 0-.583.112-.805.334-2.97 2.965-5.934 5.934-8.9 8.902L9.596 8.854a1.139 1.139 0 0 0-1.61 0l-1.775 1.773a1.139 1.139 0 0 0 0 1.61l4.166 4.163a1.421 1.421 0 0 0 2.012 0L23.666 5.121a1.136 1.136 0 0 0 0-1.61l-1.805-1.804a1.136 1.136 0 0 0-.804-.334z',
|
|
45
|
+
};
|
package/src/cli/runtime/open.ts
CHANGED
|
@@ -6,9 +6,10 @@ import type { BlobStore } from '#stores/blobs';
|
|
|
6
6
|
import type { AnyConnector, ProviderManifest } from '#connectivity';
|
|
7
7
|
import { RateLimiter, allowedConnections } from '#policy';
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
KNOWLEDGE_LAYOUT,
|
|
10
10
|
layout,
|
|
11
11
|
listProfiles,
|
|
12
|
+
undeclaredTarget,
|
|
12
13
|
workspacePath,
|
|
13
14
|
type Config,
|
|
14
15
|
type Resolution,
|
|
@@ -16,12 +17,7 @@ import {
|
|
|
16
17
|
import { ProviderRegistry, toPolicyDocument } from '#registry';
|
|
17
18
|
import { Dispatcher, createConsoleLogger } from '#dispatch';
|
|
18
19
|
import { PROVIDER_MANIFESTS } from '#providers/index.ts';
|
|
19
|
-
import {
|
|
20
|
-
createBlobVaultStore,
|
|
21
|
-
createFileVaultStore,
|
|
22
|
-
createSecretVaultStore,
|
|
23
|
-
type VaultStore,
|
|
24
|
-
} from '#providers/owner.ts';
|
|
20
|
+
import type { VaultStore } from '#providers/owner.ts';
|
|
25
21
|
import {
|
|
26
22
|
openAuditSinks,
|
|
27
23
|
openState,
|
|
@@ -30,11 +26,14 @@ import {
|
|
|
30
26
|
type StorageFactory,
|
|
31
27
|
type TargetInput,
|
|
32
28
|
} from '#deployments/target.ts';
|
|
29
|
+
import { openKnowledge, type FetchLike, type KnowledgeStores } from '#deployments/knowledge.ts';
|
|
30
|
+
import { routeBlobStore } from '#stores/blobs/route.ts';
|
|
33
31
|
import { connectorFactory } from '#connectivity/transports';
|
|
34
32
|
import { requestAuthorizer } from '#connectivity/auth/index.ts';
|
|
35
33
|
import { resolveProfile, type GlobalFlags } from './select.ts';
|
|
36
|
-
import { buildRegistryWithWorkspace,
|
|
34
|
+
import { buildRegistryWithWorkspace, readSkillsForStart, reloadSkills } from './registry.ts';
|
|
37
35
|
import { discoveryProbe } from './discovery.ts';
|
|
36
|
+
import { openVault } from './vault.ts';
|
|
38
37
|
|
|
39
38
|
/**
|
|
40
39
|
* Assembling a profile's runtime from its declared target.
|
|
@@ -56,6 +55,16 @@ export interface Runtime {
|
|
|
56
55
|
readonly storage: BlobStore;
|
|
57
56
|
/** Where skills are kept — `data/<profile>/skills.d/` locally. */
|
|
58
57
|
readonly skills: BlobStore;
|
|
58
|
+
/**
|
|
59
|
+
* Present only when this target keeps memory and skills in a repository.
|
|
60
|
+
*
|
|
61
|
+
* Nothing on the dispatch path reads it: `storage` and `skills` above are
|
|
62
|
+
* already pointed at the right bytes, which is the whole design. It is here
|
|
63
|
+
* so `target show` and `doctor` can say where those bytes are without
|
|
64
|
+
* re-reading the config, and so the migration can reach the repository it is
|
|
65
|
+
* committing to.
|
|
66
|
+
*/
|
|
67
|
+
readonly knowledge?: KnowledgeStores | undefined;
|
|
59
68
|
/** The vault's own store, so `lanes link vault` reaches the same bytes MCP does. */
|
|
60
69
|
readonly vault: VaultStore;
|
|
61
70
|
readonly registry: ProviderRegistry;
|
|
@@ -99,50 +108,29 @@ function skillStore(storage: StorageFactory, profile: string): BlobStore {
|
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
/**
|
|
102
|
-
*
|
|
111
|
+
* What a caller may hand the runtime that is not a flag somebody typed.
|
|
103
112
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* container filesystem, and every item in it was discarded by the next
|
|
108
|
-
* revision without an error to say so.
|
|
113
|
+
* One field, and it exists for the same reason `connect`'s does: the only thing
|
|
114
|
+
* a runtime reaches over the network at open time is a knowledge repository,
|
|
115
|
+
* and a test that could not replace it would either hit github.com or not run.
|
|
109
116
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
credentials: SecretStore,
|
|
114
|
-
): VaultStore {
|
|
115
|
-
const { declared, config, root } = input;
|
|
116
|
-
const vault = declared.vault ?? { adapter: 'file' as const };
|
|
117
|
-
|
|
118
|
-
switch (vault.adapter) {
|
|
119
|
-
case 'file':
|
|
120
|
-
return createFileVaultStore({
|
|
121
|
-
path: workspacePath(root, vault.path ?? layout.vault(config.instance.profile)),
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
case 'secret':
|
|
125
|
-
// The document is sealed under LANES_LINK_VAULT_KEY before it gets here,
|
|
126
|
-
// so the credential store holds ciphertext it cannot read. Separate
|
|
127
|
-
// document, separate key, separate environment variable — the backend
|
|
128
|
-
// was never what kept the two stores apart. ADR-022.
|
|
129
|
-
return createSecretVaultStore({
|
|
130
|
-
store: credentials,
|
|
131
|
-
...(vault.ref !== undefined ? { ref: vault.ref } : {}),
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
case 'blob':
|
|
135
|
-
return createBlobVaultStore({
|
|
136
|
-
store: storage(),
|
|
137
|
-
...(vault.path !== undefined ? { key: vault.path } : {}),
|
|
138
|
-
});
|
|
139
|
-
}
|
|
117
|
+
export interface OpenOptions {
|
|
118
|
+
/** Injected for tests. */
|
|
119
|
+
readonly fetch?: FetchLike | undefined;
|
|
140
120
|
}
|
|
141
121
|
|
|
142
|
-
export async function openRuntime(
|
|
122
|
+
export async function openRuntime(
|
|
123
|
+
flags: GlobalFlags,
|
|
124
|
+
options: OpenOptions = {},
|
|
125
|
+
): Promise<Runtime> {
|
|
143
126
|
const { resolution, config, target } = await resolveProfile(flags);
|
|
144
127
|
const declared = config.targets[target];
|
|
145
|
-
|
|
128
|
+
// `resolveProfile` has already checked this, so reaching it means a caller
|
|
129
|
+
// passed `allowUndeclaredTarget`. Through the shared refusal all the same:
|
|
130
|
+
// this was a fourth spelling of that sentence with the profile name and the
|
|
131
|
+
// list of what exists both missing, and the comment on `undeclaredTarget`
|
|
132
|
+
// says why one copy is the most that survives.
|
|
133
|
+
if (!declared) throw undeclaredTarget(target, config, resolution.profile);
|
|
146
134
|
|
|
147
135
|
const root = resolution.workspaceRoot;
|
|
148
136
|
const adapters: TargetInput = { declared, config, root, target };
|
|
@@ -154,7 +142,23 @@ export async function openRuntime(flags: GlobalFlags): Promise<Runtime> {
|
|
|
154
142
|
// no schema left to migrate.
|
|
155
143
|
const credentials = await openSecrets(adapters);
|
|
156
144
|
const storageFor = await openStorage(adapters, credentials);
|
|
157
|
-
|
|
145
|
+
|
|
146
|
+
// Memory and skills, where a profile keeps them somewhere else (ADR-041).
|
|
147
|
+
//
|
|
148
|
+
// The redirection happens *here*, on the store every consumer was already
|
|
149
|
+
// handed, and that is deliberate. Memory is not addressed by a name anything
|
|
150
|
+
// declares: `buildProviderContext` scopes the profile's blob root to
|
|
151
|
+
// `memory/<connection>`, and `lanes link memory` reaches the same bytes by
|
|
152
|
+
// calling the same two functions. So the dispatcher, the provider, and the
|
|
153
|
+
// CLI need no knowledge of this and cannot disagree about where an entry
|
|
154
|
+
// went — the one thing they are handed already points at the repository.
|
|
155
|
+
//
|
|
156
|
+
// The audit log, `state.kv`, the credential store and the vault keep their
|
|
157
|
+
// own roots on the target's own storage and are untouched.
|
|
158
|
+
const knowledge = await openKnowledge(adapters, credentials, options.fetch);
|
|
159
|
+
const storage = knowledge
|
|
160
|
+
? routeBlobStore(storageFor(), [{ prefix: `${KNOWLEDGE_LAYOUT.memory}/`, store: knowledge.memory }])
|
|
161
|
+
: storageFor();
|
|
158
162
|
const state = openState(storageFor, config.instance.profile);
|
|
159
163
|
|
|
160
164
|
// The durable log, plus any copies the target declares. `sink` is what
|
|
@@ -168,7 +172,7 @@ export async function openRuntime(flags: GlobalFlags): Promise<Runtime> {
|
|
|
168
172
|
(message) => logger.warn(message),
|
|
169
173
|
);
|
|
170
174
|
|
|
171
|
-
const skills = skillStore(storageFor, config.instance.profile);
|
|
175
|
+
const skills = knowledge?.skills ?? skillStore(storageFor, config.instance.profile);
|
|
172
176
|
|
|
173
177
|
// The vault's own store, beside the credential store and never it: a separate
|
|
174
178
|
// document, a separate key, and a separate environment variable
|
|
@@ -228,21 +232,39 @@ export async function openRuntime(flags: GlobalFlags): Promise<Runtime> {
|
|
|
228
232
|
account: connection.account,
|
|
229
233
|
}));
|
|
230
234
|
|
|
235
|
+
// Read before the registry is built, so a store that cannot be reached is a
|
|
236
|
+
// warning rather than the end of the process — see `readSkillsForStart`. Only
|
|
237
|
+
// tolerated when the store is a repository: a local directory that will not
|
|
238
|
+
// read is a real fault worth failing on, exactly as it always was.
|
|
239
|
+
const loaded = await readSkillsForStart(
|
|
240
|
+
skills,
|
|
241
|
+
knowledge !== undefined,
|
|
242
|
+
(message) => logger.warn(message),
|
|
243
|
+
` --profile ${config.instance.profile} --target ${target}`,
|
|
244
|
+
);
|
|
245
|
+
|
|
231
246
|
registry = await buildRegistryWithWorkspace(root, config.instance.profile, {
|
|
232
247
|
skillStore: skills,
|
|
248
|
+
skills: loaded.skills,
|
|
233
249
|
onSkillsChanged: refreshSkills,
|
|
234
250
|
vault: { store: vaultStore, items: await vaultStore.ids() },
|
|
235
251
|
setup: {
|
|
236
252
|
profile: config.instance.profile,
|
|
253
|
+
target,
|
|
237
254
|
profiles: await listProfiles(root),
|
|
238
255
|
catalogue: PROVIDER_MANIFESTS,
|
|
239
256
|
ownClients: Object.keys(config.oauth_apps),
|
|
240
257
|
reachable,
|
|
241
258
|
},
|
|
259
|
+
// Straight off the config: unlike `reachable`, nothing has to be filtered
|
|
260
|
+
// through policy first. Whether the surface is reachable at all is still
|
|
261
|
+
// policy's answer, given once by `mergeCapabilities` — this only decides
|
|
262
|
+
// what it says when it is.
|
|
263
|
+
identity: { profile: config.instance.profile, target, entries: config.identity },
|
|
242
264
|
});
|
|
243
|
-
// Seeded from the
|
|
244
|
-
//
|
|
245
|
-
fingerprint =
|
|
265
|
+
// Seeded from the read above, so the first refresh compares against what is
|
|
266
|
+
// registered rather than rebuilding once to find out.
|
|
267
|
+
fingerprint = loaded.fingerprint;
|
|
246
268
|
|
|
247
269
|
// Discovered capabilities never come from a live call on the dispatch path —
|
|
248
270
|
// that is what keeps the server stateless. But "not live" is not the same as
|
|
@@ -312,6 +334,7 @@ export async function openRuntime(flags: GlobalFlags): Promise<Runtime> {
|
|
|
312
334
|
credentials,
|
|
313
335
|
storage,
|
|
314
336
|
skills,
|
|
337
|
+
...(knowledge ? { knowledge } : {}),
|
|
315
338
|
vault: vaultStore,
|
|
316
339
|
registry,
|
|
317
340
|
refreshSkills,
|