runline 0.3.0 → 0.3.2
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/dist/core/oauth.d.ts +62 -6
- package/dist/core/oauth.js +129 -49
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/plugin/loader.d.ts +23 -1
- package/dist/plugin/loader.js +11 -4
- package/dist/sdk.d.ts +20 -2
- package/dist/sdk.js +23 -3
- package/package.json +1 -1
package/dist/core/oauth.d.ts
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generic OAuth2 authorization-code flow.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* `setOAuth(config)`: opens the provider's consent URL, catches
|
|
6
|
-
* the redirect on a local port, exchanges the code for tokens,
|
|
7
|
-
* and returns `{accessToken, refreshToken, expiresAt, scope}`.
|
|
4
|
+
* Two surfaces:
|
|
8
5
|
*
|
|
9
|
-
*
|
|
10
|
-
* the
|
|
6
|
+
* 1. `runOAuth(config, {clientId, clientSecret})` — end-to-end CLI
|
|
7
|
+
* flow. Opens the browser, captures the redirect on a pinned
|
|
8
|
+
* localhost port, exchanges the code, returns tokens. Used by
|
|
9
|
+
* `runline auth <plugin>`.
|
|
10
|
+
*
|
|
11
|
+
* 2. Primitives for callers that can't run a local callback server
|
|
12
|
+
* (hosted apps, GUIs, anywhere the user's browser doesn't talk
|
|
13
|
+
* back to the process driving the flow):
|
|
14
|
+
*
|
|
15
|
+
* - `generatePKCE()` — S256 verifier + challenge
|
|
16
|
+
* - `buildAuthUrl(config, opts)` — assemble the consent URL
|
|
17
|
+
* - `exchangeAuthCode(config, opts)` — POST to the token
|
|
18
|
+
* endpoint, get back tokens
|
|
19
|
+
*
|
|
20
|
+
* The caller orchestrates: generates state + PKCE, builds the
|
|
21
|
+
* URL, shows it to the user, receives the code back however it
|
|
22
|
+
* wants (redirect-URI paste, browser popup with postMessage,
|
|
23
|
+
* public HTTPS callback, …), calls exchangeAuthCode.
|
|
11
24
|
*/
|
|
12
25
|
import type { OAuthConfig } from "../plugin/types.js";
|
|
13
26
|
/**
|
|
@@ -19,6 +32,8 @@ import type { OAuthConfig } from "../plugin/types.js";
|
|
|
19
32
|
* URI with the provider after changing it).
|
|
20
33
|
*/
|
|
21
34
|
export declare const OAUTH_CALLBACK_PORT: number;
|
|
35
|
+
/** Canonical localhost redirect URI for CLI-based flows. */
|
|
36
|
+
export declare const OAUTH_CALLBACK_URI: string;
|
|
22
37
|
export interface OAuthTokens {
|
|
23
38
|
accessToken: string;
|
|
24
39
|
refreshToken: string;
|
|
@@ -38,9 +53,50 @@ export interface RunOAuthOptions {
|
|
|
38
53
|
/** Override the browser launcher. Defaults to OS-appropriate command. */
|
|
39
54
|
openBrowser?: (url: string) => void;
|
|
40
55
|
}
|
|
56
|
+
export interface BuildAuthUrlOptions {
|
|
57
|
+
clientId: string;
|
|
58
|
+
redirectUri: string;
|
|
59
|
+
state: string;
|
|
60
|
+
/** S256 code challenge. Pass alongside a verifier kept by the driver. */
|
|
61
|
+
pkceChallenge?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface ExchangeCodeOptions {
|
|
64
|
+
clientId: string;
|
|
65
|
+
clientSecret: string;
|
|
66
|
+
code: string;
|
|
67
|
+
redirectUri: string;
|
|
68
|
+
/** PKCE verifier matching the challenge sent on the auth URL. */
|
|
69
|
+
codeVerifier?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface PKCEPair {
|
|
72
|
+
verifier: string;
|
|
73
|
+
/** Base64url SHA-256 of the verifier, for `code_challenge_method=S256`. */
|
|
74
|
+
challenge: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Generate a PKCE verifier and its SHA-256 challenge. The verifier
|
|
78
|
+
* stays with the driver until the token exchange; the challenge
|
|
79
|
+
* goes on the auth URL.
|
|
80
|
+
*/
|
|
81
|
+
export declare function generatePKCE(): PKCEPair;
|
|
82
|
+
/**
|
|
83
|
+
* Assemble the authorization URL for a plugin's OAuth config.
|
|
84
|
+
* Pass `pkceChallenge` to include PKCE; omit for plain auth-code.
|
|
85
|
+
*/
|
|
86
|
+
export declare function buildAuthUrl(config: OAuthConfig, opts: BuildAuthUrlOptions): string;
|
|
87
|
+
/**
|
|
88
|
+
* Exchange an authorization code for tokens. Throws if the
|
|
89
|
+
* provider doesn't return a refresh_token — that's almost always a
|
|
90
|
+
* misconfiguration (e.g. missing `access_type=offline` for Google).
|
|
91
|
+
*/
|
|
92
|
+
export declare function exchangeAuthCode(config: OAuthConfig, opts: ExchangeCodeOptions): Promise<OAuthTokens>;
|
|
41
93
|
/**
|
|
42
94
|
* Run the full OAuth2 authorization-code flow end-to-end.
|
|
43
95
|
* Resolves with the exchanged tokens once the user completes the
|
|
44
96
|
* browser consent and the token endpoint returns a refresh token.
|
|
97
|
+
*
|
|
98
|
+
* Uses the pinned localhost callback port. If you can't run a
|
|
99
|
+
* local callback server, drive the flow yourself with
|
|
100
|
+
* `buildAuthUrl` + `exchangeAuthCode`.
|
|
45
101
|
*/
|
|
46
102
|
export declare function runOAuth(config: OAuthConfig, options: RunOAuthOptions): Promise<OAuthTokens>;
|
package/dist/core/oauth.js
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generic OAuth2 authorization-code flow.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* `setOAuth(config)`: opens the provider's consent URL, catches
|
|
6
|
-
* the redirect on a local port, exchanges the code for tokens,
|
|
7
|
-
* and returns `{accessToken, refreshToken, expiresAt, scope}`.
|
|
4
|
+
* Two surfaces:
|
|
8
5
|
*
|
|
9
|
-
*
|
|
10
|
-
* the
|
|
6
|
+
* 1. `runOAuth(config, {clientId, clientSecret})` — end-to-end CLI
|
|
7
|
+
* flow. Opens the browser, captures the redirect on a pinned
|
|
8
|
+
* localhost port, exchanges the code, returns tokens. Used by
|
|
9
|
+
* `runline auth <plugin>`.
|
|
10
|
+
*
|
|
11
|
+
* 2. Primitives for callers that can't run a local callback server
|
|
12
|
+
* (hosted apps, GUIs, anywhere the user's browser doesn't talk
|
|
13
|
+
* back to the process driving the flow):
|
|
14
|
+
*
|
|
15
|
+
* - `generatePKCE()` — S256 verifier + challenge
|
|
16
|
+
* - `buildAuthUrl(config, opts)` — assemble the consent URL
|
|
17
|
+
* - `exchangeAuthCode(config, opts)` — POST to the token
|
|
18
|
+
* endpoint, get back tokens
|
|
19
|
+
*
|
|
20
|
+
* The caller orchestrates: generates state + PKCE, builds the
|
|
21
|
+
* URL, shows it to the user, receives the code back however it
|
|
22
|
+
* wants (redirect-URI paste, browser popup with postMessage,
|
|
23
|
+
* public HTTPS callback, …), calls exchangeAuthCode.
|
|
11
24
|
*/
|
|
12
25
|
import { spawn } from "node:child_process";
|
|
26
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
13
27
|
import { createServer, } from "node:http";
|
|
14
28
|
/**
|
|
15
29
|
* Fixed callback port for OAuth redirects. Pinned so users can
|
|
@@ -28,40 +42,116 @@ export const OAUTH_CALLBACK_PORT = (() => {
|
|
|
28
42
|
}
|
|
29
43
|
return 47823;
|
|
30
44
|
})();
|
|
45
|
+
/** Canonical localhost redirect URI for CLI-based flows. */
|
|
46
|
+
export const OAUTH_CALLBACK_URI = `http://127.0.0.1:${OAUTH_CALLBACK_PORT}/callback`;
|
|
47
|
+
// ─── Primitives ──────────────────────────────────────────────────
|
|
48
|
+
/**
|
|
49
|
+
* Generate a PKCE verifier and its SHA-256 challenge. The verifier
|
|
50
|
+
* stays with the driver until the token exchange; the challenge
|
|
51
|
+
* goes on the auth URL.
|
|
52
|
+
*/
|
|
53
|
+
export function generatePKCE() {
|
|
54
|
+
const verifier = base64urlEncode(randomBytes(32));
|
|
55
|
+
const challenge = base64urlEncode(createHash("sha256").update(verifier).digest());
|
|
56
|
+
return { verifier, challenge };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Assemble the authorization URL for a plugin's OAuth config.
|
|
60
|
+
* Pass `pkceChallenge` to include PKCE; omit for plain auth-code.
|
|
61
|
+
*/
|
|
62
|
+
export function buildAuthUrl(config, opts) {
|
|
63
|
+
const url = new URL(config.authUrl);
|
|
64
|
+
url.searchParams.set("client_id", opts.clientId);
|
|
65
|
+
url.searchParams.set("redirect_uri", opts.redirectUri);
|
|
66
|
+
url.searchParams.set("response_type", "code");
|
|
67
|
+
url.searchParams.set("scope", config.scopes.join(" "));
|
|
68
|
+
url.searchParams.set("state", opts.state);
|
|
69
|
+
if (opts.pkceChallenge) {
|
|
70
|
+
url.searchParams.set("code_challenge", opts.pkceChallenge);
|
|
71
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
72
|
+
}
|
|
73
|
+
for (const [k, v] of Object.entries(config.authParams ?? {})) {
|
|
74
|
+
url.searchParams.set(k, v);
|
|
75
|
+
}
|
|
76
|
+
return url.toString();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Exchange an authorization code for tokens. Throws if the
|
|
80
|
+
* provider doesn't return a refresh_token — that's almost always a
|
|
81
|
+
* misconfiguration (e.g. missing `access_type=offline` for Google).
|
|
82
|
+
*/
|
|
83
|
+
export async function exchangeAuthCode(config, opts) {
|
|
84
|
+
const body = new URLSearchParams({
|
|
85
|
+
code: opts.code,
|
|
86
|
+
client_id: opts.clientId,
|
|
87
|
+
client_secret: opts.clientSecret,
|
|
88
|
+
redirect_uri: opts.redirectUri,
|
|
89
|
+
grant_type: "authorization_code",
|
|
90
|
+
});
|
|
91
|
+
if (opts.codeVerifier)
|
|
92
|
+
body.set("code_verifier", opts.codeVerifier);
|
|
93
|
+
const res = await fetch(config.tokenUrl, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
96
|
+
body: body.toString(),
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
throw new Error(`OAuth: token exchange failed (${res.status}): ${await res.text()}`);
|
|
100
|
+
}
|
|
101
|
+
const data = (await res.json());
|
|
102
|
+
if (!data.refresh_token) {
|
|
103
|
+
throw new Error("OAuth: provider did not return a refresh token. This usually means a prior consent is cached — revoke access with the provider and retry. For Google, set authParams: { access_type: 'offline', prompt: 'consent' }.");
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
accessToken: data.access_token,
|
|
107
|
+
refreshToken: data.refresh_token,
|
|
108
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
109
|
+
scope: data.scope,
|
|
110
|
+
tokenType: data.token_type,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// ─── End-to-end CLI flow ─────────────────────────────────────────
|
|
31
114
|
/**
|
|
32
115
|
* Run the full OAuth2 authorization-code flow end-to-end.
|
|
33
116
|
* Resolves with the exchanged tokens once the user completes the
|
|
34
117
|
* browser consent and the token endpoint returns a refresh token.
|
|
118
|
+
*
|
|
119
|
+
* Uses the pinned localhost callback port. If you can't run a
|
|
120
|
+
* local callback server, drive the flow yourself with
|
|
121
|
+
* `buildAuthUrl` + `exchangeAuthCode`.
|
|
35
122
|
*/
|
|
36
123
|
export async function runOAuth(config, options) {
|
|
37
|
-
const
|
|
38
|
-
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
124
|
+
const redirectUri = OAUTH_CALLBACK_URI;
|
|
39
125
|
const state = randomState();
|
|
40
|
-
const
|
|
41
|
-
authUrl
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
126
|
+
const { verifier, challenge } = generatePKCE();
|
|
127
|
+
const authUrl = buildAuthUrl(config, {
|
|
128
|
+
clientId: options.clientId,
|
|
129
|
+
redirectUri,
|
|
130
|
+
state,
|
|
131
|
+
pkceChallenge: challenge,
|
|
132
|
+
});
|
|
133
|
+
// Always make the URL visible. `onAuthUrl` is the preferred
|
|
134
|
+
// channel (CLI formats it inline); if absent, fall back to
|
|
135
|
+
// stderr so headless invocations (SSH, CI) aren't left waiting
|
|
136
|
+
// on a browser that never opens.
|
|
137
|
+
if (options.onAuthUrl) {
|
|
138
|
+
options.onAuthUrl(authUrl);
|
|
48
139
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
open(authUrl.toString());
|
|
52
|
-
const { code } = await captureCode(port, state);
|
|
53
|
-
const tokens = await exchangeCode(config.tokenUrl, code, options.clientId, options.clientSecret, redirectUri);
|
|
54
|
-
if (!tokens.refresh_token) {
|
|
55
|
-
throw new Error("OAuth: provider did not return a refresh token. This usually means a prior consent is cached — revoke access with the provider and retry. For Google, set authParams: { access_type: 'offline', prompt: 'consent' }.");
|
|
140
|
+
else {
|
|
141
|
+
console.error(`Open this URL to authorize:\n ${authUrl}`);
|
|
56
142
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
143
|
+
const open = options.openBrowser ?? defaultOpenBrowser;
|
|
144
|
+
open(authUrl);
|
|
145
|
+
const { code } = await captureCode(OAUTH_CALLBACK_PORT, state);
|
|
146
|
+
return exchangeAuthCode(config, {
|
|
147
|
+
clientId: options.clientId,
|
|
148
|
+
clientSecret: options.clientSecret,
|
|
149
|
+
code,
|
|
150
|
+
redirectUri,
|
|
151
|
+
codeVerifier: verifier,
|
|
152
|
+
});
|
|
64
153
|
}
|
|
154
|
+
// ─── Local callback server ───────────────────────────────────────
|
|
65
155
|
function captureCode(port, expectedState) {
|
|
66
156
|
return new Promise((resolve, reject) => {
|
|
67
157
|
const server = createServer((req, res) => {
|
|
@@ -106,26 +196,16 @@ function captureCode(port, expectedState) {
|
|
|
106
196
|
server.listen(port, "127.0.0.1");
|
|
107
197
|
});
|
|
108
198
|
}
|
|
109
|
-
|
|
110
|
-
const body = new URLSearchParams({
|
|
111
|
-
code,
|
|
112
|
-
client_id: clientId,
|
|
113
|
-
client_secret: clientSecret,
|
|
114
|
-
redirect_uri: redirectUri,
|
|
115
|
-
grant_type: "authorization_code",
|
|
116
|
-
});
|
|
117
|
-
const res = await fetch(tokenUrl, {
|
|
118
|
-
method: "POST",
|
|
119
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
120
|
-
body: body.toString(),
|
|
121
|
-
});
|
|
122
|
-
if (!res.ok) {
|
|
123
|
-
throw new Error(`OAuth: token exchange failed (${res.status}): ${await res.text()}`);
|
|
124
|
-
}
|
|
125
|
-
return (await res.json());
|
|
126
|
-
}
|
|
199
|
+
// ─── Helpers ─────────────────────────────────────────────────────
|
|
127
200
|
function randomState() {
|
|
128
|
-
return (
|
|
201
|
+
return base64urlEncode(randomBytes(16));
|
|
202
|
+
}
|
|
203
|
+
function base64urlEncode(buf) {
|
|
204
|
+
return buf
|
|
205
|
+
.toString("base64")
|
|
206
|
+
.replace(/\+/g, "-")
|
|
207
|
+
.replace(/\//g, "_")
|
|
208
|
+
.replace(/=+$/, "");
|
|
129
209
|
}
|
|
130
210
|
function defaultOpenBrowser(url) {
|
|
131
211
|
const cmd = process.platform === "darwin"
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export type { RunlineConfig } from "./config/types.js";
|
|
|
3
3
|
export { DEFAULT_CONFIG } from "./config/types.js";
|
|
4
4
|
export type { EngineOptions, ExecuteResult } from "./core/engine.js";
|
|
5
5
|
export { ExecutionEngine } from "./core/engine.js";
|
|
6
|
-
export type { OAuthTokens, RunOAuthOptions } from "./core/oauth.js";
|
|
7
|
-
export { OAUTH_CALLBACK_PORT, runOAuth } from "./core/oauth.js";
|
|
6
|
+
export type { BuildAuthUrlOptions, ExchangeCodeOptions, OAuthTokens, PKCEPair, RunOAuthOptions, } from "./core/oauth.js";
|
|
7
|
+
export { buildAuthUrl, exchangeAuthCode, generatePKCE, OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_URI, runOAuth, } from "./core/oauth.js";
|
|
8
8
|
export type { ActionDefinition, PluginFunction, RunlinePluginAPI, SchemaField, } from "./plugin/api.js";
|
|
9
9
|
export { createPluginAPI, isPluginFunction, resolvePluginExport, } from "./plugin/api.js";
|
|
10
10
|
export type { InstalledPlugin, PluginSource } from "./plugin/installer.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { addConnection, findConfigDir, getConnection, loadConfig, removeConnection, saveConfig, updateConnectionConfig, } from "./config/loader.js";
|
|
2
2
|
export { DEFAULT_CONFIG } from "./config/types.js";
|
|
3
3
|
export { ExecutionEngine } from "./core/engine.js";
|
|
4
|
-
export { OAUTH_CALLBACK_PORT, runOAuth } from "./core/oauth.js";
|
|
4
|
+
export { buildAuthUrl, exchangeAuthCode, generatePKCE, OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_URI, runOAuth, } from "./core/oauth.js";
|
|
5
5
|
export { createPluginAPI, isPluginFunction, resolvePluginExport, } from "./plugin/api.js";
|
|
6
6
|
export { installPlugin, listInstalled, parsePluginSource, removePlugin, } from "./plugin/installer.js";
|
|
7
7
|
export { discoverPlugins, loadAllPlugins, loadPluginFromPath, loadPluginsFromConfig, } from "./plugin/loader.js";
|
package/dist/plugin/loader.d.ts
CHANGED
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
import type { PluginDef } from "./types.js";
|
|
2
2
|
export declare function loadPluginFromPath(path: string): Promise<PluginDef>;
|
|
3
3
|
export declare function loadPluginsFromConfig(configDir: string): Promise<PluginDef[]>;
|
|
4
|
+
export interface DiscoverOptions {
|
|
5
|
+
/**
|
|
6
|
+
* When supplied, only built-in plugins whose name is in this set
|
|
7
|
+
* are loaded. Plugins discovered in the project dir, `plugins.json`,
|
|
8
|
+
* or `~/.runline/plugins` are always loaded — users put them there
|
|
9
|
+
* deliberately — but the 188 bundled builtins are gated so agents
|
|
10
|
+
* don't see every possible action regardless of configuration.
|
|
11
|
+
*
|
|
12
|
+
* Omit to load every builtin (CLI default: `runline actions` etc.
|
|
13
|
+
* surfaces the full catalog).
|
|
14
|
+
*/
|
|
15
|
+
builtinAllowlist?: Set<string> | null;
|
|
16
|
+
/**
|
|
17
|
+
* Override the directory where bundled plugins live. Default is
|
|
18
|
+
* `<loader>/../plugins`, which resolves to `dist/plugins/` at
|
|
19
|
+
* runtime. Tests set this so they can exercise allowlist logic
|
|
20
|
+
* without depending on the real bundled catalog.
|
|
21
|
+
*/
|
|
22
|
+
builtinDir?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Default path to the bundled plugin directory. */
|
|
25
|
+
export declare function defaultBuiltinDir(): string;
|
|
4
26
|
/**
|
|
5
27
|
* Discover and return all plugins from a config directory and global dir.
|
|
6
28
|
* Does NOT mutate any global state.
|
|
7
29
|
*/
|
|
8
|
-
export declare function discoverPlugins(configDir?: string | null): Promise<PluginDef[]>;
|
|
30
|
+
export declare function discoverPlugins(configDir?: string | null, options?: DiscoverOptions): Promise<PluginDef[]>;
|
|
9
31
|
/**
|
|
10
32
|
* Load all plugins and register them into the global registry.
|
|
11
33
|
* Used by the CLI.
|
package/dist/plugin/loader.js
CHANGED
|
@@ -118,11 +118,15 @@ export async function loadPluginsFromConfig(configDir) {
|
|
|
118
118
|
}
|
|
119
119
|
return plugins;
|
|
120
120
|
}
|
|
121
|
+
/** Default path to the bundled plugin directory. */
|
|
122
|
+
export function defaultBuiltinDir() {
|
|
123
|
+
return join(__dirname, "..", "plugins");
|
|
124
|
+
}
|
|
121
125
|
/**
|
|
122
126
|
* Discover and return all plugins from a config directory and global dir.
|
|
123
127
|
* Does NOT mutate any global state.
|
|
124
128
|
*/
|
|
125
|
-
export async function discoverPlugins(configDir) {
|
|
129
|
+
export async function discoverPlugins(configDir, options = {}) {
|
|
126
130
|
const loaded = new Set();
|
|
127
131
|
const result = [];
|
|
128
132
|
function addIfNew(plugin) {
|
|
@@ -144,11 +148,14 @@ export async function discoverPlugins(configDir) {
|
|
|
144
148
|
const globalPlugins = await loadFromDirectory(globalDir);
|
|
145
149
|
for (const p of globalPlugins)
|
|
146
150
|
addIfNew(p);
|
|
147
|
-
|
|
148
|
-
const builtinDir = join(__dirname, "..", "plugins");
|
|
151
|
+
const builtinDir = options.builtinDir ?? defaultBuiltinDir();
|
|
149
152
|
const builtinPlugins = await loadFromDirectory(builtinDir);
|
|
150
|
-
for (const p of builtinPlugins)
|
|
153
|
+
for (const p of builtinPlugins) {
|
|
154
|
+
if (options.builtinAllowlist && !options.builtinAllowlist.has(p.name)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
151
157
|
addIfNew(p);
|
|
158
|
+
}
|
|
152
159
|
return result;
|
|
153
160
|
}
|
|
154
161
|
/**
|
package/dist/sdk.d.ts
CHANGED
|
@@ -34,8 +34,26 @@ export declare class Runline {
|
|
|
34
34
|
connections(): ConnectionConfig[];
|
|
35
35
|
/**
|
|
36
36
|
* Load runline from a project directory.
|
|
37
|
-
*
|
|
37
|
+
*
|
|
38
|
+
* Discovers the `.runline/` config and registers:
|
|
39
|
+
* - every plugin dropped into `.runline/plugins/`,
|
|
40
|
+
* - every plugin listed in `.runline/plugins.json`,
|
|
41
|
+
* - every plugin in `~/.runline/plugins/`,
|
|
42
|
+
* - and — from the 188 builtins shipped with the package — only
|
|
43
|
+
* the ones named in `config.connections[].plugin`.
|
|
44
|
+
*
|
|
45
|
+
* Gating the builtins keeps `runline.actions()` scoped to what the
|
|
46
|
+
* project actually configured. Without this, a project with a
|
|
47
|
+
* single connection would still expose every bundled action to an
|
|
48
|
+
* agent, which is both noisy and a privacy problem (the agent sees
|
|
49
|
+
* surface area it has no credentials for).
|
|
50
|
+
*
|
|
51
|
+
* `options.builtinDir` is a test-only hook; production callers
|
|
52
|
+
* should rely on the default path to the bundled plugins.
|
|
53
|
+
*
|
|
38
54
|
* Fully self-contained — does not mutate global state.
|
|
39
55
|
*/
|
|
40
|
-
static fromProject(cwd?: string
|
|
56
|
+
static fromProject(cwd?: string, options?: {
|
|
57
|
+
builtinDir?: string;
|
|
58
|
+
}): Promise<Runline | null>;
|
|
41
59
|
}
|
package/dist/sdk.js
CHANGED
|
@@ -63,16 +63,36 @@ export class Runline {
|
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* Load runline from a project directory.
|
|
66
|
-
*
|
|
66
|
+
*
|
|
67
|
+
* Discovers the `.runline/` config and registers:
|
|
68
|
+
* - every plugin dropped into `.runline/plugins/`,
|
|
69
|
+
* - every plugin listed in `.runline/plugins.json`,
|
|
70
|
+
* - every plugin in `~/.runline/plugins/`,
|
|
71
|
+
* - and — from the 188 builtins shipped with the package — only
|
|
72
|
+
* the ones named in `config.connections[].plugin`.
|
|
73
|
+
*
|
|
74
|
+
* Gating the builtins keeps `runline.actions()` scoped to what the
|
|
75
|
+
* project actually configured. Without this, a project with a
|
|
76
|
+
* single connection would still expose every bundled action to an
|
|
77
|
+
* agent, which is both noisy and a privacy problem (the agent sees
|
|
78
|
+
* surface area it has no credentials for).
|
|
79
|
+
*
|
|
80
|
+
* `options.builtinDir` is a test-only hook; production callers
|
|
81
|
+
* should rely on the default path to the bundled plugins.
|
|
82
|
+
*
|
|
67
83
|
* Fully self-contained — does not mutate global state.
|
|
68
84
|
*/
|
|
69
|
-
static async fromProject(cwd) {
|
|
85
|
+
static async fromProject(cwd, options = {}) {
|
|
70
86
|
const dir = cwd ?? process.cwd();
|
|
71
87
|
const configDir = findRunlineDir(dir);
|
|
72
88
|
if (!configDir)
|
|
73
89
|
return null;
|
|
74
90
|
const config = loadConfigFrom(configDir);
|
|
75
|
-
const
|
|
91
|
+
const builtinAllowlist = new Set(config.connections.map((c) => c.plugin));
|
|
92
|
+
const plugins = await discoverPlugins(configDir, {
|
|
93
|
+
builtinAllowlist,
|
|
94
|
+
builtinDir: options.builtinDir,
|
|
95
|
+
});
|
|
76
96
|
const rl = new Runline({
|
|
77
97
|
connections: config.connections,
|
|
78
98
|
timeoutMs: config.timeoutMs,
|