@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/runtime/open.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { BearerAuthenticator, ownerPrincipal } from '#auth';
|
|
2
2
|
import type { SecretStore } from '#secrets';
|
|
3
3
|
import type { AuditReader } from '#audit';
|
|
4
|
-
import type
|
|
4
|
+
import { DISCOVERY_NAMESPACE, type RuntimeState } from '#stores/state';
|
|
5
5
|
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
|
+
assertGrantsResolve,
|
|
9
10
|
layout,
|
|
10
11
|
listProfiles,
|
|
12
|
+
readConnections,
|
|
13
|
+
selectConnections,
|
|
14
|
+
soleGrantFor,
|
|
11
15
|
workspacePath,
|
|
12
16
|
type Config,
|
|
17
|
+
type ConnectionConfig,
|
|
13
18
|
type Resolution,
|
|
19
|
+
type SelectedConnection,
|
|
14
20
|
type TargetConfig,
|
|
15
21
|
} from '#profile';
|
|
16
22
|
import { ProviderRegistry, toPolicyDocument } from '#registry';
|
|
@@ -33,6 +39,10 @@ import { resolveProfile, type GlobalFlags } from './select.ts';
|
|
|
33
39
|
import { buildRegistryWithWorkspace, readSkillsForStart, reloadSkills } from './registry.ts';
|
|
34
40
|
import { discoveryProbe } from './discovery.ts';
|
|
35
41
|
import { openVault } from './vault.ts';
|
|
42
|
+
import { EMPTY_SKILL_STORE, skillStore } from './stores.ts';
|
|
43
|
+
import type { Runtime } from './types.ts';
|
|
44
|
+
|
|
45
|
+
export type { Runtime } from './types.ts';
|
|
36
46
|
|
|
37
47
|
/**
|
|
38
48
|
* Assembling a profile's runtime from its declared target.
|
|
@@ -43,86 +53,20 @@ import { openVault } from './vault.ts';
|
|
|
43
53
|
* nothing at the application layer.
|
|
44
54
|
*/
|
|
45
55
|
|
|
46
|
-
export interface Runtime {
|
|
47
|
-
readonly resolution: Resolution;
|
|
48
|
-
readonly config: Config;
|
|
49
|
-
readonly target: string;
|
|
50
|
-
/**
|
|
51
|
-
* The target's adapter set, from the workspace that declares it.
|
|
52
|
-
*
|
|
53
|
-
* Here because the config no longer carries it. Every caller that used to
|
|
54
|
-
* reach `config.targets[target]` — for a `deploy` block, a storage adapter, a
|
|
55
|
-
* knowledge repository — reads this instead, and reads it without following
|
|
56
|
-
* the pointer a second time (ADR-052).
|
|
57
|
-
*/
|
|
58
|
-
readonly declared: TargetConfig;
|
|
59
|
-
readonly state: RuntimeState;
|
|
60
|
-
/** The durable log, for reading. Copies, if any, are write-only and not here. */
|
|
61
|
-
readonly audit: AuditReader;
|
|
62
|
-
readonly credentials: SecretStore;
|
|
63
|
-
readonly storage: BlobStore;
|
|
64
|
-
/** Where skills are kept — `data/<profile>/skills.d/` locally. */
|
|
65
|
-
readonly skills: BlobStore;
|
|
66
|
-
/**
|
|
67
|
-
* Present only when this target keeps memory and skills in a repository.
|
|
68
|
-
*
|
|
69
|
-
* Nothing on the dispatch path reads it: `storage` and `skills` above are
|
|
70
|
-
* already pointed at the right bytes, which is the whole design. It is here
|
|
71
|
-
* so `target show` and `doctor` can say where those bytes are without
|
|
72
|
-
* re-reading the config, and so the migration can reach the repository it is
|
|
73
|
-
* committing to.
|
|
74
|
-
*/
|
|
75
|
-
readonly knowledge?: KnowledgeStores | undefined;
|
|
76
|
-
/** The vault's own store, so `lanes link vault` reaches the same bytes MCP does. */
|
|
77
|
-
readonly vault: VaultStore;
|
|
78
|
-
readonly registry: ProviderRegistry;
|
|
79
|
-
/**
|
|
80
|
-
* Re-read the skills into the registry when they have changed on the store.
|
|
81
|
-
*
|
|
82
|
-
* Cheap and idempotent — one `list()`, and a rebuild only when the listing
|
|
83
|
-
* differs from the last one. The caller decides how often to ask.
|
|
84
|
-
*/
|
|
85
|
-
refreshSkills(): Promise<void>;
|
|
86
|
-
readonly dispatcher: Dispatcher;
|
|
87
|
-
readonly authenticator: BearerAuthenticator;
|
|
88
|
-
/** Same factory the dispatcher uses, exposed for commands that probe upstream. */
|
|
89
|
-
connectorFor(providerId: string, connectionId: string): AnyConnector | undefined;
|
|
90
|
-
/**
|
|
91
|
-
* Same authorizer the dispatcher uses, for the same reason `connectorFor` is here.
|
|
92
|
-
*
|
|
93
|
-
* A command that probes upstream needs the credential on the request, and it
|
|
94
|
-
* must not learn how to put it there — that is one switch on the resolved
|
|
95
|
-
* shape (`connectivity/auth/authorize.ts`) and a second copy would be a
|
|
96
|
-
* second answer. `settleIdentity` is the caller: it asks a provider whose
|
|
97
|
-
* account was just authorised, over whatever method that provider declares.
|
|
98
|
-
*/
|
|
99
|
-
authorizeRequest(providerId: string, connectionId: string, request: Request): Promise<Request>;
|
|
100
|
-
/** A provider's manifest, so an omitted `credential_ref` can be derived from it. */
|
|
101
|
-
manifestFor(providerId: string): ProviderManifest | undefined;
|
|
102
|
-
close(): Promise<void>;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
56
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* Locally `data/<profile>/skills.d/`; deployed, the same key under the bucket
|
|
109
|
-
* prefix. Going through the store rather than a filesystem path is what gives a
|
|
110
|
-
* deployment skills at all — a path is baked into a container image at build
|
|
111
|
-
* time and an object key is not, so before ADR-014 a deployed instance could
|
|
112
|
-
* only ever serve the skills that existed when its image was built.
|
|
57
|
+
* The accounts a profile reaches, as most callers want them.
|
|
113
58
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* An explicit area, matching `openState` and `openAudit` — a profile that
|
|
121
|
-
* declares its own `storage.path` moves its provider blobs, not the reserved
|
|
122
|
-
* roots beside them.
|
|
59
|
+
* `runtime.connections` pairs each with the grant governing it, which is what
|
|
60
|
+
* policy and the setup surface need. Everything else — probing credentials,
|
|
61
|
+
* listing accounts, reconciling state — wants the rows alone, and unwrapping
|
|
62
|
+
* them at twenty call sites is twenty chances to reach for
|
|
63
|
+
* `workspaceConnections` by mistake and answer for accounts this profile was
|
|
64
|
+
* never granted.
|
|
123
65
|
*/
|
|
124
|
-
function
|
|
125
|
-
|
|
66
|
+
export function grantedConnections(
|
|
67
|
+
runtime: Pick<Runtime, 'connections'>,
|
|
68
|
+
): ConnectionConfig[] {
|
|
69
|
+
return runtime.connections.map(({ connection }) => connection);
|
|
126
70
|
}
|
|
127
71
|
|
|
128
72
|
/**
|
|
@@ -152,6 +96,15 @@ export async function openRuntime(
|
|
|
152
96
|
const root = resolved.workspaceRoot;
|
|
153
97
|
const adapters: TargetInput = { declared, config, root, target };
|
|
154
98
|
|
|
99
|
+
// The workspace's accounts, and the check that this profile's grants name
|
|
100
|
+
// real ones (ADR-057). Read here rather than inside `resolveProfile` because
|
|
101
|
+
// it is a property of the workspace rather than of the selection, and because
|
|
102
|
+
// a command that only wants to know which profiles exist should not have to
|
|
103
|
+
// parse every connection to find out.
|
|
104
|
+
const connectionsFile = await readConnections(root);
|
|
105
|
+
assertGrantsResolve(config, connectionsFile.connections);
|
|
106
|
+
const selected = selectConnections(config, connectionsFile.connections);
|
|
107
|
+
|
|
155
108
|
// Credentials first: an S3 key pair is itself a credential reference, so the
|
|
156
109
|
// credential store has to exist before the blob store that names it. State
|
|
157
110
|
// and the log then ride that blob store rather than opening backends of
|
|
@@ -187,7 +140,18 @@ export async function openRuntime(
|
|
|
187
140
|
(message) => logger.warn(message),
|
|
188
141
|
);
|
|
189
142
|
|
|
190
|
-
|
|
143
|
+
// Rooted at the skills connection this profile grants, not at the profile
|
|
144
|
+
// (ADR-059). At most one can be granted, which is what makes this a lookup
|
|
145
|
+
// rather than a choice — `load.ts` refuses a second one, for the reason a
|
|
146
|
+
// prompt has no argument to route on.
|
|
147
|
+
//
|
|
148
|
+
// `EMPTY_SKILLS` when none is granted: a profile that denies `skills.*` has
|
|
149
|
+
// no store to open, and handing it the workspace root instead would serve
|
|
150
|
+
// every other profile's procedures.
|
|
151
|
+
const skillsConnection = soleGrantFor(config, 'lanes_skills');
|
|
152
|
+
const skills =
|
|
153
|
+
knowledge?.skills ??
|
|
154
|
+
(skillsConnection === undefined ? undefined : skillStore(storageFor, config.instance.profile, skillsConnection));
|
|
191
155
|
|
|
192
156
|
// The vault's own store, beside the credential store and never it: a separate
|
|
193
157
|
// document, a separate key, and a separate environment variable
|
|
@@ -208,6 +172,7 @@ export async function openRuntime(
|
|
|
208
172
|
let registry: ProviderRegistry;
|
|
209
173
|
let fingerprint = '';
|
|
210
174
|
const refreshSkills = async (): Promise<void> => {
|
|
175
|
+
if (skills === undefined) return;
|
|
211
176
|
fingerprint = await reloadSkills(registry, skills, fingerprint, refreshSkills);
|
|
212
177
|
};
|
|
213
178
|
|
|
@@ -226,23 +191,18 @@ export async function openRuntime(
|
|
|
226
191
|
// registered into. Per call rather than a snapshot, so a policy the runtime
|
|
227
192
|
// was opened with is re-evaluated rather than remembered.
|
|
228
193
|
const reachable = (): ReadonlyArray<{ key: string; provider: string; account: string }> =>
|
|
229
|
-
|
|
230
|
-
.filter((connection) =>
|
|
194
|
+
selected
|
|
195
|
+
.filter(({ ref, connection }) =>
|
|
231
196
|
registry
|
|
232
197
|
.capabilities()
|
|
233
198
|
.some(
|
|
234
199
|
({ id }) =>
|
|
235
200
|
id.startsWith(`${connection.provider}.`) &&
|
|
236
|
-
allowedConnections(
|
|
237
|
-
id,
|
|
238
|
-
[`${connection.provider}.${connection.id}`],
|
|
239
|
-
principal,
|
|
240
|
-
policy,
|
|
241
|
-
).length > 0,
|
|
201
|
+
allowedConnections(id, [ref], principal, policy).length > 0,
|
|
242
202
|
),
|
|
243
203
|
)
|
|
244
|
-
.map((connection) => ({
|
|
245
|
-
key:
|
|
204
|
+
.map(({ ref, connection }) => ({
|
|
205
|
+
key: ref,
|
|
246
206
|
provider: connection.provider,
|
|
247
207
|
account: connection.account,
|
|
248
208
|
...(connection.label ? { label: connection.label } : {}),
|
|
@@ -253,14 +213,14 @@ export async function openRuntime(
|
|
|
253
213
|
// tolerated when the store is a repository: a local directory that will not
|
|
254
214
|
// read is a real fault worth failing on, exactly as it always was.
|
|
255
215
|
const loaded = await readSkillsForStart(
|
|
256
|
-
skills,
|
|
216
|
+
skills ?? EMPTY_SKILL_STORE,
|
|
257
217
|
knowledge !== undefined,
|
|
258
218
|
(message) => logger.warn(message),
|
|
259
|
-
` --profile ${config.instance.profile} --
|
|
219
|
+
` --profile ${config.instance.profile} --workspace ${target}`,
|
|
260
220
|
);
|
|
261
221
|
|
|
262
|
-
registry = await buildRegistryWithWorkspace(root,
|
|
263
|
-
skillStore: skills,
|
|
222
|
+
registry = await buildRegistryWithWorkspace(root, {
|
|
223
|
+
...(skills ? { skillStore: skills } : {}),
|
|
264
224
|
skills: loaded.skills,
|
|
265
225
|
onSkillsChanged: refreshSkills,
|
|
266
226
|
vault: { store: vaultStore, items: await vaultStore.ids() },
|
|
@@ -269,7 +229,7 @@ export async function openRuntime(
|
|
|
269
229
|
target,
|
|
270
230
|
profiles: await listProfiles(root),
|
|
271
231
|
catalogue: PROVIDER_MANIFESTS,
|
|
272
|
-
ownClients: Object.keys(
|
|
232
|
+
ownClients: Object.keys(connectionsFile.oauth_apps),
|
|
273
233
|
reachable,
|
|
274
234
|
},
|
|
275
235
|
// Straight off the config: unlike `reachable`, nothing has to be filtered
|
|
@@ -309,7 +269,7 @@ export async function openRuntime(
|
|
|
309
269
|
}
|
|
310
270
|
}
|
|
311
271
|
|
|
312
|
-
const cached = await state.kv.get(
|
|
272
|
+
const cached = await state.kv.get(DISCOVERY_NAMESPACE, entry.manifest.id);
|
|
313
273
|
if (cached) {
|
|
314
274
|
try {
|
|
315
275
|
registry.setDiscovered(entry.manifest.id, JSON.parse(cached));
|
|
@@ -329,14 +289,16 @@ export async function openRuntime(
|
|
|
329
289
|
credentials,
|
|
330
290
|
// From the connection row, which is where a per-connection setting has
|
|
331
291
|
// always lived. One lookup behind both, so they cannot disagree.
|
|
332
|
-
connectionConfig: (provider, id) => row(
|
|
333
|
-
isDeclared: (provider, id) => row(
|
|
292
|
+
connectionConfig: (provider, id) => row(connectionsFile.connections, provider, id)?.config,
|
|
293
|
+
isDeclared: (provider, id) => row(connectionsFile.connections, provider, id) !== undefined,
|
|
334
294
|
});
|
|
335
295
|
const authorizeRequest = requestAuthorizer(registry, credentials);
|
|
336
296
|
let closed = false;
|
|
337
297
|
|
|
338
298
|
const dispatcher = new Dispatcher({
|
|
339
299
|
config,
|
|
300
|
+
connections: connectionsFile.connections,
|
|
301
|
+
oauthApps: Object.keys(connectionsFile.oauth_apps),
|
|
340
302
|
registry,
|
|
341
303
|
connectorFor,
|
|
342
304
|
authorizeRequest,
|
|
@@ -354,6 +316,9 @@ export async function openRuntime(
|
|
|
354
316
|
config,
|
|
355
317
|
target,
|
|
356
318
|
declared,
|
|
319
|
+
connections: selected,
|
|
320
|
+
workspaceConnections: connectionsFile.connections,
|
|
321
|
+
ownClients: Object.keys(connectionsFile.oauth_apps),
|
|
357
322
|
state,
|
|
358
323
|
audit,
|
|
359
324
|
credentials,
|
|
@@ -390,5 +355,5 @@ export async function openRuntime(
|
|
|
390
355
|
};
|
|
391
356
|
}
|
|
392
357
|
|
|
393
|
-
const row = (
|
|
394
|
-
|
|
358
|
+
const row = (connections: readonly ConnectionConfig[], provider: string, id: string) =>
|
|
359
|
+
connections.find((connection) => connection.provider === provider && connection.id === id);
|
|
@@ -3,7 +3,7 @@ import type { ProviderDefinition } from '#connectivity';
|
|
|
3
3
|
import { ConfigError } from '#profile';
|
|
4
4
|
import { ProviderRegistry } from '#registry';
|
|
5
5
|
import { RESERVED_BY_GRAMMAR } from '../commands/connect/custom/spec.ts';
|
|
6
|
-
import {
|
|
6
|
+
import { loadWorkspaceProviders } from '#providers/custom/index.ts';
|
|
7
7
|
import { loadProfileSkills, type LoadedSkill } from '#providers/skills/store.ts';
|
|
8
8
|
import { exampleProvider } from '#providers/example/provider.ts';
|
|
9
9
|
import {
|
|
@@ -138,21 +138,20 @@ export function buildRegistry(owner: OwnerLayerOptions = {}): ProviderRegistry {
|
|
|
138
138
|
* overriding it — an operator shadowing `gmail` by accident would be very hard
|
|
139
139
|
* to diagnose from the outside.
|
|
140
140
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
141
|
+
* No profile parameter any more. Manifests moved to the workspace with the
|
|
142
|
+
* connections they describe (ADR-057), so "which profile's manifests" stopped
|
|
143
|
+
* being a question — and with it the whole reason `deploy` had to rebuild a
|
|
144
|
+
* registry per profile to collect credential refs.
|
|
145
145
|
*/
|
|
146
146
|
export async function buildRegistryWithWorkspace(
|
|
147
147
|
root: string,
|
|
148
|
-
profile: string,
|
|
149
148
|
owner: OwnerLayerOptions = {},
|
|
150
149
|
): Promise<ProviderRegistry> {
|
|
151
150
|
const skills =
|
|
152
151
|
owner.skills ?? (owner.skillStore ? await loadProfileSkills(owner.skillStore) : []);
|
|
153
152
|
const registry = buildRegistry({ ...owner, skills });
|
|
154
153
|
|
|
155
|
-
for (const { manifest, path } of await
|
|
154
|
+
for (const { manifest, path } of await loadWorkspaceProviders(root)) {
|
|
156
155
|
if (registry.has(manifest.id)) {
|
|
157
156
|
throw new ConfigError(
|
|
158
157
|
`${path}: provider "${manifest.id}" is already built in. Rename it, or remove the file to use the built-in.`,
|
|
@@ -126,21 +126,12 @@ export async function resolveProfileOnly(
|
|
|
126
126
|
* inside Google's network. Opening a full runtime for either would fail on the
|
|
127
127
|
* part that is not needed.
|
|
128
128
|
*/
|
|
129
|
-
export async function openSecretStoreFor(
|
|
130
|
-
config: Config,
|
|
131
|
-
root: string,
|
|
132
|
-
target: string,
|
|
133
|
-
): Promise<SecretStore> {
|
|
129
|
+
export async function openSecretStoreFor(root: string, target: string): Promise<SecretStore> {
|
|
134
130
|
// Resolved here rather than taken from the caller, so `secrets push --from
|
|
135
131
|
// local --to cloud` can hold two targets that live in two different workspaces
|
|
136
132
|
// without the caller having to follow either pointer itself.
|
|
137
133
|
const resolved = await openTarget(root, target);
|
|
138
|
-
return openSecrets({
|
|
139
|
-
declared: resolved.declared,
|
|
140
|
-
config,
|
|
141
|
-
root: resolved.workspaceRoot,
|
|
142
|
-
target,
|
|
143
|
-
});
|
|
134
|
+
return openSecrets({ declared: resolved.declared, root: resolved.workspaceRoot, target });
|
|
144
135
|
}
|
|
145
136
|
|
|
146
137
|
/**
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { layout } from '#profile';
|
|
2
|
+
import type { BlobStore } from '#stores/blobs';
|
|
3
|
+
import type { StorageFactory } from '#deployments/target.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Where the owner layer's bytes are, for a runtime being assembled.
|
|
7
|
+
*
|
|
8
|
+
* Split from `open.ts` so that file stays inside the size budget, on the seam
|
|
9
|
+
* it already had: `open.ts` decides what a runtime *is* and this decides where
|
|
10
|
+
* one store lives. Both roots are explicit areas rather than derived from
|
|
11
|
+
* `storage.path`, matching `openState` and `openAudit` — a workspace that moves
|
|
12
|
+
* its provider blobs does not move the reserved roots beside them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A store with nothing in it, for a profile that grants no skills connection.
|
|
17
|
+
*
|
|
18
|
+
* Written out rather than imported from `#stores/blobs/testing.ts`: that module
|
|
19
|
+
* exists for tests, and production code reaching into it is how a test double
|
|
20
|
+
* ends up on a real path. Five methods, none of which can fail, is cheaper than
|
|
21
|
+
* the import would be to justify.
|
|
22
|
+
*/
|
|
23
|
+
export const EMPTY_SKILL_STORE: BlobStore = {
|
|
24
|
+
get: async () => null,
|
|
25
|
+
put: async () => {
|
|
26
|
+
throw new Error('this profile grants no skills connection');
|
|
27
|
+
},
|
|
28
|
+
has: async () => false,
|
|
29
|
+
delete: async () => {},
|
|
30
|
+
list: async () => [],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Where a connection's skills live, in either workspace.
|
|
35
|
+
*
|
|
36
|
+
* `profiles/<profile>/skills.d/<connection>/`, and deployed the same key under
|
|
37
|
+
* the bucket prefix. Going through the store rather than a filesystem path is what gives a
|
|
38
|
+
* deployment skills at all — a path is baked into a container image at build
|
|
39
|
+
* time and an object key is not, so before ADR-014 a deployed instance could
|
|
40
|
+
* only ever serve the skills that existed when its image was built.
|
|
41
|
+
*
|
|
42
|
+
* **Per profile and per connection**, which is the fourth answer this question
|
|
43
|
+
* has had. Policy gating `skills.<name>` was the whole isolation story while
|
|
44
|
+
* the bytes were shared (ADR-012 §1), and it is a weak one: it decides who may
|
|
45
|
+
* *run* a procedure, not who may read that it exists or what it says. ADR-030
|
|
46
|
+
* made the bytes per profile; ADR-059 made them per connection instead, so two
|
|
47
|
+
* profiles granting one skills connection shared a set; ADR-066 puts the
|
|
48
|
+
* profile back in front of it, so they do not. The connection stays because a
|
|
49
|
+
* profile may still hold more than one set.
|
|
50
|
+
*/
|
|
51
|
+
export function skillStore(
|
|
52
|
+
storage: StorageFactory,
|
|
53
|
+
profile: string,
|
|
54
|
+
connection: string,
|
|
55
|
+
): BlobStore {
|
|
56
|
+
return storage(layout.skills(profile, connection));
|
|
57
|
+
}
|
|
58
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { BearerAuthenticator } from '#auth';
|
|
2
|
+
import type { SecretStore } from '#secrets';
|
|
3
|
+
import type { AuditReader } from '#audit';
|
|
4
|
+
import type { RuntimeState } from '#stores/state';
|
|
5
|
+
import type { BlobStore } from '#stores/blobs';
|
|
6
|
+
import type { AnyConnector, ProviderManifest } from '#connectivity';
|
|
7
|
+
import type {
|
|
8
|
+
Config,
|
|
9
|
+
ConnectionConfig,
|
|
10
|
+
Resolution,
|
|
11
|
+
SelectedConnection,
|
|
12
|
+
TargetConfig,
|
|
13
|
+
} from '#profile';
|
|
14
|
+
import type { ProviderRegistry } from '#registry';
|
|
15
|
+
import type { Dispatcher } from '#dispatch';
|
|
16
|
+
import type { VaultStore } from '#providers/owner.ts';
|
|
17
|
+
import type { KnowledgeStores } from '#deployments/knowledge.ts';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What a command is handed once a profile has been opened.
|
|
21
|
+
*
|
|
22
|
+
* Its own file so `open.ts` stays inside the size budget, and because this is
|
|
23
|
+
* the thing every command in the CLI is written against — a reader looking for
|
|
24
|
+
* "what do I get" should not have to read the assembly to find it.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export interface Runtime {
|
|
28
|
+
readonly resolution: Resolution;
|
|
29
|
+
readonly config: Config;
|
|
30
|
+
readonly target: string;
|
|
31
|
+
/**
|
|
32
|
+
* The target's adapter set, from the workspace that declares it.
|
|
33
|
+
*
|
|
34
|
+
* Here because the config no longer carries it. Every caller that used to
|
|
35
|
+
* reach `config.targets[target]` — for a `deploy` block, a storage adapter, a
|
|
36
|
+
* knowledge repository — reads this instead, and reads it without following
|
|
37
|
+
* the pointer a second time (ADR-052).
|
|
38
|
+
*/
|
|
39
|
+
readonly declared: TargetConfig;
|
|
40
|
+
/**
|
|
41
|
+
* The accounts this profile selects, joined to the grants that govern them.
|
|
42
|
+
*
|
|
43
|
+
* The join is done once, here, because everything that asks — status, doctor,
|
|
44
|
+
* reconcile, setup — wants the same answer and three implementations of it is
|
|
45
|
+
* three chances to disagree (`profile/connections.ts`).
|
|
46
|
+
*/
|
|
47
|
+
readonly connections: readonly SelectedConnection[];
|
|
48
|
+
/** Every account the workspace holds, granted or not. `connection list` reads this. */
|
|
49
|
+
readonly workspaceConnections: readonly ConnectionConfig[];
|
|
50
|
+
/** Vendors this workspace registered a client of its own for (`oauth_apps`). */
|
|
51
|
+
readonly ownClients: readonly string[];
|
|
52
|
+
readonly state: RuntimeState;
|
|
53
|
+
/** The durable log, for reading. Copies, if any, are write-only and not here. */
|
|
54
|
+
readonly audit: AuditReader;
|
|
55
|
+
readonly credentials: SecretStore;
|
|
56
|
+
readonly storage: BlobStore;
|
|
57
|
+
/**
|
|
58
|
+
* Where this profile's skills are kept — `data/skills.d/<connection>/` locally.
|
|
59
|
+
*
|
|
60
|
+
* `undefined` when the profile grants no `skills` connection. That is a real
|
|
61
|
+
* state rather than an empty one, and it is worth the optionality: the owner
|
|
62
|
+
* layer arrives granted (ADR-050), so a profile without it was denied it on
|
|
63
|
+
* purpose, and handing back an empty store would report "no skills" for a
|
|
64
|
+
* profile that is not allowed to have any — the same answer for two different
|
|
65
|
+
* facts.
|
|
66
|
+
*/
|
|
67
|
+
readonly skills: BlobStore | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Present only when this target keeps memory and skills in a repository.
|
|
70
|
+
*
|
|
71
|
+
* Nothing on the dispatch path reads it: `storage` and `skills` above are
|
|
72
|
+
* already pointed at the right bytes, which is the whole design. It is here
|
|
73
|
+
* so `target show` and `doctor` can say where those bytes are without
|
|
74
|
+
* re-reading the config, and so the migration can reach the repository it is
|
|
75
|
+
* committing to.
|
|
76
|
+
*/
|
|
77
|
+
readonly knowledge?: KnowledgeStores | undefined;
|
|
78
|
+
/** The vault's own store, so `lanes link vault` reaches the same bytes MCP does. */
|
|
79
|
+
readonly vault: VaultStore;
|
|
80
|
+
readonly registry: ProviderRegistry;
|
|
81
|
+
/**
|
|
82
|
+
* Re-read the skills into the registry when they have changed on the store.
|
|
83
|
+
*
|
|
84
|
+
* Cheap and idempotent — one `list()`, and a rebuild only when the listing
|
|
85
|
+
* differs from the last one. The caller decides how often to ask.
|
|
86
|
+
*/
|
|
87
|
+
refreshSkills(): Promise<void>;
|
|
88
|
+
readonly dispatcher: Dispatcher;
|
|
89
|
+
readonly authenticator: BearerAuthenticator;
|
|
90
|
+
/** Same factory the dispatcher uses, exposed for commands that probe upstream. */
|
|
91
|
+
connectorFor(providerId: string, connectionId: string): AnyConnector | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Same authorizer the dispatcher uses, for the same reason `connectorFor` is here.
|
|
94
|
+
*
|
|
95
|
+
* A command that probes upstream needs the credential on the request, and it
|
|
96
|
+
* must not learn how to put it there — that is one switch on the resolved
|
|
97
|
+
* shape (`connectivity/auth/authorize.ts`) and a second copy would be a
|
|
98
|
+
* second answer. `settleIdentity` is the caller: it asks a provider whose
|
|
99
|
+
* account was just authorised, over whatever method that provider declares.
|
|
100
|
+
*/
|
|
101
|
+
authorizeRequest(providerId: string, connectionId: string, request: Request): Promise<Request>;
|
|
102
|
+
/** A provider's manifest, so an omitted `credential_ref` can be derived from it. */
|
|
103
|
+
manifestFor(providerId: string): ProviderManifest | undefined;
|
|
104
|
+
close(): Promise<void>;
|
|
105
|
+
}
|
|
106
|
+
|
package/src/cli/runtime/vault.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
soleGrantFor, layout, vaultRef, workspacePath } from '#profile';
|
|
2
3
|
import type { SecretStore } from '#secrets';
|
|
3
4
|
import {
|
|
4
5
|
createBlobVaultStore,
|
|
@@ -36,10 +37,15 @@ export function openVault(
|
|
|
36
37
|
const { declared, config, root } = input;
|
|
37
38
|
const vault = declared.vault ?? { adapter: 'file' as const };
|
|
38
39
|
|
|
40
|
+
// The vault connection this profile grants (ADR-059). `main` when it grants
|
|
41
|
+
// none, which keeps a profile that denied the vault opening against the same
|
|
42
|
+
// document every other profile uses rather than inventing a second one.
|
|
43
|
+
const connection = soleGrantFor(config, 'lanes_vault') ?? 'main';
|
|
44
|
+
|
|
39
45
|
switch (vault.adapter) {
|
|
40
46
|
case 'file':
|
|
41
47
|
return createFileVaultStore({
|
|
42
|
-
path: workspacePath(root, vault.path ?? layout.vault(config.instance.profile)),
|
|
48
|
+
path: workspacePath(root, vault.path ?? layout.vault(config.instance.profile, connection)),
|
|
43
49
|
});
|
|
44
50
|
|
|
45
51
|
case 'secret':
|
|
@@ -47,15 +53,24 @@ export function openVault(
|
|
|
47
53
|
// so the credential store holds ciphertext it cannot read. Separate
|
|
48
54
|
// document, separate key, separate environment variable — the backend
|
|
49
55
|
// was never what kept the two stores apart. ADR-022.
|
|
56
|
+
//
|
|
57
|
+
// Named per connection, like the file adapter. A `ref` written by hand
|
|
58
|
+
// still wins, because a deployment that already seals under one name has
|
|
59
|
+
// to keep opening it — but the default carries the instance, so two
|
|
60
|
+
// profiles granting different vaults share nothing. Without this only
|
|
61
|
+
// `file` honoured ADR-059 and every deployed workspace, which uses this
|
|
62
|
+
// adapter or `blob`, had one document behind every vault connection:
|
|
63
|
+
// ADR-059 calls that the worst of the three collisions, because the wrong
|
|
64
|
+
// answer is a credential.
|
|
50
65
|
return createSecretVaultStore({
|
|
51
66
|
store: credentials,
|
|
52
|
-
|
|
67
|
+
ref: vaultRef(declared, config),
|
|
53
68
|
});
|
|
54
69
|
|
|
55
70
|
case 'blob':
|
|
56
71
|
return createBlobVaultStore({
|
|
57
72
|
store: storage(),
|
|
58
|
-
|
|
73
|
+
key: vault.path ?? layout.vaultKey(connection),
|
|
59
74
|
});
|
|
60
75
|
}
|
|
61
76
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ConfigError, listProfiles, openTarget, resolveWorkspaceRoot } from '#profile';
|
|
2
|
+
import { openRuntime, type Runtime } from './open.ts';
|
|
3
|
+
import type { GlobalFlags } from './select.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A runtime for a workspace, when the command does not act on one profile.
|
|
7
|
+
*
|
|
8
|
+
* Contract 3 moved the credential store, the audit log, the state store and the
|
|
9
|
+
* blob root up to the workspace, so a command that opens any of them opens the
|
|
10
|
+
* same thing whichever profile it was handed. `openRuntime` still wants one,
|
|
11
|
+
* because a `Runtime` carries a resolved profile — so this picks one rather
|
|
12
|
+
* than making the caller ask a question with no answer.
|
|
13
|
+
*
|
|
14
|
+
* `--profile` still narrows where narrowing means something: the audit log is
|
|
15
|
+
* one chain and the flag filters it, rather than selecting which log to read.
|
|
16
|
+
* What it must never do is *decide* anything, which is why the profile chosen
|
|
17
|
+
* here when none is given is simply the first and is never reported as though
|
|
18
|
+
* it were the subject.
|
|
19
|
+
*/
|
|
20
|
+
export async function openWorkspaceRuntime(flags: GlobalFlags): Promise<Runtime> {
|
|
21
|
+
if (flags.profile !== undefined) return openRuntime(flags);
|
|
22
|
+
|
|
23
|
+
const root = resolveWorkspaceRoot();
|
|
24
|
+
const resolved = await openTarget(root, flags.target!);
|
|
25
|
+
const names = await listProfiles(resolved.workspaceRoot);
|
|
26
|
+
|
|
27
|
+
if (names.length === 0) {
|
|
28
|
+
throw new ConfigError(
|
|
29
|
+
`Workspace "${flags.target}" holds no profiles, so there is nothing to open.\n` +
|
|
30
|
+
` Create one with: lanes link profile add <name> --workspace ${flags.target}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return openRuntime({ ...flags, profile: names[0]! });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Which profile a workspace-level command should act *as*.
|
|
39
|
+
*
|
|
40
|
+
* Distinct from `openWorkspaceRuntime` because the caller needs the name rather
|
|
41
|
+
* than a runtime — `start` puts it back into flags and hands them on. Same rule:
|
|
42
|
+
* `--profile` wins, and otherwise the first is taken without being reported as
|
|
43
|
+
* a choice, because for the things this is used for it is not one.
|
|
44
|
+
*/
|
|
45
|
+
export async function primaryProfile(flags: GlobalFlags): Promise<string> {
|
|
46
|
+
if (flags.profile !== undefined) return flags.profile;
|
|
47
|
+
|
|
48
|
+
const root = resolveWorkspaceRoot();
|
|
49
|
+
const resolved = await openTarget(root, flags.target!);
|
|
50
|
+
const names = await listProfiles(resolved.workspaceRoot);
|
|
51
|
+
|
|
52
|
+
if (names.length === 0) {
|
|
53
|
+
throw new ConfigError(
|
|
54
|
+
`Workspace "${flags.target}" holds no profiles, so there is nothing to serve.\n` +
|
|
55
|
+
` Create one with: lanes link profile add <name> --workspace ${flags.target}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return names[0]!;
|
|
60
|
+
}
|
package/src/cli/runtime.ts
CHANGED
|
@@ -32,4 +32,5 @@ export {
|
|
|
32
32
|
type OwnerLayerOptions,
|
|
33
33
|
} from './runtime/registry.ts';
|
|
34
34
|
|
|
35
|
-
export { openRuntime, type OpenOptions, type Runtime } from './runtime/open.ts';
|
|
35
|
+
export { grantedConnections, openRuntime, type OpenOptions, type Runtime } from './runtime/open.ts';
|
|
36
|
+
export { openWorkspaceRuntime, primaryProfile } from './runtime/workspace.ts';
|