@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProviderManifest } from '#connectivity';
|
|
2
|
-
import { setupRequirements, type SetupRequirement } from '#connectivity';
|
|
2
|
+
import { hasOwnClientPath, setupRequirements, type SetupRequirement } from '#connectivity';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* What connecting a provider involves, assembled from its manifest.
|
|
@@ -68,10 +68,22 @@ export interface ProviderPlan {
|
|
|
68
68
|
readonly clientOperator?: string;
|
|
69
69
|
/** The line that opts out of it and registers one of your own instead. */
|
|
70
70
|
readonly ownClientCommand?: string;
|
|
71
|
+
/** What `--auth pasted_token` asks for, where that is a way in. */
|
|
72
|
+
readonly pastedCredential?: string;
|
|
73
|
+
/** The line that takes that way in. */
|
|
74
|
+
readonly tokenCommand?: string;
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
export interface PlanContext {
|
|
74
78
|
readonly profile: string;
|
|
79
|
+
/**
|
|
80
|
+
* Which target's stores the emitted command should act on.
|
|
81
|
+
*
|
|
82
|
+
* Stamped like `profile` is, and for a stronger reason: a connection's
|
|
83
|
+
* credential lives in one target's store, so a `connect` that lands in the
|
|
84
|
+
* wrong one authorises an account the endpoint asking for it cannot read.
|
|
85
|
+
*/
|
|
86
|
+
readonly target: string;
|
|
75
87
|
/** Every configured connection this caller may see, as `provider.id`. */
|
|
76
88
|
readonly connections: readonly string[];
|
|
77
89
|
/**
|
|
@@ -88,21 +100,22 @@ export function planFor(
|
|
|
88
100
|
context: PlanContext,
|
|
89
101
|
connectionId?: string,
|
|
90
102
|
): ProviderPlan {
|
|
91
|
-
const { requirements, needsId, brokered } = setupRequirements(
|
|
103
|
+
const { requirements, needsId, brokered, pastedCredential } = setupRequirements(
|
|
92
104
|
manifest,
|
|
93
105
|
connectionId,
|
|
94
|
-
context.profile,
|
|
106
|
+
{ profile: context.profile, target: context.target },
|
|
95
107
|
{ ...(context.ownClients ? { ownClients: context.ownClients } : {}) },
|
|
96
108
|
);
|
|
97
109
|
|
|
98
110
|
const connected = context.connections.filter((key) => key.startsWith(`${manifest.id}.`));
|
|
99
111
|
|
|
100
|
-
//
|
|
101
|
-
// and the shell this
|
|
102
|
-
//
|
|
103
|
-
//
|
|
112
|
+
// Both, always, never conditionally. One endpoint serves every profile and
|
|
113
|
+
// each profile may declare several targets, and the shell this is pasted into
|
|
114
|
+
// supplies neither — nothing but the command line does. An emitted command
|
|
115
|
+
// missing either is one that refuses, or worse, writes a credential into a
|
|
116
|
+
// store the endpoint that asked for it does not read.
|
|
104
117
|
const command =
|
|
105
|
-
`lanes link connect ${manifest.id} --profile ${context.profile}` +
|
|
118
|
+
`lanes link connect ${manifest.id} --profile ${context.profile} --target ${context.target}` +
|
|
106
119
|
(needsId ? ' --id <name>' : connectionId ? ` --id ${connectionId}` : '');
|
|
107
120
|
|
|
108
121
|
return {
|
|
@@ -119,13 +132,22 @@ export function planFor(
|
|
|
119
132
|
needsId,
|
|
120
133
|
command,
|
|
121
134
|
brokered,
|
|
135
|
+
...(pastedCredential
|
|
136
|
+
? { pastedCredential, tokenCommand: `${command} --auth pasted_token` }
|
|
137
|
+
: {}),
|
|
122
138
|
...(brokered && manifest.auth.kind === 'oauth' && manifest.auth.broker
|
|
123
139
|
? {
|
|
124
140
|
clientOperator: manifest.auth.broker.operator,
|
|
125
141
|
// The steps stay in `steps` either way. A renderer decides whether to
|
|
126
142
|
// show a console walkthrough for a path nobody has asked for; the
|
|
127
143
|
// plan's job is to say the path exists and what opens it.
|
|
128
|
-
|
|
144
|
+
//
|
|
145
|
+
// Offered only where the manifest actually describes a client to
|
|
146
|
+
// register. Slack's does not — it asks for a token, never for a
|
|
147
|
+
// client id and secret — and `resolveOAuthClient` refuses the flag on
|
|
148
|
+
// exactly that ground, so printing it here would be handing somebody
|
|
149
|
+
// a command that answers back with "there is no such path".
|
|
150
|
+
...(hasOwnClientPath(manifest) ? { ownClientCommand: `${command} --own-client` } : {}),
|
|
129
151
|
}
|
|
130
152
|
: {}),
|
|
131
153
|
};
|
|
@@ -47,6 +47,13 @@ export interface SetupProviderOptions {
|
|
|
47
47
|
* here — one registry is built per profile, so each instance gets its own.
|
|
48
48
|
*/
|
|
49
49
|
readonly profile: string;
|
|
50
|
+
/**
|
|
51
|
+
* Which target this instance's stores came from.
|
|
52
|
+
*
|
|
53
|
+
* Stamped at construction for the same reason `profile` is, and it travels no
|
|
54
|
+
* further than the commands this provider emits — nothing here opens a store.
|
|
55
|
+
*/
|
|
56
|
+
readonly target: string;
|
|
50
57
|
/** Sibling profile names on this endpoint. Names only; already at `/health`. */
|
|
51
58
|
readonly profiles?: readonly string[];
|
|
52
59
|
/**
|
|
@@ -79,6 +86,7 @@ export function createSetupProvider(options: SetupProviderOptions): ProviderDefi
|
|
|
79
86
|
|
|
80
87
|
const context = () => ({
|
|
81
88
|
profile: options.profile,
|
|
89
|
+
target: options.target,
|
|
82
90
|
connections: reachable().map((connection) => connection.key),
|
|
83
91
|
...(options.ownClients ? { ownClients: options.ownClients } : {}),
|
|
84
92
|
});
|
|
@@ -309,6 +317,21 @@ function renderProvider(plan: ProviderPlan): string {
|
|
|
309
317
|
);
|
|
310
318
|
}
|
|
311
319
|
|
|
320
|
+
// An alternative, said as one. It is not a value the command above needs, and
|
|
321
|
+
// rendering it beside the requirements — which is what it did before there
|
|
322
|
+
// was anywhere else to put it — reads as a second mandatory step in a setup
|
|
323
|
+
// whose whole selling point is that it has none.
|
|
324
|
+
if (plan.tokenCommand && plan.pastedCredential) {
|
|
325
|
+
lines.push(
|
|
326
|
+
'',
|
|
327
|
+
'If the browser path is refused — a workspace that has not approved this app, which an ' +
|
|
328
|
+
`admin decides — the same command takes --auth pasted_token and asks for the ` +
|
|
329
|
+
`${plan.pastedCredential}:`,
|
|
330
|
+
` ${plan.tokenCommand}`,
|
|
331
|
+
' The console steps for obtaining one are in the setup documentation above.',
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
312
335
|
if (plan.browser) {
|
|
313
336
|
lines.push(
|
|
314
337
|
'',
|
|
@@ -1,68 +1,116 @@
|
|
|
1
1
|
import { defineProvider } from '#connectivity';
|
|
2
2
|
import { SLACK_REDACT } from './redact.ts';
|
|
3
|
+
import { SLACK_APP, SLACK_BROKER, SLACK_SCOPES } from './oauth.ts';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Slack, through the server Slack runs.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* loopback listener HTTPS. A broker would answer it and `defineProvider`
|
|
13
|
-
* refuses one on an mcp connector, because the SDK owns that exchange.
|
|
8
|
+
* Slack does not offer Dynamic Client Registration and is not going to: it
|
|
9
|
+
* would let a client authenticate a user without an app existing, and on
|
|
10
|
+
* Enterprise Grid an admin approves each app first. So a client has to be
|
|
11
|
+
* pre-registered — and the question this provider used to answer wrongly is
|
|
12
|
+
* *whose*.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* It was the operator's: create an app, transcribe sixteen user-token scopes,
|
|
15
|
+
* install it, paste the `xoxp-` it mints. That was the honest shape of it only
|
|
16
|
+
* while the alternative was believed impossible. It was not. Every client that
|
|
17
|
+
* reaches Slack without a console visit does the same thing — registers one app
|
|
18
|
+
* and ships its id — and this now does too, with the secret behind the broker
|
|
19
|
+
* ADR-028 already built for Google and the redirect on a port Slack has been
|
|
20
|
+
* told about. ADR-040 records what changed and why the reasoning in ADR-033 no
|
|
21
|
+
* longer holds.
|
|
19
22
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* except Google's, and that is the honest shape of it.
|
|
23
|
+
* The paste is still here, behind `--auth pasted_token`. A workspace whose admin has not
|
|
24
|
+
* approved the Lanes app cannot use the flow above, and that is not a decision
|
|
25
|
+
* the person running this command can make.
|
|
24
26
|
*/
|
|
25
27
|
export const slack = defineProvider({
|
|
26
28
|
id: 'slack',
|
|
27
29
|
name: 'Slack',
|
|
28
30
|
description: 'Messages, threads, channels, files, and canvases, via Slack\'s official MCP server.',
|
|
29
31
|
connector: { kind: 'mcp', endpoint: 'https://mcp.slack.com/mcp' },
|
|
30
|
-
auth: { kind: 'bearer' },
|
|
31
32
|
/**
|
|
32
|
-
*
|
|
33
|
+
* An mcp connector that names its own endpoints, which is what takes it off
|
|
34
|
+
* the SDK's flow and onto the one this repository drives.
|
|
33
35
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* Not an override of discovery for its own sake — Slack publishes perfectly
|
|
37
|
+
* good metadata at `/.well-known/oauth-authorization-server` and these two
|
|
38
|
+
* values are copied from it. It is that the SDK ends its flow by posting to
|
|
39
|
+
* the token endpoint with the client *it* holds, and the client here is held
|
|
40
|
+
* by a broker. Declaring the endpoints is how a manifest says the exchange is
|
|
41
|
+
* ours to route. See `defineProvider` and ADR-040.
|
|
42
|
+
*/
|
|
43
|
+
auth: {
|
|
44
|
+
kind: 'oauth',
|
|
45
|
+
registration: 'manual',
|
|
46
|
+
app: SLACK_APP,
|
|
47
|
+
authorize_url: 'https://slack.com/oauth/v2_user/authorize',
|
|
48
|
+
token_url: 'https://slack.com/api/oauth.v2.user.access',
|
|
49
|
+
scopes: [...SLACK_SCOPES],
|
|
50
|
+
broker: SLACK_BROKER,
|
|
51
|
+
/**
|
|
52
|
+
* Slack returns no refresh token, and that is the successful answer.
|
|
53
|
+
*
|
|
54
|
+
* A user token is long-lived unless token rotation is enabled on the app.
|
|
55
|
+
* Demanding one here would refuse every connection that worked — the
|
|
56
|
+
* default exists for Google, where a missing refresh token means the grant
|
|
57
|
+
* already existed and the connection would die in an hour.
|
|
58
|
+
*/
|
|
59
|
+
refresh_token: 'optional',
|
|
60
|
+
},
|
|
61
|
+
/**
|
|
62
|
+
* The person *and* the workspace, because either alone collides.
|
|
63
|
+
*
|
|
64
|
+
* `settleIdentity` matches a resolved account against existing connections to
|
|
65
|
+
* decide whether this is a reconnect or a new account, so the string has to
|
|
66
|
+
* be unique per credential. Neither half of `auth.test` is:
|
|
67
|
+
*
|
|
68
|
+
* - `team` alone — two people in one workspace look like one account, and
|
|
69
|
+
* the second connect overwrites the first's token.
|
|
70
|
+
* - `user` alone — Slack's "user" is a workspace-scoped handle, so one
|
|
71
|
+
* person in two workspaces looks like a reconnect and the second
|
|
72
|
+
* workspace overwrites the first. Connecting more than one workspace is
|
|
73
|
+
* the ordinary case here, which made this the more likely of the two.
|
|
74
|
+
*
|
|
75
|
+
* Together they are unique, and `alice (Acme)` is a row somebody can read.
|
|
39
76
|
*
|
|
40
77
|
* Slack answers a bad token with HTTP 200 and `{ok: false}`, so a wrong token
|
|
41
78
|
* reaches `connect`'s "which account is this?" fallback rather than a clear
|
|
42
79
|
* refusal. Discovery fails loudly one step later, which is where the real
|
|
43
80
|
* error is.
|
|
44
81
|
*/
|
|
45
|
-
identity: {
|
|
82
|
+
identity: {
|
|
83
|
+
kind: 'http',
|
|
84
|
+
url: 'https://slack.com/api/auth.test',
|
|
85
|
+
field: 'user',
|
|
86
|
+
qualifier: 'team',
|
|
87
|
+
},
|
|
46
88
|
redact: SLACK_REDACT,
|
|
89
|
+
/**
|
|
90
|
+
* Read only by `--auth pasted_token`. The browser route asks for nothing.
|
|
91
|
+
*
|
|
92
|
+
* `connection` scope rather than `shared` is what makes `--own-client` refuse
|
|
93
|
+
* with "no bring-your-own client path", which is true: this asks for a token,
|
|
94
|
+
* never for a client of the operator's to register.
|
|
95
|
+
*/
|
|
47
96
|
setup: {
|
|
48
97
|
summary:
|
|
49
|
-
'Slack needs
|
|
50
|
-
'
|
|
51
|
-
'
|
|
98
|
+
'Slack normally needs nothing set up — one browser round trip against the app Lanes ' +
|
|
99
|
+
'registered. Pasting a token is the way past a workspace whose admin has not approved ' +
|
|
100
|
+
'that app, using one from an app the workspace already trusts.',
|
|
52
101
|
docs: 'docs/detailed/setup/slack.md',
|
|
53
102
|
docs_url: 'https://api.slack.com/apps',
|
|
54
103
|
steps: [
|
|
55
|
-
'Open https://api.slack.com/apps and choose "Create New App" → "From scratch". Name it
|
|
56
|
-
'Open "OAuth & Permissions" and
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'The token does not expire unless you enable token rotation on the app. If you rotate or reinstall, run: lanes link connect slack --replace.',
|
|
104
|
+
'Open https://api.slack.com/apps and choose "Create New App" → "From scratch". Name it and pick the workspace.',
|
|
105
|
+
'Open "OAuth & Permissions" and add the scopes you need under USER TOKEN SCOPES — not Bot Token Scopes; the MCP server reads the user token. The full set this provider asks for in the browser is listed in docs/detailed/setup/slack.md.',
|
|
106
|
+
'Choose "Install to Workspace" and approve. A Slack admin may have to approve it for you.',
|
|
107
|
+
'Copy the "User OAuth Token". It starts with xoxp- — not the bot token, which starts with xoxb- and will not work here.',
|
|
108
|
+
'The token does not expire unless you enable token rotation on the app. If you rotate or reinstall, run: lanes link connect slack --profile personal --target local --auth pasted_token --replace.',
|
|
61
109
|
],
|
|
62
110
|
troubleshooting:
|
|
63
111
|
'Slack refused the token. The usual causes are a bot token (xoxb-) pasted where the user token (xoxp-) belongs, ' +
|
|
64
112
|
'a scope missing from USER TOKEN SCOPES, or an app that was reinstalled since — reinstalling mints a new token. ' +
|
|
65
|
-
'Copy the User OAuth Token from https://api.slack.com/apps and re-run: lanes link connect slack --replace.',
|
|
113
|
+
'Copy the User OAuth Token from https://api.slack.com/apps and re-run: lanes link connect slack --profile personal --target local --auth pasted_token --replace.',
|
|
66
114
|
prompts: [
|
|
67
115
|
{
|
|
68
116
|
key: 'token',
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { AuthBroker } from '#connectivity';
|
|
2
|
+
import { brokerOriginOverride } from '#connectivity/auth/index.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where Slack's client comes from, and why it cannot come from here.
|
|
6
|
+
*
|
|
7
|
+
* Slack refuses Dynamic Client Registration and says so in its documentation.
|
|
8
|
+
* That is not an omission waiting to be filled: DCR would let a client
|
|
9
|
+
* authenticate a user without an app existing, and on Enterprise Grid an admin
|
|
10
|
+
* approves each app before it can authenticate anyone. Waiting for it is
|
|
11
|
+
* waiting for something that is not coming.
|
|
12
|
+
*
|
|
13
|
+
* So somebody has to be pre-registered. Until now that somebody was the
|
|
14
|
+
* operator, once per person, in a browser tab, transcribing sixteen scopes. It
|
|
15
|
+
* is this project instead now — one app, registered once, exactly the
|
|
16
|
+
* arrangement every other client that connects to Slack without a console visit
|
|
17
|
+
* uses. See ADR-040.
|
|
18
|
+
*/
|
|
19
|
+
export const SLACK_APP = 'slack';
|
|
20
|
+
|
|
21
|
+
const BROKER_ORIGIN = 'https://api.lanes.sh';
|
|
22
|
+
const BROKER_PATH = '/v1/auth/link/slack';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The client Lanes operates, reached at an origin an override can move.
|
|
26
|
+
*
|
|
27
|
+
* Same shape as Google's and for the same reasons — see
|
|
28
|
+
* `../google/shared/oauth.ts`. What differs is what a broker outage costs.
|
|
29
|
+
* Slack issues a long-lived user token and no refresh token unless token
|
|
30
|
+
* rotation is switched on for the app, so this is consulted at `connect` and
|
|
31
|
+
* never again: an outage here cannot interrupt an agent mid-request the way
|
|
32
|
+
* ADR-028 warned a shared dependency can. That is the whole of why the fallback
|
|
33
|
+
* below is a reasonable second answer rather than a necessary one.
|
|
34
|
+
*/
|
|
35
|
+
export function slackBroker(env?: Record<string, string | undefined>): AuthBroker {
|
|
36
|
+
return {
|
|
37
|
+
url: `${brokerOriginOverride(env) ?? BROKER_ORIGIN}${BROKER_PATH}`,
|
|
38
|
+
operator: 'Lanes',
|
|
39
|
+
docs_url: 'https://lanes.sh/link#slack',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const SLACK_BROKER: AuthBroker = slackBroker();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Where Slack sends the browser back — and why it is not this machine.
|
|
47
|
+
*
|
|
48
|
+
* Slack refuses to register a Redirect URL that is not HTTPS. Verified against
|
|
49
|
+
* a real app: the field rejects `http://localhost:<port>/callback` outright, so
|
|
50
|
+
* a loopback listener cannot be named to Slack at all. A CLI cannot be HTTPS
|
|
51
|
+
* either — there is no certificate for 127.0.0.1 a browser will accept.
|
|
52
|
+
*
|
|
53
|
+
* So the redirect goes to the broker, which bounces it straight down to the
|
|
54
|
+
* listener `connect` opened, carrying the port in `state`. The URL itself is
|
|
55
|
+
* not written here: `/config` publishes it, because which one is correct
|
|
56
|
+
* depends on which deployment answered, and a broker running on loopback for a
|
|
57
|
+
* test would otherwise need its own spelling of it.
|
|
58
|
+
*
|
|
59
|
+
* The cost is that Slack cannot be connected without the broker. Google's is
|
|
60
|
+
* optional — a profile may register its own client and never call it — and
|
|
61
|
+
* Slack's is not, because the broker *is* the HTTPS origin. Recorded in
|
|
62
|
+
* ADR-040, and softened by Slack issuing no refresh token: an outage stops
|
|
63
|
+
* `connect`, never a connection already made.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What the browser grant asks for.
|
|
68
|
+
*
|
|
69
|
+
* These are user-token scopes: Slack's MCP server reads the user token, and a
|
|
70
|
+
* bot token is a different credential that does not work there at all. The list
|
|
71
|
+
* is what the setup page used to ask the operator to transcribe by hand, moved
|
|
72
|
+
* to where it can be shown before consent instead — which is what restores the
|
|
73
|
+
* scope-disclosure gate ADR-033 recorded as permanently absent for Slack.
|
|
74
|
+
*
|
|
75
|
+
* The last four are what `reactions`, `canvases`, and channel creation need.
|
|
76
|
+
* They are requested rather than left out because the tools appear in the list
|
|
77
|
+
* either way and fail at call time without them, and a tool that is visible and
|
|
78
|
+
* always fails is worse than a scope on the consent screen. What an agent may
|
|
79
|
+
* actually call is bounded by policy, where `connect` grants read and nothing
|
|
80
|
+
* else by default.
|
|
81
|
+
*/
|
|
82
|
+
export const SLACK_SCOPES = [
|
|
83
|
+
'search:read.public',
|
|
84
|
+
'search:read.private',
|
|
85
|
+
'search:read.im',
|
|
86
|
+
'search:read.mpim',
|
|
87
|
+
'search:read.users',
|
|
88
|
+
'search:read.files',
|
|
89
|
+
'channels:history',
|
|
90
|
+
'groups:history',
|
|
91
|
+
'im:history',
|
|
92
|
+
'mpim:history',
|
|
93
|
+
'channels:read',
|
|
94
|
+
'groups:read',
|
|
95
|
+
'mpim:read',
|
|
96
|
+
'users:read',
|
|
97
|
+
'chat:write',
|
|
98
|
+
'files:read',
|
|
99
|
+
'reactions:write',
|
|
100
|
+
'canvases:read',
|
|
101
|
+
'canvases:write',
|
|
102
|
+
'channels:write',
|
|
103
|
+
] as const;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ScopeMeaning } from '../scopes.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Slack's user-token scopes, said in words before the browser opens.
|
|
5
|
+
*
|
|
6
|
+
* These used to be sixteen lines of a setup page the operator transcribed into
|
|
7
|
+
* a console, where nothing described them and nothing could refuse. Asking for
|
|
8
|
+
* them in a browser is what makes `confirmScopes` apply to Slack at all — the
|
|
9
|
+
* gate ADR-033 recorded as permanently absent here.
|
|
10
|
+
*
|
|
11
|
+
* `broad` is set for the four that reach private conversations and the one that
|
|
12
|
+
* writes as the person. A grant of `search:read.private` is not "search" in the
|
|
13
|
+
* sense a reader assumes: it covers every private channel and DM the account
|
|
14
|
+
* can see, which is usually the most sensitive thing in the workspace.
|
|
15
|
+
*/
|
|
16
|
+
export const SLACK_SCOPE_MEANINGS: Record<string, ScopeMeaning> = {
|
|
17
|
+
'search:read.public': { meaning: 'search public channels' },
|
|
18
|
+
'search:read.private': { meaning: 'search private channels', broad: true },
|
|
19
|
+
'search:read.im': { meaning: 'search direct messages', broad: true },
|
|
20
|
+
'search:read.mpim': { meaning: 'search group direct messages', broad: true },
|
|
21
|
+
'search:read.users': { meaning: 'search people in the workspace' },
|
|
22
|
+
'search:read.files': { meaning: 'search files' },
|
|
23
|
+
'channels:history': { meaning: 'read messages in public channels' },
|
|
24
|
+
'groups:history': { meaning: 'read messages in private channels', broad: true },
|
|
25
|
+
'im:history': { meaning: 'read direct messages', broad: true },
|
|
26
|
+
'mpim:history': { meaning: 'read group direct messages', broad: true },
|
|
27
|
+
'channels:read': { meaning: 'list public channels' },
|
|
28
|
+
'groups:read': { meaning: 'list private channels' },
|
|
29
|
+
'mpim:read': { meaning: 'list group direct messages' },
|
|
30
|
+
'users:read': { meaning: 'read people and profiles' },
|
|
31
|
+
'chat:write': { meaning: 'send messages as you', broad: true },
|
|
32
|
+
'files:read': { meaning: 'read files and their contents' },
|
|
33
|
+
'reactions:write': { meaning: 'add and remove reactions as you' },
|
|
34
|
+
'canvases:read': { meaning: 'read canvases' },
|
|
35
|
+
'canvases:write': { meaning: 'create and edit canvases' },
|
|
36
|
+
'channels:write': { meaning: 'create and manage public channels' },
|
|
37
|
+
};
|
package/src/server/container.ts
CHANGED
|
@@ -24,7 +24,9 @@ import { streamLogger } from './logging.ts';
|
|
|
24
24
|
* nobody wrote — so the endpoint refuses rather than
|
|
25
25
|
* serving an empty workspace.
|
|
26
26
|
* LANES_LINK_TARGET which target's adapters to open. Baked to `cloud`.
|
|
27
|
-
* LANES_LINK_PROFILE
|
|
27
|
+
* LANES_LINK_PROFILE the primary profile this revision serves. Required —
|
|
28
|
+
* there is no workspace default any more (ADR-037), and
|
|
29
|
+
* `lanes link deploy` sets it at rollout.
|
|
28
30
|
* PORT injected by Cloud Run. 8080 is its default.
|
|
29
31
|
*/
|
|
30
32
|
|
|
@@ -42,6 +44,21 @@ if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
|
42
44
|
}
|
|
43
45
|
const host = env['LANES_LINK_HOST'] ?? '0.0.0.0';
|
|
44
46
|
|
|
47
|
+
// In this container's own voice, not the CLI's. A revision serves one primary
|
|
48
|
+
// profile — it decides the token that opens the endpoint — and nothing here can
|
|
49
|
+
// pick one. `lanes link deploy` sets the variable at rollout, in the same
|
|
50
|
+
// `gcloud run deploy` as the image, so the two never disagree; a container
|
|
51
|
+
// started by hand has to be told. Refusing here beats a CLI-shaped "pass
|
|
52
|
+
// --profile" reaching a log where there is no command line to pass it on.
|
|
53
|
+
if (!env['LANES_LINK_PROFILE']) {
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
'LANES_LINK_PROFILE is not set on this service. A deployed revision serves one\n' +
|
|
56
|
+
'primary profile and must be told which. `lanes link deploy` sets it at rollout;\n' +
|
|
57
|
+
'if this container was started by hand, pass -e LANES_LINK_PROFILE=<name>.\n',
|
|
58
|
+
);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
const log = (message: string): void => {
|
|
46
63
|
process.stdout.write(`${new Date().toISOString()} ${message}\n`);
|
|
47
64
|
};
|