railcode 0.1.23 → 0.1.25
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 +1 -1
- package/dist/connections.js +24 -1
- package/dist/connectors.js +4 -1
- package/dist/index.js +349 -4
- package/dist/manifest.js +15 -1
- package/dist/observability.js +1 -1
- package/package.json +1 -1
- package/static/sdk.js +113 -8
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ railcode roles <list|create|update|delete|add-member|remove-member|grants|grant|
|
|
|
32
32
|
Org roles + the granular grants table (admin)
|
|
33
33
|
railcode apps <list|show|access|set-access|transfer|delete> ...
|
|
34
34
|
Apps + access policy (owner/admin)
|
|
35
|
-
railcode analytics <app> [--range 7d|30d|90d] Per-app pageview analytics
|
|
35
|
+
railcode analytics <app> [--range 1d|7d|30d|90d] Per-app pageview analytics
|
|
36
36
|
railcode logs <connector|service-connector|llm|email|agent> [filters]
|
|
37
37
|
Org observability logs (admin)
|
|
38
38
|
```
|
package/dist/connections.js
CHANGED
|
@@ -83,12 +83,16 @@ export function parseAuthType(value) {
|
|
|
83
83
|
}
|
|
84
84
|
return v;
|
|
85
85
|
}
|
|
86
|
+
// A null content_type is a pre-default row: the proxy still sends application/json with
|
|
87
|
+
// any body, so show the effective value rather than a bare "-".
|
|
88
|
+
export const DEFAULT_CONTENT_TYPE = "application/json";
|
|
86
89
|
export function serviceConnectorAdminTable(list) {
|
|
87
|
-
return formatTable(["name", "base_url", "auth", "cred", "methods", "enabled", "native"], list.map((c) => [
|
|
90
|
+
return formatTable(["name", "base_url", "auth", "cred", "content_type", "methods", "enabled", "native"], list.map((c) => [
|
|
88
91
|
c.name,
|
|
89
92
|
c.base_url,
|
|
90
93
|
c.auth_type,
|
|
91
94
|
c.has_credential ? "yes" : "no",
|
|
95
|
+
c.content_type || DEFAULT_CONTENT_TYPE,
|
|
92
96
|
(c.allowed_methods ?? []).join(", ") || "all",
|
|
93
97
|
c.enabled ? "yes" : "no",
|
|
94
98
|
c.native_key ?? "-",
|
|
@@ -141,11 +145,30 @@ export function buildServiceConnectorCreateBody(input) {
|
|
|
141
145
|
body.auth_config = input.authConfig;
|
|
142
146
|
if (input.description !== undefined)
|
|
143
147
|
body.description = input.description;
|
|
148
|
+
// Omitted → the server stores application/json. Only send the field when the caller
|
|
149
|
+
// actually chose a type (e.g. a form-encoded API).
|
|
150
|
+
if (input.contentType !== undefined)
|
|
151
|
+
body.content_type = parseContentType(input.contentType);
|
|
144
152
|
if (input.allowedMethods && input.allowedMethods.length > 0) {
|
|
145
153
|
body.allowed_methods = input.allowedMethods;
|
|
146
154
|
}
|
|
147
155
|
return body;
|
|
148
156
|
}
|
|
157
|
+
// `--content-type <type>`: a single header value, so no CR/LF and nothing blank (an
|
|
158
|
+
// empty value would read as "unset" server-side — just leave the flag off for that).
|
|
159
|
+
export function parseContentType(value) {
|
|
160
|
+
const text = value.trim();
|
|
161
|
+
if (!text) {
|
|
162
|
+
throw new ConnectionAdminError("--content-type requires a value (e.g. application/x-www-form-urlencoded); omit it for application/json.");
|
|
163
|
+
}
|
|
164
|
+
if (/[\r\n]/.test(text)) {
|
|
165
|
+
throw new ConnectionAdminError("--content-type must not contain newlines.");
|
|
166
|
+
}
|
|
167
|
+
if (text.length > 255) {
|
|
168
|
+
throw new ConnectionAdminError("--content-type must be 255 characters or fewer.");
|
|
169
|
+
}
|
|
170
|
+
return text;
|
|
171
|
+
}
|
|
149
172
|
export function parseMethodList(value) {
|
|
150
173
|
if (value === undefined || value.trim() === "")
|
|
151
174
|
return [];
|
package/dist/connectors.js
CHANGED
|
@@ -49,7 +49,10 @@ export function connectorOpenapiOutput(docs) {
|
|
|
49
49
|
// API docs (link or inline), and the OpenAPI spec (link or inline) when present.
|
|
50
50
|
export function renderConnectorDocs(docs) {
|
|
51
51
|
const methods = (docs.allowed_methods ?? []).join(", ") || "(all)";
|
|
52
|
-
const
|
|
52
|
+
const identity = [`${docs.name} (${docs.auth_type}) — methods: ${methods}`];
|
|
53
|
+
if (docs.content_type)
|
|
54
|
+
identity.push(`body Content-Type: ${docs.content_type}`);
|
|
55
|
+
const sections = [identity.join("\n")];
|
|
53
56
|
if (docs.description)
|
|
54
57
|
sections.push(docs.description);
|
|
55
58
|
const hasAny = docs.usage_instructions ||
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,8 @@ Usage:
|
|
|
47
47
|
Service connectors: call them, or manage them (admin)
|
|
48
48
|
railcode connections <list|create|delete> ... Manage org data connectors (admin)
|
|
49
49
|
railcode llm <providers|models> List the LLM providers/models apps can call
|
|
50
|
+
railcode personal-connectors <list|tools|connect|call> ...
|
|
51
|
+
Your own connected accounts (Gmail, ...): discover + test
|
|
50
52
|
railcode manifest <validate|show> ... Validate ${APP_MANIFEST_NAME} locally / show an app's ratified manifest
|
|
51
53
|
railcode agent <list|show|create|update|delete|run|schedule> ...
|
|
52
54
|
Manage org-scoped managed agents
|
|
@@ -54,7 +56,7 @@ Usage:
|
|
|
54
56
|
railcode roles <list|create|update|delete|add-member|remove-member|grants|grant|revoke|materialize|effective|catalog> ...
|
|
55
57
|
Manage org roles + the granular grants table (admin)
|
|
56
58
|
railcode apps <list|show|access|set-access|transfer|delete> ... Manage apps + access policy
|
|
57
|
-
railcode analytics <app> [--range 7d|30d|90d] Per-app pageview analytics (admin/owner)
|
|
59
|
+
railcode analytics <app> [--range 1d|7d|30d|90d] Per-app pageview analytics (admin/owner)
|
|
58
60
|
railcode logs <connector|service-connector|llm|email|agent> [filters]
|
|
59
61
|
Read org observability logs (admin)
|
|
60
62
|
railcode --version
|
|
@@ -112,6 +114,11 @@ async function main() {
|
|
|
112
114
|
case "llm":
|
|
113
115
|
await commandLlm(args);
|
|
114
116
|
return;
|
|
117
|
+
case "personal-connectors":
|
|
118
|
+
case "personal-connector":
|
|
119
|
+
case "pc":
|
|
120
|
+
await commandPersonalConnectors(args);
|
|
121
|
+
return;
|
|
115
122
|
case "member":
|
|
116
123
|
case "members":
|
|
117
124
|
await commandMembers(args);
|
|
@@ -4740,15 +4747,22 @@ Admin (service-connector:manage):
|
|
|
4740
4747
|
railcode connector enable <key> --credentials '{...}'
|
|
4741
4748
|
One-click enable/rotate a native preset
|
|
4742
4749
|
railcode connector create --name <n> --base-url <url> [--auth-type <t>]
|
|
4743
|
-
[--auth-config '{...}'] [--methods GET,POST] [--description <d>]
|
|
4750
|
+
[--auth-config '{...}'] [--methods GET,POST] [--description <d>]
|
|
4751
|
+
[--content-type <type>] [--disabled]
|
|
4744
4752
|
Create/replace a custom connector
|
|
4745
4753
|
railcode connector delete <name|uuid> Delete a service connector
|
|
4746
4754
|
|
|
4747
4755
|
Aliases: connector = connectors; list = ls, connectors; docs = doc; fetch = request.
|
|
4748
4756
|
|
|
4757
|
+
Create options:
|
|
4758
|
+
--content-type <type> Content-Type sent with request bodies
|
|
4759
|
+
(default application/json; e.g.
|
|
4760
|
+
application/x-www-form-urlencoded for a form API)
|
|
4761
|
+
|
|
4749
4762
|
List options:
|
|
4750
4763
|
--admin Manage view: base_url, credential + enabled
|
|
4751
|
-
state, native key
|
|
4764
|
+
state, content type, native key
|
|
4765
|
+
(service-connector:manage)
|
|
4752
4766
|
--json Print the raw connector array
|
|
4753
4767
|
|
|
4754
4768
|
Docs options:
|
|
@@ -4846,6 +4860,7 @@ async function commandConnectorCreate(args) {
|
|
|
4846
4860
|
"authConfig",
|
|
4847
4861
|
"authConfigFile",
|
|
4848
4862
|
"description",
|
|
4863
|
+
"contentType",
|
|
4849
4864
|
"methods",
|
|
4850
4865
|
"disabled",
|
|
4851
4866
|
"apiUrl",
|
|
@@ -4860,6 +4875,7 @@ async function commandConnectorCreate(args) {
|
|
|
4860
4875
|
authType,
|
|
4861
4876
|
authConfig,
|
|
4862
4877
|
description: optionString(args, "description"),
|
|
4878
|
+
contentType: optionString(args, "contentType"),
|
|
4863
4879
|
allowedMethods: parseMethodList(optionString(args, "methods")),
|
|
4864
4880
|
enabled: args.options.disabled !== true,
|
|
4865
4881
|
});
|
|
@@ -5094,6 +5110,164 @@ function assertKnownLlmOptions(args, allowed) {
|
|
|
5094
5110
|
}
|
|
5095
5111
|
}
|
|
5096
5112
|
// ---------------------------------------------------------------------------
|
|
5113
|
+
// Personal connectors: your OWN connected accounts (Gmail, Slack, ...).
|
|
5114
|
+
//
|
|
5115
|
+
// Everything here hits the NON-org-scoped `/api/personal-connections/*` plane —
|
|
5116
|
+
// a personal connection belongs to the human, not the org. It's the same
|
|
5117
|
+
// resource the console's Connectors tab drives. `call` executes as YOU against
|
|
5118
|
+
// YOUR own connection (an identity op, not an app), so it's how you smoke-test a
|
|
5119
|
+
// tool from the terminal before wiring it into an app or agent.
|
|
5120
|
+
// ---------------------------------------------------------------------------
|
|
5121
|
+
const PC_HELP = `railcode personal-connectors — your own connected accounts (Gmail, Slack, ...)
|
|
5122
|
+
|
|
5123
|
+
Usage:
|
|
5124
|
+
railcode personal-connectors list Toolkits this deployment brokers + your status
|
|
5125
|
+
railcode personal-connectors tools <toolkit> The callable tools of a toolkit (and their inputs)
|
|
5126
|
+
railcode personal-connectors connect <toolkit> Print the OAuth URL to connect it (open in a browser)
|
|
5127
|
+
railcode personal-connectors call <tool> [--args '<json>'] Run one tool on your own account
|
|
5128
|
+
|
|
5129
|
+
Aliases: personal-connector, pc
|
|
5130
|
+
|
|
5131
|
+
Discovery is what you build against: \`tools <toolkit> --json\` shows each tool's
|
|
5132
|
+
input schema, so you know what to put in an app's \`personal_connectors:\` and what
|
|
5133
|
+
\`personalConnections.call('<tool>', {...})\` expects.
|
|
5134
|
+
|
|
5135
|
+
Options:
|
|
5136
|
+
--json Print raw JSON instead of a table
|
|
5137
|
+
--args '<json>' Arguments object for \`call\` (default {})
|
|
5138
|
+
`;
|
|
5139
|
+
async function commandPersonalConnectors(args) {
|
|
5140
|
+
const sub = args.rest[0] ?? "";
|
|
5141
|
+
switch (sub) {
|
|
5142
|
+
case "list":
|
|
5143
|
+
await commandPcList(args);
|
|
5144
|
+
return;
|
|
5145
|
+
case "tools":
|
|
5146
|
+
await commandPcTools(args);
|
|
5147
|
+
return;
|
|
5148
|
+
case "connect":
|
|
5149
|
+
await commandPcConnect(args);
|
|
5150
|
+
return;
|
|
5151
|
+
case "call":
|
|
5152
|
+
await commandPcCall(args);
|
|
5153
|
+
return;
|
|
5154
|
+
case "":
|
|
5155
|
+
case "help":
|
|
5156
|
+
console.log(PC_HELP);
|
|
5157
|
+
return;
|
|
5158
|
+
default:
|
|
5159
|
+
throw new CliError(`Unknown personal-connectors command: ${sub}\n\n${PC_HELP}`, 2);
|
|
5160
|
+
}
|
|
5161
|
+
}
|
|
5162
|
+
// A 503 here means the DEPLOYMENT has personal connectors turned off — a state to
|
|
5163
|
+
// report plainly, not a failure. Anything else is a real error.
|
|
5164
|
+
function pcUnavailable(status) {
|
|
5165
|
+
return status === 503;
|
|
5166
|
+
}
|
|
5167
|
+
async function commandPcList(args) {
|
|
5168
|
+
rejectUnknownOptions(args, ["json", "apiUrl"], "personal-connectors list", PC_HELP);
|
|
5169
|
+
const config = requireLogin(args);
|
|
5170
|
+
const resp = await authedFetch(config, `/api/personal-connections/toolkits`);
|
|
5171
|
+
if (resp.status === 401)
|
|
5172
|
+
await reLoginOrThrow();
|
|
5173
|
+
if (pcUnavailable(resp.status)) {
|
|
5174
|
+
console.log("Personal connectors are not enabled on this deployment.");
|
|
5175
|
+
return;
|
|
5176
|
+
}
|
|
5177
|
+
const toolkits = (await readJsonOrThrow(resp, "List toolkits"));
|
|
5178
|
+
if (args.options.json) {
|
|
5179
|
+
console.log(JSON.stringify(toolkits, null, 2));
|
|
5180
|
+
return;
|
|
5181
|
+
}
|
|
5182
|
+
if (toolkits.length === 0) {
|
|
5183
|
+
console.log("No connectors are available yet.");
|
|
5184
|
+
return;
|
|
5185
|
+
}
|
|
5186
|
+
console.log(formatTable(["TOOLKIT", "NAME", "STATUS"], toolkits.map((t) => [t.slug, t.name, t.connected ? "connected" : t.status ?? "not connected"])));
|
|
5187
|
+
}
|
|
5188
|
+
async function commandPcTools(args) {
|
|
5189
|
+
rejectUnknownOptions(args, ["json", "apiUrl"], "personal-connectors tools", PC_HELP);
|
|
5190
|
+
const toolkit = args.rest[1];
|
|
5191
|
+
if (!toolkit)
|
|
5192
|
+
throw new CliError("Usage: railcode personal-connectors tools <toolkit>", 2);
|
|
5193
|
+
const config = requireLogin(args);
|
|
5194
|
+
const resp = await authedFetch(config, `/api/personal-connections/toolkits/${encodeURIComponent(toolkit)}/tools`);
|
|
5195
|
+
if (resp.status === 401)
|
|
5196
|
+
await reLoginOrThrow();
|
|
5197
|
+
if (pcUnavailable(resp.status)) {
|
|
5198
|
+
console.log("Personal connectors are not enabled on this deployment.");
|
|
5199
|
+
return;
|
|
5200
|
+
}
|
|
5201
|
+
const tools = (await readJsonOrThrow(resp, `List ${toolkit} tools`));
|
|
5202
|
+
if (args.options.json) {
|
|
5203
|
+
// --json carries the input_schema, which is what you actually build against.
|
|
5204
|
+
console.log(JSON.stringify(tools, null, 2));
|
|
5205
|
+
return;
|
|
5206
|
+
}
|
|
5207
|
+
if (tools.length === 0) {
|
|
5208
|
+
console.log(`No tools found for "${toolkit}".`);
|
|
5209
|
+
return;
|
|
5210
|
+
}
|
|
5211
|
+
console.log(formatTable(["TOOL", "DESCRIPTION"], tools.map((t) => [t.slug, t.description])));
|
|
5212
|
+
console.log(`\n${tools.length} tool(s). Add \`--json\` to see each tool's input schema.`);
|
|
5213
|
+
}
|
|
5214
|
+
async function commandPcConnect(args) {
|
|
5215
|
+
rejectUnknownOptions(args, ["json", "apiUrl"], "personal-connectors connect", PC_HELP);
|
|
5216
|
+
const toolkit = args.rest[1];
|
|
5217
|
+
if (!toolkit)
|
|
5218
|
+
throw new CliError("Usage: railcode personal-connectors connect <toolkit>", 2);
|
|
5219
|
+
const config = requireLogin(args);
|
|
5220
|
+
// Connecting is browser-bound OAuth; the CLI can't complete it. It prints the
|
|
5221
|
+
// URL and leaves opening it to you — the same handoff `railcode login` uses.
|
|
5222
|
+
const resp = await authedFetch(config, `/api/personal-connections/${encodeURIComponent(toolkit)}/connect`, { method: "POST" });
|
|
5223
|
+
if (resp.status === 401)
|
|
5224
|
+
await reLoginOrThrow();
|
|
5225
|
+
if (pcUnavailable(resp.status)) {
|
|
5226
|
+
console.log("Personal connectors are not enabled on this deployment.");
|
|
5227
|
+
return;
|
|
5228
|
+
}
|
|
5229
|
+
const body = (await readJsonOrThrow(resp, `Connect ${toolkit}`));
|
|
5230
|
+
if (args.options.json) {
|
|
5231
|
+
console.log(JSON.stringify(body, null, 2));
|
|
5232
|
+
return;
|
|
5233
|
+
}
|
|
5234
|
+
console.log(`Open this URL to connect ${toolkit}, then re-run your command:\n\n ${body.redirect_url}\n`);
|
|
5235
|
+
}
|
|
5236
|
+
async function commandPcCall(args) {
|
|
5237
|
+
rejectUnknownOptions(args, ["json", "args", "apiUrl"], "personal-connectors call", PC_HELP);
|
|
5238
|
+
const tool = args.rest[1];
|
|
5239
|
+
if (!tool)
|
|
5240
|
+
throw new CliError("Usage: railcode personal-connectors call <tool> [--args '<json>']", 2);
|
|
5241
|
+
let toolArgs = {};
|
|
5242
|
+
if (typeof args.options.args === "string") {
|
|
5243
|
+
try {
|
|
5244
|
+
toolArgs = JSON.parse(args.options.args);
|
|
5245
|
+
}
|
|
5246
|
+
catch {
|
|
5247
|
+
throw new CliError(`--args must be a JSON object (got: ${args.options.args})`, 2);
|
|
5248
|
+
}
|
|
5249
|
+
}
|
|
5250
|
+
const config = requireLogin(args);
|
|
5251
|
+
const resp = await authedFetch(config, `/api/personal-connections/call`, {
|
|
5252
|
+
method: "POST",
|
|
5253
|
+
headers: { "Content-Type": "application/json" },
|
|
5254
|
+
body: JSON.stringify({ tool, arguments: toolArgs }),
|
|
5255
|
+
});
|
|
5256
|
+
if (resp.status === 401)
|
|
5257
|
+
await reLoginOrThrow();
|
|
5258
|
+
if (pcUnavailable(resp.status)) {
|
|
5259
|
+
console.log("Personal connectors are not enabled on this deployment.");
|
|
5260
|
+
return;
|
|
5261
|
+
}
|
|
5262
|
+
// A 409 means "connect this toolkit first" — surface the server's hint verbatim
|
|
5263
|
+
// rather than a generic failure, since it names the exact command to run.
|
|
5264
|
+
if (resp.status === 409) {
|
|
5265
|
+
throw new CliError(await errorDetail(resp), 1);
|
|
5266
|
+
}
|
|
5267
|
+
const body = await readJsonOrThrow(resp, `Call ${tool}`);
|
|
5268
|
+
console.log(JSON.stringify(body, null, 2));
|
|
5269
|
+
}
|
|
5270
|
+
// ---------------------------------------------------------------------------
|
|
5097
5271
|
// Shared admin helpers (members / roles / apps / analytics / logs / connections)
|
|
5098
5272
|
//
|
|
5099
5273
|
// All of these hit the org-scoped `/api/organizations/{org}/...` management APIs
|
|
@@ -5692,7 +5866,7 @@ async function commandAppsDelete(args) {
|
|
|
5692
5866
|
const ANALYTICS_HELP = `railcode analytics — per-app pageview analytics (admin/owner)
|
|
5693
5867
|
|
|
5694
5868
|
Usage:
|
|
5695
|
-
railcode analytics <app> [--range 7d|30d|90d] Views, uniques, daily, top paths/users
|
|
5869
|
+
railcode analytics <app> [--range 1d|7d|30d|90d] Views, uniques, daily, top paths/users
|
|
5696
5870
|
|
|
5697
5871
|
<app> is an app slug or UUID. Default range 30d.
|
|
5698
5872
|
|
|
@@ -6173,6 +6347,7 @@ async function commandDev(args) {
|
|
|
6173
6347
|
fileStores: new Map(),
|
|
6174
6348
|
port: requestedPort,
|
|
6175
6349
|
config,
|
|
6350
|
+
personalConnectors: readAppPersonalConnectors(cwd),
|
|
6176
6351
|
};
|
|
6177
6352
|
// Asset mode needs its deps before we bind anything; the check is cheap and
|
|
6178
6353
|
// failing early avoids leaving the proxy port bound on a misconfigured app.
|
|
@@ -6673,6 +6848,10 @@ async function handleLocalApi(ctx, req, res, url) {
|
|
|
6673
6848
|
await handleFiles(ctx, req, res, path, url);
|
|
6674
6849
|
return;
|
|
6675
6850
|
}
|
|
6851
|
+
if (path === "/personal-connections" || path.startsWith("/personal-connections/")) {
|
|
6852
|
+
await handlePersonalConnections(ctx, req, res, path, url);
|
|
6853
|
+
return;
|
|
6854
|
+
}
|
|
6676
6855
|
// Proxied surfaces → forward to the real instance with the saved token.
|
|
6677
6856
|
if (path === "/connections" ||
|
|
6678
6857
|
path === "/queries" ||
|
|
@@ -7021,6 +7200,172 @@ export function devUpstreamAuthFallback(status, degradeOk) {
|
|
|
7021
7200
|
},
|
|
7022
7201
|
};
|
|
7023
7202
|
}
|
|
7203
|
+
// ── personal connectors in dev ────────────────────────────────────────────────
|
|
7204
|
+
//
|
|
7205
|
+
// The SDK's `personalConnections.*` methods hit the app plane in prod, which is
|
|
7206
|
+
// bounded by the app's ratified manifest. `railcode dev` has no ratified app, so
|
|
7207
|
+
// it reproduces that bound HERE from the local manifest.yaml and forwards the
|
|
7208
|
+
// actual work to the USER plane (`/api/personal-connections/*`) as the signed-in
|
|
7209
|
+
// developer — the same "run as you, not as a deployed app" model dev uses for
|
|
7210
|
+
// SQL and LLM. The result: declared tools run against your real connection,
|
|
7211
|
+
// undeclared ones are refused exactly as they will be in prod, so a bad call
|
|
7212
|
+
// fails locally instead of only after deploy.
|
|
7213
|
+
function readAppPersonalConnectors(cwd) {
|
|
7214
|
+
const path = join(cwd, APP_MANIFEST_NAME);
|
|
7215
|
+
if (!existsSync(path))
|
|
7216
|
+
return [];
|
|
7217
|
+
try {
|
|
7218
|
+
return parseManifestYaml(readFileSync(path, "utf8")).personal_connectors ?? [];
|
|
7219
|
+
}
|
|
7220
|
+
catch {
|
|
7221
|
+
// A malformed manifest is caught (loudly) at deploy; dev must not crash on it.
|
|
7222
|
+
// Treating it as "nothing declared" makes personal-connector calls 403 here,
|
|
7223
|
+
// which is the safe direction.
|
|
7224
|
+
return [];
|
|
7225
|
+
}
|
|
7226
|
+
}
|
|
7227
|
+
// The toolkit slugs an app declared (any tool scope). Mirrors the server's
|
|
7228
|
+
// `manifest.personal_connector_toolkits`.
|
|
7229
|
+
function pcDeclaredToolkits(entries) {
|
|
7230
|
+
return new Set(entries.map((e) => e.split(":", 1)[0]));
|
|
7231
|
+
}
|
|
7232
|
+
// May this app call `tool` of `toolkit`? Mirrors the server's
|
|
7233
|
+
// `manifest.personal_connector_allowed`: a bare/`*` entry covers the whole
|
|
7234
|
+
// toolkit, else the tool must match exactly.
|
|
7235
|
+
function pcAllows(entries, toolkit, tool) {
|
|
7236
|
+
for (const entry of entries) {
|
|
7237
|
+
const idx = entry.indexOf(":");
|
|
7238
|
+
const entryToolkit = idx === -1 ? entry : entry.slice(0, idx);
|
|
7239
|
+
if (entryToolkit !== toolkit)
|
|
7240
|
+
continue;
|
|
7241
|
+
const entryTool = idx === -1 ? "" : entry.slice(idx + 1);
|
|
7242
|
+
if (entryTool === "" || entryTool === "*" || entryTool === tool)
|
|
7243
|
+
return true;
|
|
7244
|
+
}
|
|
7245
|
+
return false;
|
|
7246
|
+
}
|
|
7247
|
+
// Forward one request to the USER plane and return its parsed body. Kept separate
|
|
7248
|
+
// from forwardCompute because these need post-filtering by the local manifest,
|
|
7249
|
+
// not a raw pass-through.
|
|
7250
|
+
async function pcForward(ctx, method, apiPath, body) {
|
|
7251
|
+
const creds = devCreds(ctx.config);
|
|
7252
|
+
if (!creds) {
|
|
7253
|
+
return {
|
|
7254
|
+
status: 503,
|
|
7255
|
+
body: {
|
|
7256
|
+
error: "not_logged_in",
|
|
7257
|
+
message: "Run `railcode login` to use personal connectors in dev.",
|
|
7258
|
+
},
|
|
7259
|
+
};
|
|
7260
|
+
}
|
|
7261
|
+
const headers = {
|
|
7262
|
+
Authorization: `Bearer ${creds.token}`,
|
|
7263
|
+
"X-Railcode-Source": "local-dev",
|
|
7264
|
+
};
|
|
7265
|
+
if (body !== undefined)
|
|
7266
|
+
headers["Content-Type"] = "application/json";
|
|
7267
|
+
let upstream;
|
|
7268
|
+
try {
|
|
7269
|
+
upstream = await fetch(`${creds.apiUrl}/api${apiPath}`, {
|
|
7270
|
+
method,
|
|
7271
|
+
headers,
|
|
7272
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
7273
|
+
redirect: "manual",
|
|
7274
|
+
});
|
|
7275
|
+
}
|
|
7276
|
+
catch {
|
|
7277
|
+
return { status: 503, body: { error: "unreachable", message: `Could not reach ${creds.apiUrl}.` } };
|
|
7278
|
+
}
|
|
7279
|
+
const text = await upstream.text();
|
|
7280
|
+
let parsed = null;
|
|
7281
|
+
try {
|
|
7282
|
+
parsed = text ? JSON.parse(text) : null;
|
|
7283
|
+
}
|
|
7284
|
+
catch {
|
|
7285
|
+
parsed = text;
|
|
7286
|
+
}
|
|
7287
|
+
return { status: upstream.status, body: parsed };
|
|
7288
|
+
}
|
|
7289
|
+
// Which declared toolkit owns `tool` — asked of the user-plane catalog, scanning
|
|
7290
|
+
// only the app's declared toolkits (as the server's app-plane resolver does).
|
|
7291
|
+
async function pcResolveToolkit(ctx, entries, tool) {
|
|
7292
|
+
for (const toolkit of pcDeclaredToolkits(entries)) {
|
|
7293
|
+
const { status, body } = await pcForward(ctx, "GET", `/personal-connections/toolkits/${encodeURIComponent(toolkit)}/tools`);
|
|
7294
|
+
if (status === 200 && Array.isArray(body) && body.some((t) => t.slug === tool)) {
|
|
7295
|
+
return toolkit;
|
|
7296
|
+
}
|
|
7297
|
+
}
|
|
7298
|
+
return null;
|
|
7299
|
+
}
|
|
7300
|
+
const PC_NOT_IN_MANIFEST = (label) => ({
|
|
7301
|
+
detail: `personal connector "${label}" was not in the manifest`,
|
|
7302
|
+
});
|
|
7303
|
+
async function handlePersonalConnections(ctx, req, res, path, url) {
|
|
7304
|
+
const entries = ctx.personalConnectors;
|
|
7305
|
+
const declared = pcDeclaredToolkits(entries);
|
|
7306
|
+
const rest = path.slice("/personal-connections".length); // "", "/gmail/tools", "/call", ...
|
|
7307
|
+
// list() — forward, then filter to the toolkits this app declared (the app
|
|
7308
|
+
// plane never shows an app the user's OTHER connected accounts).
|
|
7309
|
+
if (rest === "" && req.method === "GET") {
|
|
7310
|
+
const { status, body } = await pcForward(ctx, "GET", "/personal-connections");
|
|
7311
|
+
const filtered = Array.isArray(body)
|
|
7312
|
+
? body.filter((c) => declared.has(c.toolkit))
|
|
7313
|
+
: body;
|
|
7314
|
+
return sendJson(res, status === 200 ? 200 : status, status === 200 ? filtered : body);
|
|
7315
|
+
}
|
|
7316
|
+
// tools(toolkit) — require the toolkit is declared, then filter to the declared
|
|
7317
|
+
// tool subset. Both mirror the app plane.
|
|
7318
|
+
const toolsMatch = rest.match(/^\/([^/]+)\/tools$/);
|
|
7319
|
+
if (toolsMatch && req.method === "GET") {
|
|
7320
|
+
const toolkit = decodeURIComponent(toolsMatch[1]);
|
|
7321
|
+
if (!declared.has(toolkit))
|
|
7322
|
+
return sendJson(res, 403, PC_NOT_IN_MANIFEST(toolkit));
|
|
7323
|
+
const { status, body } = await pcForward(ctx, "GET", `/personal-connections/toolkits/${encodeURIComponent(toolkit)}/tools`);
|
|
7324
|
+
const filtered = status === 200 && Array.isArray(body)
|
|
7325
|
+
? body.filter((t) => pcAllows(entries, toolkit, t.slug))
|
|
7326
|
+
: body;
|
|
7327
|
+
return sendJson(res, status, status === 200 ? filtered : body);
|
|
7328
|
+
}
|
|
7329
|
+
// connect(toolkit) — require declared (an app shouldn't walk you through
|
|
7330
|
+
// connecting an account it can never use), then forward.
|
|
7331
|
+
const connectMatch = rest.match(/^\/([^/]+)\/connect$/);
|
|
7332
|
+
if (connectMatch && req.method === "POST") {
|
|
7333
|
+
const toolkit = decodeURIComponent(connectMatch[1]);
|
|
7334
|
+
if (!declared.has(toolkit))
|
|
7335
|
+
return sendJson(res, 403, PC_NOT_IN_MANIFEST(toolkit));
|
|
7336
|
+
const { status, body } = await pcForward(ctx, "POST", `/personal-connections/${encodeURIComponent(toolkit)}/connect`);
|
|
7337
|
+
return sendJson(res, status, body);
|
|
7338
|
+
}
|
|
7339
|
+
// call(tool, args) — the load-bearing gate: resolve the tool's toolkit, require
|
|
7340
|
+
// both that toolkit declared AND this specific tool permitted, else 403 (never
|
|
7341
|
+
// forwarded). No declaration at all ⇒ nothing resolves ⇒ refused, matching the
|
|
7342
|
+
// prod chokepoint's "no manifest = refuse".
|
|
7343
|
+
if (rest === "/call" && req.method === "POST") {
|
|
7344
|
+
const raw = await readBody(req);
|
|
7345
|
+
let payload;
|
|
7346
|
+
try {
|
|
7347
|
+
payload = raw ? JSON.parse(raw.toString("utf8")) : {};
|
|
7348
|
+
}
|
|
7349
|
+
catch {
|
|
7350
|
+
return sendJson(res, 400, { detail: "body must be JSON" });
|
|
7351
|
+
}
|
|
7352
|
+
const tool = payload.tool;
|
|
7353
|
+
if (!tool)
|
|
7354
|
+
return sendJson(res, 400, { detail: '"tool" is required' });
|
|
7355
|
+
if (entries.length === 0)
|
|
7356
|
+
return sendJson(res, 403, PC_NOT_IN_MANIFEST(tool));
|
|
7357
|
+
const toolkit = await pcResolveToolkit(ctx, entries, tool);
|
|
7358
|
+
if (!toolkit || !pcAllows(entries, toolkit, tool)) {
|
|
7359
|
+
return sendJson(res, 403, PC_NOT_IN_MANIFEST(tool));
|
|
7360
|
+
}
|
|
7361
|
+
const { status, body } = await pcForward(ctx, "POST", "/personal-connections/call", {
|
|
7362
|
+
tool,
|
|
7363
|
+
arguments: payload.arguments ?? {},
|
|
7364
|
+
});
|
|
7365
|
+
return sendJson(res, status, body);
|
|
7366
|
+
}
|
|
7367
|
+
sendJson(res, 404, { detail: "not found" });
|
|
7368
|
+
}
|
|
7024
7369
|
async function forwardCompute(ctx, req, res, path, url) {
|
|
7025
7370
|
// Never reply 401 to the browser — the SDK reloads the page on 401 (loop).
|
|
7026
7371
|
// Load-time list calls degrade to [] so dataConnectors()/savedQueries()/
|
package/dist/manifest.js
CHANGED
|
@@ -41,9 +41,14 @@ const ALLOWED_KEYS = [
|
|
|
41
41
|
"email",
|
|
42
42
|
"adhoc_sql",
|
|
43
43
|
"agents",
|
|
44
|
+
"personal_connectors",
|
|
44
45
|
];
|
|
45
46
|
const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
46
47
|
const SAVED_QUERY_NAME = /^[a-z0-9_]{1,80}$/;
|
|
48
|
+
// Mirrors the server's _PERSONAL_CONNECTOR_RE exactly. The server is
|
|
49
|
+
// authoritative; this exists so `railcode deploy` can reject a bad manifest
|
|
50
|
+
// locally instead of after a round trip.
|
|
51
|
+
const PERSONAL_CONNECTOR = /^[a-z0-9][a-z0-9_]{0,63}(?::(?:\*|[A-Z0-9_]{1,120}))?$/;
|
|
47
52
|
// PyYAML's YAML 1.1 plain-scalar resolver: a bare (unquoted) scalar matching one
|
|
48
53
|
// of these resolves to a non-string (bool/null/int/float/timestamp), and the
|
|
49
54
|
// server rejects a non-string wherever the manifest wants a name. The quirks are
|
|
@@ -396,6 +401,7 @@ export function parseManifestYaml(source) {
|
|
|
396
401
|
email: false,
|
|
397
402
|
adhoc_sql: [],
|
|
398
403
|
agents: [],
|
|
404
|
+
personal_connectors: [],
|
|
399
405
|
};
|
|
400
406
|
for (const [key, entry] of entries) {
|
|
401
407
|
if (key === "run_as") {
|
|
@@ -415,12 +421,14 @@ export function parseManifestYaml(source) {
|
|
|
415
421
|
doc.connectors = parseConnectors(entry);
|
|
416
422
|
}
|
|
417
423
|
else {
|
|
418
|
-
// saved_queries / adhoc_sql / agents: a list of names.
|
|
424
|
+
// saved_queries / adhoc_sql / agents / personal_connectors: a list of names.
|
|
419
425
|
const items = parseStringList(entry, key);
|
|
420
426
|
if (key === "saved_queries")
|
|
421
427
|
doc.saved_queries = items;
|
|
422
428
|
else if (key === "agents")
|
|
423
429
|
doc.agents = items;
|
|
430
|
+
else if (key === "personal_connectors")
|
|
431
|
+
doc.personal_connectors = items;
|
|
424
432
|
else
|
|
425
433
|
doc.adhoc_sql = items;
|
|
426
434
|
}
|
|
@@ -643,6 +651,12 @@ function validateDoc(doc, lines) {
|
|
|
643
651
|
doc.saved_queries = [...new Set(doc.saved_queries)].sort();
|
|
644
652
|
doc.adhoc_sql = [...new Set(doc.adhoc_sql)].sort();
|
|
645
653
|
doc.agents = [...new Set(doc.agents)].sort();
|
|
654
|
+
for (const entry of doc.personal_connectors) {
|
|
655
|
+
if (!PERSONAL_CONNECTOR.test(entry)) {
|
|
656
|
+
throw new ManifestParseError(`"${entry}" is not a valid personal_connectors entry — use "gmail", "gmail:*", or "gmail:GMAIL_SEND_EMAIL"`, lineOf(entry));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
doc.personal_connectors = [...new Set(doc.personal_connectors)].sort();
|
|
646
660
|
for (const name of Object.keys(doc.connectors)) {
|
|
647
661
|
doc.connectors[name] = [...new Set(doc.connectors[name])].sort();
|
|
648
662
|
}
|
package/dist/observability.js
CHANGED
|
@@ -177,7 +177,7 @@ export function logTable(stream, rows) {
|
|
|
177
177
|
return formatTable(stream.columns, rows.map((r) => stream.map(r)));
|
|
178
178
|
}
|
|
179
179
|
// ── Analytics (per-app pageviews) ───────────────────────────────────────────
|
|
180
|
-
export const ANALYTICS_RANGES = ["7d", "30d", "90d"];
|
|
180
|
+
export const ANALYTICS_RANGES = ["1d", "7d", "30d", "90d"];
|
|
181
181
|
export function parseRange(value) {
|
|
182
182
|
if (value === undefined)
|
|
183
183
|
return undefined;
|
package/package.json
CHANGED
package/static/sdk.js
CHANGED
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
localStorage.setItem("__sdk_open", v ? "1" : "0");
|
|
83
83
|
root.getElementById("panel").style.display = v ? "flex" : "none";
|
|
84
84
|
applyWidth();
|
|
85
|
+
window.dispatchEvent(new CustomEvent("railcode:inspector", { detail: { open: v } }));
|
|
85
86
|
}
|
|
86
87
|
var escMap = { "&": "&", "<": "<", ">": ">" };
|
|
87
88
|
var escHtml = (s) => String(s == null ? "" : s).replace(/[&<>]/g, (c) => escMap[c]);
|
|
@@ -106,7 +107,9 @@
|
|
|
106
107
|
if (entries.length > 100) entries.shift();
|
|
107
108
|
render();
|
|
108
109
|
},
|
|
109
|
-
update: render
|
|
110
|
+
update: render,
|
|
111
|
+
toggle: () => setOpen(!isOpen),
|
|
112
|
+
isOpen: () => isOpen
|
|
110
113
|
};
|
|
111
114
|
|
|
112
115
|
// ../sdk/src/config.ts
|
|
@@ -219,14 +222,84 @@
|
|
|
219
222
|
}();
|
|
220
223
|
}
|
|
221
224
|
|
|
225
|
+
// ../sdk/src/identity.ts
|
|
226
|
+
var me = () => track("me", "me()", () => call("GET", "/me"));
|
|
227
|
+
var appUsers = () => track("app-users", "appUsers()", () => call("GET", "/app-users"));
|
|
228
|
+
var roles = () => track("roles", "roles()", () => call("GET", "/roles"));
|
|
229
|
+
|
|
230
|
+
// ../sdk/src/debug-fab.ts
|
|
231
|
+
function mount2() {
|
|
232
|
+
const host2 = document.createElement("div");
|
|
233
|
+
host2.id = "__sdk_debug_fab";
|
|
234
|
+
const root2 = host2.attachShadow({ mode: "open" });
|
|
235
|
+
root2.innerHTML = `
|
|
236
|
+
<style>
|
|
237
|
+
:host { all: initial; }
|
|
238
|
+
button {
|
|
239
|
+
box-sizing: border-box; position: fixed; right: 16px; bottom: 16px;
|
|
240
|
+
width: 36px; height: 36px; border-radius: 50%; display: flex;
|
|
241
|
+
align-items: center; justify-content: center; cursor: pointer; padding: 0;
|
|
242
|
+
background: #14171c; color: #d6d9de; border: 1px solid #2c333d;
|
|
243
|
+
box-shadow: 0 2px 10px rgba(0,0,0,.35); z-index: 2147483001;
|
|
244
|
+
}
|
|
245
|
+
button:hover { background: #1c2128; color: #fff; }
|
|
246
|
+
svg { width: 16px; height: 16px; }
|
|
247
|
+
</style>
|
|
248
|
+
<button title="SDK activity (Ctrl+\`)" aria-label="Toggle SDK activity">
|
|
249
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
|
250
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
251
|
+
<path d="M12 19h8"></path>
|
|
252
|
+
<path d="m4 17 6-6-6-6"></path>
|
|
253
|
+
</svg>
|
|
254
|
+
</button>`;
|
|
255
|
+
root2.querySelector("button").onclick = () => Inspector.toggle();
|
|
256
|
+
(document.body || document.documentElement).appendChild(host2);
|
|
257
|
+
const sync = () => {
|
|
258
|
+
host2.style.display = Inspector.isOpen() ? "none" : "";
|
|
259
|
+
};
|
|
260
|
+
sync();
|
|
261
|
+
window.addEventListener("railcode:inspector", sync);
|
|
262
|
+
}
|
|
263
|
+
me().then((who) => {
|
|
264
|
+
if (who.user.is_admin || who.user.is_app_owner) mount2();
|
|
265
|
+
}).catch(() => {
|
|
266
|
+
});
|
|
267
|
+
|
|
222
268
|
// ../sdk/src/agents.ts
|
|
223
|
-
|
|
269
|
+
var TERMINAL = ["success", "failed", "cancelled", "limit_exceeded"];
|
|
270
|
+
var POLL_MIN_MS = 400;
|
|
271
|
+
var POLL_MAX_MS = 2e3;
|
|
272
|
+
var POLL_BACKOFF = 1.4;
|
|
273
|
+
function isFinished(run) {
|
|
274
|
+
return TERMINAL.includes(run.status);
|
|
275
|
+
}
|
|
276
|
+
async function doStart(name, input) {
|
|
224
277
|
return call("POST", `/agents/${encodeURIComponent(name)}`, { body: { input } });
|
|
225
278
|
}
|
|
279
|
+
async function doGet(requestId) {
|
|
280
|
+
return call("GET", `/agents/runs/${encodeURIComponent(requestId)}`);
|
|
281
|
+
}
|
|
282
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
283
|
+
async function doInvoke(name, input) {
|
|
284
|
+
let run = await doStart(name, input);
|
|
285
|
+
let wait = POLL_MIN_MS;
|
|
286
|
+
while (!isFinished(run)) {
|
|
287
|
+
await sleep(wait);
|
|
288
|
+
wait = Math.min(wait * POLL_BACKOFF, POLL_MAX_MS);
|
|
289
|
+
run = await doGet(run.request_id);
|
|
290
|
+
}
|
|
291
|
+
return run;
|
|
292
|
+
}
|
|
293
|
+
function start(name, input) {
|
|
294
|
+
return track("agents", `agents.start(${q(name)})`, () => doStart(name, input));
|
|
295
|
+
}
|
|
296
|
+
function get(requestId) {
|
|
297
|
+
return doGet(requestId);
|
|
298
|
+
}
|
|
226
299
|
function invoke(name, input) {
|
|
227
300
|
return track("agents", `agents.invoke(${q(name)})`, () => doInvoke(name, input));
|
|
228
301
|
}
|
|
229
|
-
var agents = { invoke };
|
|
302
|
+
var agents = { invoke, start, get };
|
|
230
303
|
|
|
231
304
|
// ../sdk/src/databases.ts
|
|
232
305
|
var QUERY_LABEL_MAX = 80;
|
|
@@ -567,11 +640,6 @@
|
|
|
567
640
|
delete: shared2.delete
|
|
568
641
|
};
|
|
569
642
|
|
|
570
|
-
// ../sdk/src/identity.ts
|
|
571
|
-
var me = () => track("me", "me()", () => call("GET", "/me"));
|
|
572
|
-
var appUsers = () => track("app-users", "appUsers()", () => call("GET", "/app-users"));
|
|
573
|
-
var roles = () => track("roles", "roles()", () => call("GET", "/roles"));
|
|
574
|
-
|
|
575
643
|
// ../sdk/src/llm.ts
|
|
576
644
|
function label2(method, input, opts = {}) {
|
|
577
645
|
const preview = typeof input === "string" ? shorten(input.replace(/\s+/g, " ").trim(), 70) : `${input.length} message${input.length === 1 ? "" : "s"}`;
|
|
@@ -643,6 +711,42 @@
|
|
|
643
711
|
var llm = { generate, stream };
|
|
644
712
|
var llmProviders = () => track("llm", "llmProviders()", () => call("GET", "/llm/providers"));
|
|
645
713
|
|
|
714
|
+
// ../sdk/src/personal-connections.ts
|
|
715
|
+
function list2() {
|
|
716
|
+
return track(
|
|
717
|
+
"personalConnections",
|
|
718
|
+
"personalConnections.list()",
|
|
719
|
+
() => call("GET", "/personal-connections")
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
function connect(toolkit) {
|
|
723
|
+
return track(
|
|
724
|
+
"personalConnections",
|
|
725
|
+
`personalConnections.connect(${q(toolkit)})`,
|
|
726
|
+
() => call(
|
|
727
|
+
"POST",
|
|
728
|
+
`/personal-connections/${encodeURIComponent(toolkit)}/connect`
|
|
729
|
+
)
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
function tools(toolkit) {
|
|
733
|
+
return track(
|
|
734
|
+
"personalConnections",
|
|
735
|
+
`personalConnections.tools(${q(toolkit)})`,
|
|
736
|
+
() => call("GET", `/personal-connections/${encodeURIComponent(toolkit)}/tools`)
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
function callTool(tool, args = {}) {
|
|
740
|
+
return track(
|
|
741
|
+
"personalConnections",
|
|
742
|
+
`personalConnections.call(${q(tool)})`,
|
|
743
|
+
() => call("POST", "/personal-connections/call", {
|
|
744
|
+
body: { tool, arguments: args }
|
|
745
|
+
})
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
var personalConnections = { list: list2, connect, tools, call: callTool };
|
|
749
|
+
|
|
646
750
|
// ../sdk/src/queries.ts
|
|
647
751
|
var PARAMS_LABEL_MAX2 = 40;
|
|
648
752
|
var query = (name, params) => {
|
|
@@ -701,6 +805,7 @@
|
|
|
701
805
|
target.email = email;
|
|
702
806
|
target.llm = llm;
|
|
703
807
|
target.llmProviders = llmProviders;
|
|
808
|
+
target.personalConnections = personalConnections;
|
|
704
809
|
target.data = data;
|
|
705
810
|
target.postgres = postgres;
|
|
706
811
|
target.bigquery = bigquery;
|