runline 0.29.0 → 0.30.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/dist/auth/errors.d.ts +22 -0
- package/dist/auth/errors.js +25 -0
- package/dist/auth/oauth2.d.ts +21 -0
- package/dist/auth/oauth2.js +154 -0
- package/dist/auth/token.d.ts +11 -0
- package/dist/auth/token.js +231 -0
- package/dist/auth/types.d.ts +93 -0
- package/dist/auth/types.js +1 -0
- package/dist/commands/auth.js +26 -13
- package/dist/commands/connection.js +3 -6
- package/dist/commands/exec.js +7 -2
- package/dist/commands/init.js +9 -2
- package/dist/config/loader.d.ts +7 -13
- package/dist/config/loader.js +58 -88
- package/dist/config/store.d.ts +17 -0
- package/dist/config/store.js +85 -0
- package/dist/connections/file.d.ts +13 -0
- package/dist/connections/file.js +53 -0
- package/dist/connections/memory.d.ts +15 -0
- package/dist/connections/memory.js +70 -0
- package/dist/connections/types.d.ts +47 -0
- package/dist/connections/types.js +10 -0
- package/dist/core/engine.d.ts +5 -2
- package/dist/core/engine.js +48 -55
- package/dist/core/oauth.d.ts +20 -15
- package/dist/core/oauth.js +195 -98
- package/dist/credentials/http.d.ts +19 -0
- package/dist/credentials/http.js +109 -0
- package/dist/credentials/policy.d.ts +5 -0
- package/dist/credentials/policy.js +92 -0
- package/dist/credentials/registry.d.ts +31 -0
- package/dist/credentials/registry.js +171 -0
- package/dist/credentials/transport.d.ts +38 -0
- package/dist/credentials/transport.js +283 -0
- package/dist/credentials/types.d.ts +80 -0
- package/dist/credentials/types.js +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +8 -0
- package/dist/plugin/types.d.ts +30 -17
- package/dist/plugins/_shared/credentialAdapter.js +88 -0
- package/dist/plugins/_shared/googleAuth.js +89 -103
- package/dist/plugins/_shared/googleCredentials.js +160 -0
- package/dist/plugins/_shared/microsoftAuth.js +81 -74
- package/dist/plugins/_shared/microsoftCredentials.js +90 -0
- package/dist/plugins/_shared/tokenRefresh.js +39 -0
- package/dist/plugins/bitwarden/src/index.js +13 -31
- package/dist/plugins/gmail/src/index.js +8 -44
- package/dist/plugins/googleAppsScript/src/index.js +3 -25
- package/dist/plugins/googleCalendar/src/index.js +3 -41
- package/dist/plugins/googleContacts/src/index.js +3 -36
- package/dist/plugins/googleDocs/src/index.js +5 -0
- package/dist/plugins/googleDocs/src/shared.js +2 -30
- package/dist/plugins/googleDrive/src/index.js +52 -98
- package/dist/plugins/googleSheets/src/index.js +3 -40
- package/dist/plugins/googleSlides/src/index.js +5 -39
- package/dist/plugins/googleTasks/src/index.js +3 -30
- package/dist/plugins/halopsa/src/index.js +16 -31
- package/dist/plugins/microsoftCalendar/src/index.js +2 -1
- package/dist/plugins/microsoftFiles/src/index.js +10 -24
- package/dist/plugins/microsoftMail/src/index.js +2 -1
- package/dist/plugins/paypal/src/index.js +14 -29
- package/dist/plugins/plaud/src/index.js +210 -0
- package/dist/plugins/plaud/src/shared.js +221 -0
- package/dist/plugins/salesforce/src/shared.js +17 -32
- package/dist/sdk.d.ts +13 -5
- package/dist/sdk.js +35 -29
- package/dist/utils/observer.d.ts +2 -0
- package/dist/utils/observer.js +11 -0
- package/package.json +1 -1
package/dist/commands/exec.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { findConfigDir, loadConfig } from "../config/loader.js";
|
|
4
|
+
import { FileConnectionProvider } from "../connections/file.js";
|
|
3
5
|
import { ExecutionEngine } from "../core/engine.js";
|
|
4
6
|
import { loadAllPlugins } from "../plugin/loader.js";
|
|
5
7
|
import { registry } from "../plugin/registry.js";
|
|
@@ -9,7 +11,10 @@ export async function exec(code, options) {
|
|
|
9
11
|
await loadAllPlugins({
|
|
10
12
|
builtinAllowlist: new Set(config.connections.map((c) => c.plugin)),
|
|
11
13
|
});
|
|
12
|
-
const
|
|
14
|
+
const configDir = findConfigDir() ?? join(process.cwd(), ".runline");
|
|
15
|
+
const engine = new ExecutionEngine(registry, config, {
|
|
16
|
+
connectionProvider: new FileConnectionProvider(join(configDir, "config.json")),
|
|
17
|
+
});
|
|
13
18
|
if (options.file) {
|
|
14
19
|
if (!existsSync(code)) {
|
|
15
20
|
printError(`File not found: ${code}`);
|
package/dist/commands/init.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
+
import { updateConnectionFile } from "../config/store.js";
|
|
4
5
|
import { DEFAULT_CONFIG } from "../config/types.js";
|
|
5
6
|
import { printJson, printSuccess, printWarn } from "../utils/output.js";
|
|
6
7
|
export async function init(options) {
|
|
@@ -16,7 +17,13 @@ export async function init(options) {
|
|
|
16
17
|
}
|
|
17
18
|
mkdirSync(dir, { recursive: true });
|
|
18
19
|
mkdirSync(join(dir, "plugins"), { recursive: true });
|
|
19
|
-
|
|
20
|
+
const path = join(dir, "config.json");
|
|
21
|
+
await updateConnectionFile(path, (data) => {
|
|
22
|
+
if (existsSync(path))
|
|
23
|
+
return { result: undefined, write: false };
|
|
24
|
+
Object.assign(data, structuredClone(DEFAULT_CONFIG));
|
|
25
|
+
return { result: undefined, write: true };
|
|
26
|
+
});
|
|
20
27
|
if (options.json) {
|
|
21
28
|
printJson({ ok: true, path: dir });
|
|
22
29
|
}
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
import type { ConnectionConfig, ConnectionSchema } from "../plugin/types.js";
|
|
2
2
|
import { type RunlineConfig } from "./types.js";
|
|
3
3
|
export declare function findConfigDir(): string | null;
|
|
4
|
+
export declare function loadConfigFrom(path: string): RunlineConfig;
|
|
4
5
|
export declare function loadConfig(): RunlineConfig;
|
|
5
|
-
|
|
6
|
-
export declare function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* Used by plugins that need to persist refreshed OAuth tokens (or any
|
|
12
|
-
* other runtime-mutated credential) back to disk. The whole read-
|
|
13
|
-
* modify-write is guarded by a file lock so two concurrent `runline
|
|
14
|
-
* exec` processes refreshing the same token don't stomp each other.
|
|
15
|
-
*
|
|
16
|
-
* If the connection doesn't exist the call is a no-op.
|
|
17
|
-
*/
|
|
6
|
+
/** Replace the entire config, fencing handles bound to its previous accounts. */
|
|
7
|
+
export declare function saveConfig(config: RunlineConfig): Promise<void>;
|
|
8
|
+
/** A new generation fences handles from an earlier login with the same name. */
|
|
9
|
+
export declare function addConnection(name: string, plugin: string, configValues: Record<string, unknown>): Promise<void>;
|
|
10
|
+
export declare function removeConnection(name: string): Promise<boolean>;
|
|
11
|
+
/** Administrative patch. Plugin refreshes use their resolved connection handle. */
|
|
18
12
|
export declare function updateConnectionConfig(name: string, patch: Record<string, unknown>): Promise<void>;
|
|
19
13
|
export declare function getConnection(plugin: string, name?: string): ConnectionConfig | undefined;
|
|
20
14
|
export declare function applyEnvOverrides(conn: ConnectionConfig, schema?: ConnectionSchema): ConnectionConfig;
|
package/dist/config/loader.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
|
-
import lockfile from "proper-lockfile";
|
|
4
4
|
import { connectionFields } from "../plugin/schema.js";
|
|
5
|
+
import { readConnectionFile, updateConnectionFile } from "./store.js";
|
|
5
6
|
import { DEFAULT_CONFIG } from "./types.js";
|
|
6
7
|
const CONFIG_DIR_NAME = ".runline";
|
|
7
8
|
const CONFIG_FILE = "config.json";
|
|
@@ -18,108 +19,77 @@ export function findConfigDir() {
|
|
|
18
19
|
}
|
|
19
20
|
return null;
|
|
20
21
|
}
|
|
22
|
+
function configPath() {
|
|
23
|
+
return join(findConfigDir() ?? join(process.cwd(), CONFIG_DIR_NAME), CONFIG_FILE);
|
|
24
|
+
}
|
|
25
|
+
export function loadConfigFrom(path) {
|
|
26
|
+
return { ...DEFAULT_CONFIG, ...readConnectionFile(path) };
|
|
27
|
+
}
|
|
21
28
|
export function loadConfig() {
|
|
22
|
-
|
|
23
|
-
if (!configDir)
|
|
24
|
-
return { ...DEFAULT_CONFIG };
|
|
25
|
-
const configPath = join(configDir, CONFIG_FILE);
|
|
26
|
-
if (!existsSync(configPath))
|
|
27
|
-
return { ...DEFAULT_CONFIG };
|
|
28
|
-
try {
|
|
29
|
-
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
30
|
-
return {
|
|
31
|
-
...DEFAULT_CONFIG,
|
|
32
|
-
...raw,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return { ...DEFAULT_CONFIG };
|
|
37
|
-
}
|
|
29
|
+
return loadConfigFrom(configPath());
|
|
38
30
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
/** Replace the entire config, fencing handles bound to its previous accounts. */
|
|
32
|
+
export async function saveConfig(config) {
|
|
33
|
+
const replacement = structuredClone(config);
|
|
34
|
+
await updateConnectionFile(configPath(), (data) => {
|
|
35
|
+
for (const key of Object.keys(data))
|
|
36
|
+
delete data[key];
|
|
37
|
+
Object.assign(data, replacement, {
|
|
38
|
+
connections: replacement.connections.map((entry) => ({
|
|
39
|
+
...entry,
|
|
40
|
+
generation: randomUUID(),
|
|
41
|
+
})),
|
|
42
|
+
});
|
|
43
|
+
return { result: undefined, write: true };
|
|
44
|
+
});
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
config
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
/** A new generation fences handles from an earlier login with the same name. */
|
|
47
|
+
export async function addConnection(name, plugin, configValues) {
|
|
48
|
+
const conn = {
|
|
49
|
+
name,
|
|
50
|
+
plugin,
|
|
51
|
+
config: structuredClone(configValues),
|
|
52
|
+
generation: randomUUID(),
|
|
53
|
+
};
|
|
54
|
+
await updateConnectionFile(configPath(), (data) => {
|
|
55
|
+
const existing = data.connections.findIndex((c) => c.name === name);
|
|
56
|
+
if (existing >= 0)
|
|
57
|
+
data.connections[existing] = conn;
|
|
58
|
+
else
|
|
59
|
+
data.connections.push(conn);
|
|
60
|
+
return { result: undefined, write: true };
|
|
61
|
+
});
|
|
56
62
|
}
|
|
57
|
-
export function removeConnection(name) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
export async function removeConnection(name) {
|
|
64
|
+
return updateConnectionFile(configPath(), (data) => {
|
|
65
|
+
const idx = data.connections.findIndex((c) => c.name === name);
|
|
66
|
+
if (idx < 0)
|
|
67
|
+
return { result: false, write: false };
|
|
68
|
+
data.connections.splice(idx, 1);
|
|
69
|
+
return { result: true, write: true };
|
|
70
|
+
});
|
|
65
71
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Merge a partial config patch into an existing connection, atomically.
|
|
68
|
-
*
|
|
69
|
-
* Used by plugins that need to persist refreshed OAuth tokens (or any
|
|
70
|
-
* other runtime-mutated credential) back to disk. The whole read-
|
|
71
|
-
* modify-write is guarded by a file lock so two concurrent `runline
|
|
72
|
-
* exec` processes refreshing the same token don't stomp each other.
|
|
73
|
-
*
|
|
74
|
-
* If the connection doesn't exist the call is a no-op.
|
|
75
|
-
*/
|
|
72
|
+
/** Administrative patch. Plugin refreshes use their resolved connection handle. */
|
|
76
73
|
export async function updateConnectionConfig(name, patch) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
stale: 30_000,
|
|
85
|
-
realpath: false,
|
|
74
|
+
const copy = structuredClone(patch);
|
|
75
|
+
await updateConnectionFile(configPath(), (data) => {
|
|
76
|
+
const current = data.connections.find((entry) => entry.name === name);
|
|
77
|
+
if (!current)
|
|
78
|
+
throw new Error(`Connection not found: ${name}`);
|
|
79
|
+
current.config = { ...current.config, ...copy };
|
|
80
|
+
return { result: undefined, write: true };
|
|
86
81
|
});
|
|
87
|
-
try {
|
|
88
|
-
let raw;
|
|
89
|
-
try {
|
|
90
|
-
raw = {
|
|
91
|
-
...DEFAULT_CONFIG,
|
|
92
|
-
...JSON.parse(readFileSync(configPath, "utf-8")),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
raw = { ...DEFAULT_CONFIG };
|
|
97
|
-
}
|
|
98
|
-
const idx = raw.connections.findIndex((c) => c.name === name);
|
|
99
|
-
if (idx < 0)
|
|
100
|
-
return;
|
|
101
|
-
raw.connections[idx] = {
|
|
102
|
-
...raw.connections[idx],
|
|
103
|
-
config: { ...raw.connections[idx].config, ...patch },
|
|
104
|
-
};
|
|
105
|
-
writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`);
|
|
106
|
-
}
|
|
107
|
-
finally {
|
|
108
|
-
await release();
|
|
109
|
-
}
|
|
110
82
|
}
|
|
111
83
|
export function getConnection(plugin, name) {
|
|
112
84
|
const config = loadConfig();
|
|
113
|
-
|
|
114
|
-
return config.connections.find((c) => c.name === name);
|
|
115
|
-
return config.connections.find((c) => c.plugin === plugin);
|
|
85
|
+
return config.connections.find((c) => c.plugin === plugin && (name === undefined || c.name === name));
|
|
116
86
|
}
|
|
117
87
|
export function applyEnvOverrides(conn, schema) {
|
|
118
88
|
if (!schema)
|
|
119
89
|
return conn;
|
|
120
90
|
const config = { ...conn.config };
|
|
121
91
|
for (const [key, field] of Object.entries(connectionFields(schema))) {
|
|
122
|
-
if (field.env &&
|
|
92
|
+
if (field.env && config[key] == null) {
|
|
123
93
|
const envVal = process.env[field.env];
|
|
124
94
|
if (envVal)
|
|
125
95
|
config[key] = envVal;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ConnectionConfig } from "../plugin/types.js";
|
|
2
|
+
export interface ConnectionFile {
|
|
3
|
+
connections: Array<ConnectionConfig & {
|
|
4
|
+
generation?: string;
|
|
5
|
+
}>;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
/** Missing files are empty; invalid files never become empty credential stores. */
|
|
9
|
+
export declare function readConnectionFile(path: string): ConnectionFile;
|
|
10
|
+
/** All local writes share ownership across reread, async work, and atomic commit. */
|
|
11
|
+
export declare function updateConnectionFile<T>(path: string, update: (data: ConnectionFile) => Promise<{
|
|
12
|
+
result: T;
|
|
13
|
+
write: boolean;
|
|
14
|
+
}> | {
|
|
15
|
+
result: T;
|
|
16
|
+
write: boolean;
|
|
17
|
+
}): Promise<T>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import lockfile from "proper-lockfile";
|
|
5
|
+
/** Missing files are empty; invalid files never become empty credential stores. */
|
|
6
|
+
export function readConnectionFile(path) {
|
|
7
|
+
let text;
|
|
8
|
+
try {
|
|
9
|
+
text = readFileSync(path, "utf8");
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (error.code === "ENOENT")
|
|
13
|
+
return { connections: [] };
|
|
14
|
+
throw new Error("Invalid connection file: unreadable or malformed JSON");
|
|
15
|
+
}
|
|
16
|
+
let data;
|
|
17
|
+
try {
|
|
18
|
+
data = JSON.parse(text);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// JSON parser diagnostics can include secret-bearing input.
|
|
22
|
+
throw new Error("Invalid connection file: unreadable or malformed JSON");
|
|
23
|
+
}
|
|
24
|
+
if (!data ||
|
|
25
|
+
typeof data !== "object" ||
|
|
26
|
+
Array.isArray(data) ||
|
|
27
|
+
(data.connections !== undefined && !Array.isArray(data.connections))) {
|
|
28
|
+
throw new Error("Invalid connection file: expected connections array");
|
|
29
|
+
}
|
|
30
|
+
data.connections ??= [];
|
|
31
|
+
const names = new Set();
|
|
32
|
+
for (const entry of data.connections) {
|
|
33
|
+
if (!entry ||
|
|
34
|
+
typeof entry.name !== "string" ||
|
|
35
|
+
typeof entry.plugin !== "string" ||
|
|
36
|
+
!entry.config ||
|
|
37
|
+
typeof entry.config !== "object" ||
|
|
38
|
+
Array.isArray(entry.config) ||
|
|
39
|
+
(entry.generation !== undefined &&
|
|
40
|
+
typeof entry.generation !== "string") ||
|
|
41
|
+
names.has(entry.name)) {
|
|
42
|
+
throw new Error("Invalid connection file: malformed or duplicate connection");
|
|
43
|
+
}
|
|
44
|
+
names.add(entry.name);
|
|
45
|
+
}
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
/** All local writes share ownership across reread, async work, and atomic commit. */
|
|
49
|
+
export async function updateConnectionFile(path, update) {
|
|
50
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
51
|
+
let compromised;
|
|
52
|
+
const release = await lockfile.lock(path, {
|
|
53
|
+
realpath: false,
|
|
54
|
+
stale: 30_000,
|
|
55
|
+
retries: { retries: 10, factor: 2, minTimeout: 50, maxTimeout: 2_000 },
|
|
56
|
+
onCompromised: (error) => {
|
|
57
|
+
compromised = error;
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
if (compromised)
|
|
62
|
+
throw compromised;
|
|
63
|
+
const data = readConnectionFile(path);
|
|
64
|
+
const { result, write } = await update(data);
|
|
65
|
+
if (compromised)
|
|
66
|
+
throw compromised;
|
|
67
|
+
if (write) {
|
|
68
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
69
|
+
try {
|
|
70
|
+
writeFileSync(temporary, `${JSON.stringify(data, null, 2)}\n`, {
|
|
71
|
+
mode: 0o600,
|
|
72
|
+
flag: "wx",
|
|
73
|
+
});
|
|
74
|
+
renameSync(temporary, path);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
rmSync(temporary, { force: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await release();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ConnectionHandle, type ConnectionProvider, type ConnectionRequest } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Explicit CLI adapter for a config file. The absolute path is captured at
|
|
4
|
+
* construction; environment hints fill missing values only on this adapter.
|
|
5
|
+
* Runtime and config-management writes share a renewable file-wide lock.
|
|
6
|
+
* Manual file editors are not coordinated. This is plaintext local storage
|
|
7
|
+
* (writes use 0600), not an encrypted vault or a distributed credential broker.
|
|
8
|
+
*/
|
|
9
|
+
export declare class FileConnectionProvider implements ConnectionProvider {
|
|
10
|
+
readonly path: string;
|
|
11
|
+
constructor(path: string);
|
|
12
|
+
resolve(request: ConnectionRequest): Promise<ConnectionHandle>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { applyEnvOverrides } from "../config/loader.js";
|
|
3
|
+
import { readConnectionFile, updateConnectionFile, } from "../config/store.js";
|
|
4
|
+
import { connectionPatch, } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Explicit CLI adapter for a config file. The absolute path is captured at
|
|
7
|
+
* construction; environment hints fill missing values only on this adapter.
|
|
8
|
+
* Runtime and config-management writes share a renewable file-wide lock.
|
|
9
|
+
* Manual file editors are not coordinated. This is plaintext local storage
|
|
10
|
+
* (writes use 0600), not an encrypted vault or a distributed credential broker.
|
|
11
|
+
*/
|
|
12
|
+
export class FileConnectionProvider {
|
|
13
|
+
path;
|
|
14
|
+
constructor(path) {
|
|
15
|
+
this.path = resolve(path);
|
|
16
|
+
}
|
|
17
|
+
async resolve(request) {
|
|
18
|
+
const plugin = request.plugin;
|
|
19
|
+
const schema = structuredClone(request.schema);
|
|
20
|
+
const connection = readConnectionFile(this.path).connections.find((entry) => entry.plugin === plugin);
|
|
21
|
+
const name = connection?.name ?? plugin;
|
|
22
|
+
const generation = connection?.generation;
|
|
23
|
+
const hydrate = (entry) => applyEnvOverrides(structuredClone(entry), schema);
|
|
24
|
+
const find = (data) => data.connections.find((entry) => entry.name === name &&
|
|
25
|
+
entry.plugin === plugin &&
|
|
26
|
+
entry.generation === generation);
|
|
27
|
+
const missing = () => new Error(`Connection not found: ${name}`);
|
|
28
|
+
return {
|
|
29
|
+
read: async () => {
|
|
30
|
+
// A credential-free binding never attaches to an account created later.
|
|
31
|
+
if (!connection)
|
|
32
|
+
return hydrate({ name, plugin, config: {} });
|
|
33
|
+
const current = find(readConnectionFile(this.path));
|
|
34
|
+
if (!current)
|
|
35
|
+
throw missing();
|
|
36
|
+
return hydrate(current);
|
|
37
|
+
},
|
|
38
|
+
update: async (change) => {
|
|
39
|
+
if (!connection)
|
|
40
|
+
throw missing();
|
|
41
|
+
return updateConnectionFile(this.path, async (data) => {
|
|
42
|
+
const current = find(data);
|
|
43
|
+
if (!current)
|
|
44
|
+
throw missing();
|
|
45
|
+
const patch = await connectionPatch(change, hydrate(current).config);
|
|
46
|
+
if (patch !== undefined)
|
|
47
|
+
current.config = { ...current.config, ...patch };
|
|
48
|
+
return { result: hydrate(current), write: patch !== undefined };
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ConnectionConfig } from "../plugin/types.js";
|
|
2
|
+
import { type ConnectionHandle, type ConnectionProvider, type ConnectionRequest } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Process-local storage. Share this instance across engines to share credentials
|
|
5
|
+
* and update ownership. It neither reads environment variables nor touches disk.
|
|
6
|
+
*/
|
|
7
|
+
export declare class MemoryConnectionProvider implements ConnectionProvider {
|
|
8
|
+
private readonly connections;
|
|
9
|
+
private readonly pending;
|
|
10
|
+
constructor(connections?: ConnectionConfig[]);
|
|
11
|
+
/** Add new identities, never overwrite a credential during an in-flight refresh. */
|
|
12
|
+
add(connections: ConnectionConfig[]): void;
|
|
13
|
+
list(): ConnectionConfig[];
|
|
14
|
+
resolve(request: ConnectionRequest): Promise<ConnectionHandle>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { connectionPatch, } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Process-local storage. Share this instance across engines to share credentials
|
|
4
|
+
* and update ownership. It neither reads environment variables nor touches disk.
|
|
5
|
+
*/
|
|
6
|
+
export class MemoryConnectionProvider {
|
|
7
|
+
connections = new Map();
|
|
8
|
+
pending = new Map();
|
|
9
|
+
constructor(connections = []) {
|
|
10
|
+
this.add(connections);
|
|
11
|
+
}
|
|
12
|
+
/** Add new identities, never overwrite a credential during an in-flight refresh. */
|
|
13
|
+
add(connections) {
|
|
14
|
+
const names = new Set(this.connections.keys());
|
|
15
|
+
for (const connection of connections) {
|
|
16
|
+
if (names.has(connection.name))
|
|
17
|
+
throw new Error(`Duplicate connection: ${connection.name}`);
|
|
18
|
+
names.add(connection.name);
|
|
19
|
+
}
|
|
20
|
+
const copies = structuredClone(connections);
|
|
21
|
+
for (const connection of copies)
|
|
22
|
+
this.connections.set(connection.name, connection);
|
|
23
|
+
}
|
|
24
|
+
list() {
|
|
25
|
+
return structuredClone([...this.connections.values()]);
|
|
26
|
+
}
|
|
27
|
+
async resolve(request) {
|
|
28
|
+
let connection = [...this.connections.values()].find((value) => value.plugin === request.plugin);
|
|
29
|
+
if (!connection) {
|
|
30
|
+
// Credential-free plugins also have a mutable, process-local connection.
|
|
31
|
+
let name = request.plugin;
|
|
32
|
+
while (this.connections.has(name))
|
|
33
|
+
name = `_${name}`;
|
|
34
|
+
connection = { name, plugin: request.plugin, config: {} };
|
|
35
|
+
this.connections.set(name, connection);
|
|
36
|
+
}
|
|
37
|
+
const name = connection.name;
|
|
38
|
+
const read = () => {
|
|
39
|
+
const current = this.connections.get(name);
|
|
40
|
+
if (!current)
|
|
41
|
+
throw new Error(`Connection not found: ${name}`);
|
|
42
|
+
return structuredClone(current);
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
read: async () => read(),
|
|
46
|
+
update: async (change) => {
|
|
47
|
+
const previous = this.pending.get(name) ?? Promise.resolve();
|
|
48
|
+
const work = previous
|
|
49
|
+
.catch(() => { })
|
|
50
|
+
.then(async () => {
|
|
51
|
+
const current = read();
|
|
52
|
+
const patch = await connectionPatch(change, current.config);
|
|
53
|
+
if (patch !== undefined) {
|
|
54
|
+
current.config = { ...current.config, ...patch };
|
|
55
|
+
this.connections.set(name, current);
|
|
56
|
+
}
|
|
57
|
+
return structuredClone(current);
|
|
58
|
+
});
|
|
59
|
+
this.pending.set(name, work);
|
|
60
|
+
try {
|
|
61
|
+
return await work;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
if (this.pending.get(name) === work)
|
|
65
|
+
this.pending.delete(name);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ConnectionConfig, ConnectionSchema } from "../plugin/types.js";
|
|
2
|
+
export type ConnectionPatch = Record<string, unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* Runs once under the adapter's update ownership, against freshly read state.
|
|
5
|
+
* Return a shallow config patch, or undefined to keep the current state.
|
|
6
|
+
* May perform a token refresh. Must not recursively update the same store.
|
|
7
|
+
* A thrown error aborts the write; adapters must not automatically replay it.
|
|
8
|
+
*/
|
|
9
|
+
export type ConnectionUpdater = (current: Readonly<Record<string, unknown>>) => ConnectionPatch | undefined | Promise<ConnectionPatch | undefined>;
|
|
10
|
+
export type ConnectionUpdate = ConnectionPatch | ConnectionUpdater;
|
|
11
|
+
export interface ConnectionRequest {
|
|
12
|
+
plugin: string;
|
|
13
|
+
action?: string;
|
|
14
|
+
schema?: ConnectionSchema;
|
|
15
|
+
/** Host-supplied per-execution authority, never action input. */
|
|
16
|
+
context?: unknown;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* An adapter-owned binding to one resolved connection. Read and update must
|
|
20
|
+
* target that same identity even when other callers resolve different accounts.
|
|
21
|
+
* Returned snapshots must not alias authoritative storage.
|
|
22
|
+
*/
|
|
23
|
+
export interface ConnectionHandle {
|
|
24
|
+
read(): Promise<ConnectionConfig>;
|
|
25
|
+
/**
|
|
26
|
+
* Acquire ownership, re-read, run the updater, persist, then return the committed
|
|
27
|
+
* snapshot. Ownership spans the updater's async work, not just the final write.
|
|
28
|
+
* Coordinate across every writer sharing this storage; fail closed on failure
|
|
29
|
+
* to acquire ownership. Do not recreate a deleted connection. The host must
|
|
30
|
+
* fence lifecycle changes (disconnect/reconnect) against stale refreshes.
|
|
31
|
+
*
|
|
32
|
+
* This is not a distributed transaction with the remote provider: a provider
|
|
33
|
+
* may rotate a token before a write fails. Surface that failure; never silently
|
|
34
|
+
* report the replacement token as durably saved or replay the callback.
|
|
35
|
+
*/
|
|
36
|
+
update(change: ConnectionUpdate): Promise<ConnectionConfig>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Trusted-host integration seam. Resolve and authorize on each action invocation;
|
|
40
|
+
* rejection never falls back to environment variables or local files.
|
|
41
|
+
* These handles are for trusted plugin code, not a security boundary for arbitrary
|
|
42
|
+
* code. A broker for untrusted plugins must expose constrained requests instead.
|
|
43
|
+
*/
|
|
44
|
+
export interface ConnectionProvider {
|
|
45
|
+
resolve(request: ConnectionRequest): Promise<ConnectionHandle>;
|
|
46
|
+
}
|
|
47
|
+
export declare function connectionPatch(change: ConnectionUpdate, current: Record<string, unknown>): Promise<ConnectionPatch | undefined>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export async function connectionPatch(change, current) {
|
|
2
|
+
const patch = typeof change === "function"
|
|
3
|
+
? await change(structuredClone(current))
|
|
4
|
+
: change;
|
|
5
|
+
if (patch !== undefined &&
|
|
6
|
+
(patch === null || typeof patch !== "object" || Array.isArray(patch))) {
|
|
7
|
+
throw new Error("Connection update must return an object patch or undefined");
|
|
8
|
+
}
|
|
9
|
+
return patch === undefined ? undefined : structuredClone(patch);
|
|
10
|
+
}
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RunlineConfig } from "../config/types.js";
|
|
2
|
+
import type { ConnectionProvider } from "../connections/types.js";
|
|
2
3
|
import type { PluginRegistry } from "../plugin/registry.js";
|
|
3
4
|
export interface ExecuteResult {
|
|
4
5
|
result: unknown;
|
|
@@ -31,11 +32,13 @@ export interface ActionInvocation {
|
|
|
31
32
|
error?: unknown;
|
|
32
33
|
}
|
|
33
34
|
export interface EngineHooks {
|
|
35
|
+
/** Host-owned resolution and persistence. Defaults to process-local memory. */
|
|
36
|
+
connectionProvider?: ConnectionProvider;
|
|
34
37
|
/**
|
|
35
38
|
* Observer for every action invocation. Must not throw — but if it
|
|
36
39
|
* does, the engine swallows it: observability never breaks a run.
|
|
37
40
|
*/
|
|
38
|
-
onAction?: (info: ActionInvocation) => void
|
|
41
|
+
onAction?: (info: ActionInvocation) => void | Promise<void>;
|
|
39
42
|
}
|
|
40
43
|
/**
|
|
41
44
|
* Whether to arm the host-side RSS watchdog for a worker run.
|
|
@@ -84,6 +87,7 @@ export declare class ExecutionEngine {
|
|
|
84
87
|
private registry;
|
|
85
88
|
private config;
|
|
86
89
|
private hooks;
|
|
90
|
+
private connectionProvider;
|
|
87
91
|
private pooled;
|
|
88
92
|
private readonly maxRunsPerWorker;
|
|
89
93
|
constructor(registry: PluginRegistry, config: RunlineConfig, hooks?: EngineHooks);
|
|
@@ -116,7 +120,6 @@ export declare class ExecutionEngine {
|
|
|
116
120
|
* teach the caller — agents read error messages and self-correct.
|
|
117
121
|
*/
|
|
118
122
|
private selfPrefixHint;
|
|
119
|
-
private resolveConnection;
|
|
120
123
|
}
|
|
121
124
|
/**
|
|
122
125
|
* Normalize a caller's code into a function body. Kept host-side so the
|