@lanes-sh/link 0.7.2 → 0.8.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 +136 -61
- 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 +65 -0
- package/src/auth/remote.ts +32 -9
- package/src/cli/accepts.ts +108 -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 +88 -87
- 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 +4 -2
- 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 +182 -165
- package/src/cli/commands/grant.ts +140 -0
- package/src/cli/commands/identity.ts +21 -9
- package/src/cli/commands/knowledge/index.ts +46 -79
- package/src/cli/commands/knowledge/migrate.ts +74 -13
- package/src/cli/commands/knowledge/show.ts +92 -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 +29 -12
- 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 +52 -4
- 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/shared.ts +13 -2
- package/src/cli/commands/owner/skills.ts +28 -8
- package/src/cli/commands/profile/removal.ts +79 -76
- package/src/cli/commands/profile/remove.ts +16 -1
- package/src/cli/commands/profile.ts +46 -10
- package/src/cli/commands/relabel.ts +112 -0
- package/src/cli/commands/secrets.ts +34 -12
- 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.ts +58 -17
- package/src/cli/config-edit.ts +75 -140
- package/src/cli/config-migrate.ts +82 -64
- package/src/cli/config-repair.ts +89 -32
- package/src/cli/config-templates.ts +198 -0
- package/src/cli/contract3-data.ts +328 -0
- package/src/cli/contract3-shape.ts +186 -0
- package/src/cli/contract3.ts +282 -0
- package/src/cli/endpoint-url.ts +1 -1
- package/src/cli/lanes.ts +25 -1
- package/src/cli/main.ts +89 -14
- package/src/cli/migrate-plan.ts +12 -6
- package/src/cli/output.ts +34 -1
- package/src/cli/publish.ts +5 -2
- package/src/cli/runtime/open.ts +63 -98
- package/src/cli/runtime/registry.ts +6 -7
- package/src/cli/runtime/stores.ts +53 -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 +34 -18
- package/src/cli/workspace-migrate.ts +125 -16
- package/src/connectivity/manifest/provider.ts +3 -1
- package/src/connectivity/manifest/requirements.ts +1 -1
- package/src/deployments/bind.ts +1 -1
- package/src/deployments/deploy.ts +36 -27
- package/src/deployments/gcp/bucket.ts +18 -7
- package/src/deployments/gcp/provision.ts +7 -7
- 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 +15 -15
- package/src/deployments/upload.ts +46 -22
- 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 +183 -0
- package/src/profile/deployments.ts +3 -3
- package/src/profile/index.ts +30 -5
- package/src/profile/layout.ts +86 -89
- package/src/profile/load.ts +80 -47
- 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 +172 -21
- package/src/profile/targets.ts +21 -9
- package/src/profile/testing.ts +69 -2
- package/src/profile/workspace.ts +58 -3
- package/src/providers/custom/index.ts +1 -1
- package/src/providers/custom/load.ts +2 -3
- package/src/providers/identity/provider.ts +1 -1
- package/src/providers/memory/provider.ts +20 -2
- package/src/providers/setup/plan.ts +1 -1
- package/src/providers/slack/index.ts +2 -2
- package/src/registry/policy-bridge.ts +33 -11
- package/src/registry/reconcile.ts +4 -4
- package/src/server/authorization.ts +94 -0
- package/src/server/edge.ts +14 -1
- package/src/server/endpoint.ts +85 -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 +1 -1
- package/src/server/mcp/prompts.ts +7 -3
- package/src/server/mcp/resources.ts +16 -8
- package/src/server/mcp/tools.ts +9 -3
- package/src/server/mcp/visibility.ts +18 -3
- 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/cli/main.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { connect } from './commands/connect/index.ts';
|
|
2
2
|
import { connectCustom } from './commands/connect/custom/index.ts';
|
|
3
|
-
import { disconnect
|
|
3
|
+
import { disconnect } from './commands/connection.ts';
|
|
4
|
+
import { grantConnectionTo, revokeConnectionFrom } from './commands/grant.ts';
|
|
5
|
+
import { connectionList } from './commands/connection-list.ts';
|
|
6
|
+
import { membersAdd, membersList, membersRemove } from './commands/members.ts';
|
|
7
|
+
import { relabel } from './commands/relabel.ts';
|
|
4
8
|
import {
|
|
5
9
|
attachFile,
|
|
6
10
|
auditTail,
|
|
@@ -11,6 +15,7 @@ import {
|
|
|
11
15
|
desktop,
|
|
12
16
|
doctor,
|
|
13
17
|
outputs,
|
|
18
|
+
pair,
|
|
14
19
|
plan,
|
|
15
20
|
policyList,
|
|
16
21
|
policyRule,
|
|
@@ -26,13 +31,23 @@ import { syncTargets } from './commands/sync.ts';
|
|
|
26
31
|
import { targetList, targetShow, targetUse } from './commands/target.ts';
|
|
27
32
|
import { identityAdd, identityList, identityRemove } from './commands/identity.ts';
|
|
28
33
|
import { setupPlan } from './commands/setup.ts';
|
|
29
|
-
import { mcpAdd, mcpList, mcpStdio, skillDocument } from './commands/mcp.ts';
|
|
34
|
+
import { installInstructions, mcpAdd, mcpList, mcpStdio, skillDocument } from './commands/mcp.ts';
|
|
30
35
|
import { deploy } from '#deployments/deploy.ts';
|
|
31
36
|
import { secretsList, secretsPush, secretsSet } from './commands/secrets.ts';
|
|
32
37
|
import { knowledgeShow, knowledgeUse } from './commands/knowledge.ts';
|
|
33
38
|
import { dispatchOwner } from './dispatch-owner.ts';
|
|
34
39
|
import { update } from './commands/update.ts';
|
|
35
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
all,
|
|
42
|
+
customFlags,
|
|
43
|
+
globalFlags,
|
|
44
|
+
knowledgeFlags,
|
|
45
|
+
ownerFlags,
|
|
46
|
+
parseArgv,
|
|
47
|
+
text,
|
|
48
|
+
} from './argv.ts';
|
|
49
|
+
import type { GlobalFlags } from './runtime.ts';
|
|
50
|
+
import type { OwnerFlags } from './commands/owner/shared.ts';
|
|
36
51
|
import { assertKnownFlags } from './selection.ts';
|
|
37
52
|
import { requireSelection } from './selection-require.ts';
|
|
38
53
|
import { PROGRAM, USAGE } from './usage.ts';
|
|
@@ -57,8 +72,12 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
57
72
|
const { command, flags } = parseArgv(argv);
|
|
58
73
|
const [first, second, ...rest] = command;
|
|
59
74
|
|
|
60
|
-
|
|
61
|
-
|
|
75
|
+
// Both are read *after* `requireSelection`, which is what resolves
|
|
76
|
+
// `default_workspace` onto `flags` (ADR-061). Capturing them before it would
|
|
77
|
+
// hand every command an undefined workspace on the path the default exists to
|
|
78
|
+
// serve — the flag would be resolved and nothing would be looking.
|
|
79
|
+
let global: GlobalFlags;
|
|
80
|
+
let owner: OwnerFlags;
|
|
62
81
|
|
|
63
82
|
const show = flags['show'] === true;
|
|
64
83
|
const raw = flags['raw'] === true;
|
|
@@ -77,6 +96,9 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
77
96
|
assertKnownFlags(first, second, flags);
|
|
78
97
|
await requireSelection(first, second, flags);
|
|
79
98
|
|
|
99
|
+
global = globalFlags(flags);
|
|
100
|
+
owner = ownerFlags(flags, argv);
|
|
101
|
+
|
|
80
102
|
switch (first) {
|
|
81
103
|
case 'connect':
|
|
82
104
|
// A nested switch rather than an `if`, so `selection.test.ts` sees
|
|
@@ -117,6 +139,20 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
117
139
|
});
|
|
118
140
|
}
|
|
119
141
|
|
|
142
|
+
// The command connecting no longer implies. A connection belongs to the
|
|
143
|
+
// workspace (ADR-057), so which profiles may reach it is a separate say.
|
|
144
|
+
case 'connection':
|
|
145
|
+
if (second !== 'list' && second !== undefined) {
|
|
146
|
+
throw new Error(`Unknown: ${PROGRAM} connection ${second}`);
|
|
147
|
+
}
|
|
148
|
+
return connectionList({ ...global, json });
|
|
149
|
+
|
|
150
|
+
case 'grant':
|
|
151
|
+
return grantConnectionTo(second, { ...global, json });
|
|
152
|
+
|
|
153
|
+
case 'revoke':
|
|
154
|
+
return revokeConnectionFrom(second, { ...global, json });
|
|
155
|
+
|
|
120
156
|
case 'setup':
|
|
121
157
|
if (second !== 'plan' && second !== undefined) {
|
|
122
158
|
throw new Error(`Unknown: ${PROGRAM} setup ${second}`);
|
|
@@ -127,27 +163,46 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
127
163
|
switch (second) {
|
|
128
164
|
case 'add':
|
|
129
165
|
if (!rest[0]) {
|
|
130
|
-
throw new Error(`Usage: ${PROGRAM} profile add <name> --
|
|
166
|
+
throw new Error(`Usage: ${PROGRAM} profile add <name> --workspace <name>`);
|
|
131
167
|
}
|
|
132
168
|
return profileAdd(rest[0], {
|
|
133
169
|
// One target, not a list. A profile declared every target it could
|
|
134
170
|
// run on under contract 1, which is why this read the repeated flag
|
|
135
171
|
// out of argv; it lives in exactly one now (ADR-052), so a second
|
|
136
172
|
// --target would be naming a second place to put the same file.
|
|
137
|
-
targets: [text(flags, '
|
|
173
|
+
targets: [text(flags, 'workspace')!],
|
|
138
174
|
nonInteractive: flags['non-interactive'] === true,
|
|
139
175
|
json,
|
|
140
176
|
});
|
|
141
177
|
case 'list':
|
|
142
178
|
case undefined:
|
|
143
|
-
return profileList(text(flags, '
|
|
179
|
+
return profileList(text(flags, 'workspace')!, { json });
|
|
144
180
|
case 'default':
|
|
145
181
|
if (!rest[0]) throw new Error(`Usage: ${PROGRAM} profile default <name>`);
|
|
146
182
|
return profileDefault(rest[0]);
|
|
183
|
+
case 'members': {
|
|
184
|
+
const [action, subject] = rest;
|
|
185
|
+
switch (action) {
|
|
186
|
+
case 'add':
|
|
187
|
+
return membersAdd(subject, {
|
|
188
|
+
...global,
|
|
189
|
+
json,
|
|
190
|
+
me: flags['me'] === true,
|
|
191
|
+
role: text(flags, 'role'),
|
|
192
|
+
});
|
|
193
|
+
case 'remove':
|
|
194
|
+
return membersRemove(subject, { ...global, json });
|
|
195
|
+
case 'list':
|
|
196
|
+
case undefined:
|
|
197
|
+
return membersList({ ...global, json });
|
|
198
|
+
default:
|
|
199
|
+
throw new Error(`Unknown: ${PROGRAM} profile members ${action}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
147
202
|
case 'remove':
|
|
148
203
|
if (!rest[0]) {
|
|
149
204
|
throw new Error(
|
|
150
|
-
`Usage: ${PROGRAM} profile remove <name> [--
|
|
205
|
+
`Usage: ${PROGRAM} profile remove <name> [--workspace <name>] [--dry-run] [--yes]`,
|
|
151
206
|
);
|
|
152
207
|
}
|
|
153
208
|
return profileRemove(rest[0], {
|
|
@@ -160,18 +215,22 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
160
215
|
throw new Error(`Unknown: ${PROGRAM} profile ${second}`);
|
|
161
216
|
}
|
|
162
217
|
|
|
218
|
+
// `target` was the old word for the same thing (ADR-061). Kept as its own
|
|
219
|
+
// case rather than aliased, so `selection.test.ts` sees both spellings and
|
|
220
|
+
// neither can default quietly.
|
|
163
221
|
case 'target':
|
|
222
|
+
case 'workspace':
|
|
164
223
|
switch (second) {
|
|
165
224
|
case 'list':
|
|
166
225
|
case undefined:
|
|
167
226
|
return targetList({ ...global, json, urls: flags['urls'] === true });
|
|
168
227
|
case 'use':
|
|
169
|
-
if (!rest[0]) throw new Error(`Usage: ${PROGRAM}
|
|
228
|
+
if (!rest[0]) throw new Error(`Usage: ${PROGRAM} workspace use <name>`);
|
|
170
229
|
return targetUse(rest[0]);
|
|
171
230
|
case 'show':
|
|
172
231
|
return targetShow(rest[0], { ...global, json });
|
|
173
232
|
default:
|
|
174
|
-
throw new Error(`Unknown: ${PROGRAM}
|
|
233
|
+
throw new Error(`Unknown: ${PROGRAM} workspace ${second}`);
|
|
175
234
|
}
|
|
176
235
|
|
|
177
236
|
case 'identity': {
|
|
@@ -206,11 +265,15 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
206
265
|
const [capability] = rest;
|
|
207
266
|
if (!capability) {
|
|
208
267
|
throw new Error(
|
|
209
|
-
`Usage: ${PROGRAM} policy ${second} <capability>\n` +
|
|
210
|
-
` e.g. ${PROGRAM} policy ${second} gmail.send_message
|
|
268
|
+
`Usage: ${PROGRAM} policy ${second} <capability> --connection <provider>.<id>\n` +
|
|
269
|
+
` e.g. ${PROGRAM} policy ${second} gmail.send_message --connection gmail.personal,\n` +
|
|
270
|
+
` or gmail.* for everything that connection's provider offers`,
|
|
211
271
|
);
|
|
212
272
|
}
|
|
213
|
-
return policyRule(second, capability,
|
|
273
|
+
return policyRule(second, capability, {
|
|
274
|
+
...global,
|
|
275
|
+
...(typeof flags.connection === 'string' ? { connection: flags.connection } : {}),
|
|
276
|
+
});
|
|
214
277
|
}
|
|
215
278
|
default:
|
|
216
279
|
throw new Error(`Unknown: ${PROGRAM} policy ${second}`);
|
|
@@ -227,6 +290,14 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
227
290
|
throw new Error(`Unknown: ${PROGRAM} token ${second}`);
|
|
228
291
|
}
|
|
229
292
|
|
|
293
|
+
case 'pair':
|
|
294
|
+
return pair({
|
|
295
|
+
...global,
|
|
296
|
+
print: flags['print'] === true,
|
|
297
|
+
rotate: flags['rotate'] === true,
|
|
298
|
+
yes: flags['yes'] === true,
|
|
299
|
+
});
|
|
300
|
+
|
|
230
301
|
case 'audit':
|
|
231
302
|
if (second === 'verify') return auditVerify(global);
|
|
232
303
|
if (second !== 'tail' && second !== undefined) {
|
|
@@ -340,7 +411,10 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
340
411
|
dryRun: flags['dry-run'] === true,
|
|
341
412
|
force: flags['force'] === true,
|
|
342
413
|
noSkill: flags['no-skill'] === true,
|
|
414
|
+
headless: flags['headless'] === true,
|
|
343
415
|
});
|
|
416
|
+
case 'install-instructions':
|
|
417
|
+
return installInstructions({ ...(text(flags, 'client') ? { client: text(flags, 'client') } : {}) });
|
|
344
418
|
case 'stdio':
|
|
345
419
|
return mcpStdio({ ...global, ...(flags['only'] === true ? { only: true } : {}) });
|
|
346
420
|
case 'list':
|
|
@@ -398,6 +472,7 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
398
472
|
switch (second) {
|
|
399
473
|
case undefined:
|
|
400
474
|
case 'targets':
|
|
475
|
+
case 'workspaces':
|
|
401
476
|
return syncTargets({
|
|
402
477
|
...global,
|
|
403
478
|
dryRun: flags['dry-run'] === true,
|
package/src/cli/migrate-plan.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DATA_DIR, ConfigError, layout, isRemoteWorkspace, type LegacyTarget, type WorkspaceTarget } from '#profile';
|
|
2
3
|
import { deployedWorkspace } from '#deployments/upload.ts';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -88,7 +89,7 @@ export function toEntry(
|
|
|
88
89
|
// produces `gs://b` pointing at `gs://b`. That is a loop, and `openTarget`
|
|
89
90
|
// refuses it — which made `deploy` unable to run against the bucket it had
|
|
90
91
|
// just migrated, on the one command the refusal names as the fix.
|
|
91
|
-
if (remote && remote !== workspaceRoot) return {
|
|
92
|
+
if (remote && remote !== workspaceRoot) return { at: remote };
|
|
92
93
|
|
|
93
94
|
// **A filesystem target is dropped from a remote workspace.**
|
|
94
95
|
//
|
|
@@ -105,8 +106,13 @@ export function toEntry(
|
|
|
105
106
|
const storagePath = declared.storage.path;
|
|
106
107
|
const credentialsPath = declared.credentials.path;
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
// The **contract-1** defaults, spelled out rather than taken from `layout`.
|
|
110
|
+
// This function reads a contract-1 profile, where every path was per profile,
|
|
111
|
+
// and `layout` describes where things live *now* — workspace-level since
|
|
112
|
+
// ADR-057. Asking it would compare a contract-1 path against a contract-3
|
|
113
|
+
// default and refuse every profile that had written the ordinary one.
|
|
114
|
+
const defaultStorage = `./${DATA_DIR}/${profile}`;
|
|
115
|
+
const defaultCredentials = `./${DATA_DIR}/${profile}/credentials.enc`;
|
|
110
116
|
|
|
111
117
|
refuseCustomPath(name, profile, workspaceRoot, 'storage.path', storagePath, defaultStorage);
|
|
112
118
|
refuseCustomPath(
|
|
@@ -149,12 +155,12 @@ function refuseCustomPath(
|
|
|
149
155
|
` A target is declared once for the whole workspace now (ADR-052), so it cannot hold a\n` +
|
|
150
156
|
` different path per profile. The default it would get is ${expected}.\n\n` +
|
|
151
157
|
` Move the data there and delete the line, or keep this profile in a workspace of its\n` +
|
|
152
|
-
` own: LANES_LINK_HOME=${workspaceRoot}/<somewhere> lanes link profile add ${profile} --
|
|
158
|
+
` own: LANES_LINK_HOME=${workspaceRoot}/<somewhere> lanes link profile add ${profile} --workspace ${target}`,
|
|
153
159
|
);
|
|
154
160
|
}
|
|
155
161
|
|
|
156
162
|
export function summarise(entry: WorkspaceTarget): string {
|
|
157
|
-
if (entry.
|
|
163
|
+
if (entry.at !== undefined) return `points at ${entry.at}`;
|
|
158
164
|
const parts = [entry.credentials?.adapter, entry.storage?.adapter].filter(Boolean);
|
|
159
165
|
return `${parts.join(' + ')}${entry.deploy ? `, deploys ${entry.deploy.service}` : ''}`;
|
|
160
166
|
}
|
package/src/cli/output.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wasDefaulted } from './selection-require.ts';
|
|
1
2
|
import type { Resolution } from '#profile';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -126,16 +127,48 @@ export function announceProfile(selection: {
|
|
|
126
127
|
print(style.dim(`profile ${style.bold(selection.profile)} ${selection.workspaceRoot}`));
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
/**
|
|
131
|
+
* The line every command prints before its output.
|
|
132
|
+
*
|
|
133
|
+
* It names the workspace, and whether that workspace was *typed* or came from
|
|
134
|
+
* `default_workspace`. The provenance is not decoration: ADR-037 removed sticky
|
|
135
|
+
* selection because "the dotfile nothing prints" is how an operator runs a
|
|
136
|
+
* command against the wrong thing, and ADR-061 gives the default back only on
|
|
137
|
+
* the condition that it announces itself. Dropping the `(default)` marker for
|
|
138
|
+
* tidiness would reverse that decision.
|
|
139
|
+
*/
|
|
129
140
|
export function announce(resolution: Resolution): void {
|
|
141
|
+
const provenance = wasDefaulted(resolution.target) ? ' (default)' : '';
|
|
142
|
+
|
|
130
143
|
print(
|
|
131
144
|
style.dim(
|
|
132
145
|
`profile ${style.bold(resolution.profile)} ` +
|
|
133
|
-
`
|
|
146
|
+
`workspace ${style.bold(resolution.target)}${provenance} ` +
|
|
134
147
|
`${resolution.workspaceRoot}`,
|
|
135
148
|
),
|
|
136
149
|
);
|
|
137
150
|
}
|
|
138
151
|
|
|
152
|
+
/**
|
|
153
|
+
* The same line, for a command whose subject is the workspace.
|
|
154
|
+
*
|
|
155
|
+
* It names no profile, and that is the point rather than a saving. A
|
|
156
|
+
* workspace-level command still opens a runtime, and a runtime carries a
|
|
157
|
+
* profile — so `announce` would print whichever one happened to be picked, and
|
|
158
|
+
* a reader would take it for the thing being acted on. The audit log is one
|
|
159
|
+
* chain for the whole workspace; saying `profile personal` above it describes a
|
|
160
|
+
* log that does not exist.
|
|
161
|
+
*/
|
|
162
|
+
export function announceWorkspace(resolution: Resolution): void {
|
|
163
|
+
const provenance = wasDefaulted(resolution.target) ? ' (default)' : '';
|
|
164
|
+
|
|
165
|
+
print(
|
|
166
|
+
style.dim(
|
|
167
|
+
`workspace ${style.bold(resolution.target)}${provenance} ${resolution.workspaceRoot}`,
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
139
172
|
/**
|
|
140
173
|
* Print a machine-readable result, or fall through to the human rendering.
|
|
141
174
|
*
|
package/src/cli/publish.ts
CHANGED
|
@@ -53,7 +53,8 @@ export async function publishAndNotify(input: {
|
|
|
53
53
|
readonly config: Config;
|
|
54
54
|
readonly workspaceRoot: string;
|
|
55
55
|
readonly target: string;
|
|
56
|
-
|
|
56
|
+
/** Every profile the edit touched. See `publishWorkspace`. */
|
|
57
|
+
readonly profile: string | readonly string[];
|
|
57
58
|
readonly credentials: SecretStore;
|
|
58
59
|
}): Promise<PublishOutcome> {
|
|
59
60
|
let published: string | null = null;
|
|
@@ -98,6 +99,8 @@ export async function publishProfileEdit(input: {
|
|
|
98
99
|
readonly resolution: { readonly workspaceRoot: string; readonly profile: string };
|
|
99
100
|
readonly config: Config;
|
|
100
101
|
readonly target: string;
|
|
102
|
+
/** Every profile the edit touched, where it reached more than the one named. */
|
|
103
|
+
readonly touched?: readonly string[] | undefined;
|
|
101
104
|
}): Promise<PublishOutcome> {
|
|
102
105
|
const credentials = await openSecretStoreFor(
|
|
103
106
|
input.config,
|
|
@@ -109,7 +112,7 @@ export async function publishProfileEdit(input: {
|
|
|
109
112
|
config: input.config,
|
|
110
113
|
workspaceRoot: input.resolution.workspaceRoot,
|
|
111
114
|
target: input.target,
|
|
112
|
-
profile: input.resolution.profile,
|
|
115
|
+
profile: input.touched ?? input.resolution.profile,
|
|
113
116
|
credentials,
|
|
114
117
|
});
|
|
115
118
|
}
|
package/src/cli/runtime/open.ts
CHANGED
|
@@ -6,11 +6,17 @@ 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
|
|
@@ -174,7 +127,7 @@ export async function openRuntime(
|
|
|
174
127
|
// own roots on the target's own storage and are untouched.
|
|
175
128
|
const knowledge = await openKnowledge(adapters, credentials, options.fetch);
|
|
176
129
|
const storage = knowledge ? routeBlobStore(storageFor(), knowledgeRoutes(knowledge)) : storageFor();
|
|
177
|
-
const state = openState(storageFor
|
|
130
|
+
const state = openState(storageFor);
|
|
178
131
|
|
|
179
132
|
// The durable log, plus any copies the target declares. `sink` is what
|
|
180
133
|
// dispatch writes to; `audit` is what `tail` and `verify` read, and those are
|
|
@@ -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, 'skills');
|
|
152
|
+
const skills =
|
|
153
|
+
knowledge?.skills ??
|
|
154
|
+
(skillsConnection === undefined ? undefined : skillStore(storageFor, 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
|
|
@@ -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.`,
|