railcode 0.1.24 → 0.1.26
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 +16 -1
- package/dist/index.js +360 -7
- package/dist/manifest.js +15 -1
- package/package.json +1 -1
- package/static/sdk.js +38 -1
package/README.md
CHANGED
|
@@ -89,7 +89,13 @@ your saved org and are gated server-side by the same capabilities as the console
|
|
|
89
89
|
manifest for editing.
|
|
90
90
|
- `railcode agent create --file agent.json` creates an agent; `update <agent>
|
|
91
91
|
--file agent.json` replaces an existing agent manifest; `delete <agent> --yes`
|
|
92
|
-
archives it.
|
|
92
|
+
archives it. `--visibility <org|personal>` on `create`/`update`/`test` sets or
|
|
93
|
+
changes who the agent belongs to (default `org` on create; omitted on `update`
|
|
94
|
+
leaves it alone). A `personal` agent is invokable and manageable by its
|
|
95
|
+
creator alone — no grant makes it shared — and `personal -> org` is blocked
|
|
96
|
+
once created. Requires `agent:create` (personal) or `agent:create_org` (org).
|
|
97
|
+
`agent show`/`list` print `visibility`, and `show` prints `Owner:` for a
|
|
98
|
+
personal agent.
|
|
93
99
|
- `railcode agent test --file agent.json --input '{"k":"v"}'` runs a draft
|
|
94
100
|
manifest without saving; `railcode agent run <agent> --input '{"k":"v"}'`
|
|
95
101
|
invokes a saved agent and prints the result. Use `--trace` for the step trace
|
|
@@ -97,6 +103,15 @@ your saved org and are gated server-side by the same capabilities as the console
|
|
|
97
103
|
- `railcode agent schedule set <agent> --cron "0 9 * * *" --timezone UTC`
|
|
98
104
|
creates or updates the agent's one schedule. `schedule show`, `pause`,
|
|
99
105
|
`resume`, `delete --yes`, and `run-now` operate on that single schedule.
|
|
106
|
+
- **`personal-connectors`** (aliases `personal-connector`, `pc`) manages **your
|
|
107
|
+
own** connected third-party accounts (Gmail, Slack, …), brokered by Composio —
|
|
108
|
+
distinct from org-admin **service connectors** and from an agent's
|
|
109
|
+
`tools.personal_connectors`. `list` shows toolkits this deployment brokers and
|
|
110
|
+
your connection status; `tools <toolkit>` prints that toolkit's callable tools
|
|
111
|
+
and input schemas; `connect <toolkit>` prints an OAuth URL to open in a
|
|
112
|
+
browser; `call <toolkit> <tool> [--args '<json>']` runs one tool on your own
|
|
113
|
+
connected account. Hits the non-org-scoped `/api/personal-connections/*` plane
|
|
114
|
+
— a personal connection belongs to the human, not the org.
|
|
100
115
|
|
|
101
116
|
## Configuration
|
|
102
117
|
|
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
|
|
@@ -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);
|
|
@@ -3802,6 +3809,12 @@ Agent options:
|
|
|
3802
3809
|
--input-file <path> Read invoke/test input JSON from a file
|
|
3803
3810
|
--trace Include the run step trace in human output
|
|
3804
3811
|
--yes Confirm delete without a prompt
|
|
3812
|
+
--visibility <org|personal> create/update/test only (default: org). A personal
|
|
3813
|
+
agent is owned/invoked by its creator alone — no
|
|
3814
|
+
grant makes it shared, and "personal -> org" is
|
|
3815
|
+
blocked once created. Requires the matching
|
|
3816
|
+
capability: agent:create for personal,
|
|
3817
|
+
agent:create_org for org.
|
|
3805
3818
|
|
|
3806
3819
|
Schedule options:
|
|
3807
3820
|
--name <name> Schedule name (default: default on create)
|
|
@@ -3874,9 +3887,10 @@ async function commandAgentList(args) {
|
|
|
3874
3887
|
console.log("No managed agents in this organization.");
|
|
3875
3888
|
return;
|
|
3876
3889
|
}
|
|
3877
|
-
console.log(formatTable(["name", "model", "uuid", "updated"], agents.map((agent) => [
|
|
3890
|
+
console.log(formatTable(["name", "model", "visibility", "uuid", "updated"], agents.map((agent) => [
|
|
3878
3891
|
agent.name,
|
|
3879
3892
|
typeof agent.manifest.model === "string" ? agent.manifest.model : "-",
|
|
3893
|
+
agent.visibility,
|
|
3880
3894
|
agent.uuid,
|
|
3881
3895
|
shortDate(agent.updated_at),
|
|
3882
3896
|
])));
|
|
@@ -3911,13 +3925,14 @@ async function commandAgentPull(args) {
|
|
|
3911
3925
|
process.stdout.write(manifest);
|
|
3912
3926
|
}
|
|
3913
3927
|
async function commandAgentCreate(args) {
|
|
3914
|
-
assertKnownAgentOptions(args, ["file", "json", "apiUrl"]);
|
|
3928
|
+
assertKnownAgentOptions(args, ["file", "json", "visibility", "apiUrl"]);
|
|
3915
3929
|
const manifest = readAgentManifest(args);
|
|
3930
|
+
const visibility = agentVisibilityOption(args);
|
|
3916
3931
|
const config = requireLogin(args);
|
|
3917
3932
|
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents`, {
|
|
3918
3933
|
method: "POST",
|
|
3919
3934
|
headers: { "Content-Type": "application/json" },
|
|
3920
|
-
body: JSON.stringify({ manifest }),
|
|
3935
|
+
body: JSON.stringify({ manifest, ...(visibility !== undefined ? { visibility } : {}) }),
|
|
3921
3936
|
});
|
|
3922
3937
|
if (resp.status === 401)
|
|
3923
3938
|
await reLoginOrThrow();
|
|
@@ -3925,14 +3940,15 @@ async function commandAgentCreate(args) {
|
|
|
3925
3940
|
printAgentMutation(result, args.options.json === true, "created");
|
|
3926
3941
|
}
|
|
3927
3942
|
async function commandAgentUpdate(args) {
|
|
3928
|
-
assertKnownAgentOptions(args, ["file", "json", "apiUrl"]);
|
|
3943
|
+
assertKnownAgentOptions(args, ["file", "json", "visibility", "apiUrl"]);
|
|
3929
3944
|
const ref = agentArg(args, "update");
|
|
3930
3945
|
const manifest = readAgentManifest(args);
|
|
3946
|
+
const visibility = agentVisibilityOption(args);
|
|
3931
3947
|
const config = requireLogin(args);
|
|
3932
3948
|
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/${encodeURIComponent(ref)}`, {
|
|
3933
3949
|
method: "PUT",
|
|
3934
3950
|
headers: { "Content-Type": "application/json" },
|
|
3935
|
-
body: JSON.stringify({ manifest }),
|
|
3951
|
+
body: JSON.stringify({ manifest, ...(visibility !== undefined ? { visibility } : {}) }),
|
|
3936
3952
|
});
|
|
3937
3953
|
if (resp.status === 401)
|
|
3938
3954
|
await reLoginOrThrow();
|
|
@@ -3961,14 +3977,15 @@ async function commandAgentDelete(args) {
|
|
|
3961
3977
|
console.log(`Agent "${ref}" deleted.`);
|
|
3962
3978
|
}
|
|
3963
3979
|
async function commandAgentTest(args) {
|
|
3964
|
-
assertKnownAgentOptions(args, ["file", "input", "inputFile", "json", "trace", "apiUrl"]);
|
|
3980
|
+
assertKnownAgentOptions(args, ["file", "input", "inputFile", "json", "trace", "visibility", "apiUrl"]);
|
|
3965
3981
|
const manifest = readAgentManifest(args);
|
|
3966
3982
|
const input = readJsonInput(args);
|
|
3983
|
+
const visibility = agentVisibilityOption(args);
|
|
3967
3984
|
const config = requireLogin(args);
|
|
3968
3985
|
const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/test`, {
|
|
3969
3986
|
method: "POST",
|
|
3970
3987
|
headers: { "Content-Type": "application/json" },
|
|
3971
|
-
body: JSON.stringify({ manifest, input }),
|
|
3988
|
+
body: JSON.stringify({ manifest, input, ...(visibility !== undefined ? { visibility } : {}) }),
|
|
3972
3989
|
});
|
|
3973
3990
|
if (resp.status === 401)
|
|
3974
3991
|
await reLoginOrThrow();
|
|
@@ -4171,6 +4188,17 @@ function readAgentManifest(args) {
|
|
|
4171
4188
|
throw new CliError("Pass --file <agent-manifest.json>.", 2);
|
|
4172
4189
|
return readJsonObjectFile(path, "Agent manifest");
|
|
4173
4190
|
}
|
|
4191
|
+
// Omitted means "use the server default" on create/test (org) and "leave alone"
|
|
4192
|
+
// on update — never send the key unless the caller passed it explicitly.
|
|
4193
|
+
function agentVisibilityOption(args) {
|
|
4194
|
+
const value = optionString(args, "visibility");
|
|
4195
|
+
if (value === undefined)
|
|
4196
|
+
return undefined;
|
|
4197
|
+
if (value !== "org" && value !== "personal") {
|
|
4198
|
+
throw new CliError(`Invalid --visibility: ${value} (expected "org" or "personal").`, 2);
|
|
4199
|
+
}
|
|
4200
|
+
return value;
|
|
4201
|
+
}
|
|
4174
4202
|
function readJsonObjectFile(path, label) {
|
|
4175
4203
|
const resolved = resolve(process.cwd(), path);
|
|
4176
4204
|
if (!existsSync(resolved))
|
|
@@ -4257,6 +4285,7 @@ function printAgentMutation(result, json, verb) {
|
|
|
4257
4285
|
}
|
|
4258
4286
|
console.log(`Agent "${result.agent.name}" ${verb}.`);
|
|
4259
4287
|
console.log(`UUID: ${result.agent.uuid}`);
|
|
4288
|
+
console.log(`Visibility: ${result.agent.visibility}`);
|
|
4260
4289
|
if (result.warnings.length > 0) {
|
|
4261
4290
|
console.log("");
|
|
4262
4291
|
console.log("Warnings:");
|
|
@@ -4269,6 +4298,9 @@ function printAgent(agent) {
|
|
|
4269
4298
|
console.log(`UUID: ${agent.uuid}`);
|
|
4270
4299
|
console.log(`Description: ${agent.description || "-"}`);
|
|
4271
4300
|
console.log(`Model: ${typeof agent.manifest.model === "string" ? agent.manifest.model : "-"}`);
|
|
4301
|
+
console.log(`Visibility: ${agent.visibility}`);
|
|
4302
|
+
if (agent.visibility === "personal")
|
|
4303
|
+
console.log(`Owner: ${agent.owner_uuid ?? "-"}`);
|
|
4272
4304
|
console.log(`Updated: ${agent.updated_at}`);
|
|
4273
4305
|
console.log(`Content hash: ${agent.content_hash}`);
|
|
4274
4306
|
}
|
|
@@ -5103,6 +5135,167 @@ function assertKnownLlmOptions(args, allowed) {
|
|
|
5103
5135
|
}
|
|
5104
5136
|
}
|
|
5105
5137
|
// ---------------------------------------------------------------------------
|
|
5138
|
+
// Personal connectors: your OWN connected accounts (Gmail, Slack, ...).
|
|
5139
|
+
//
|
|
5140
|
+
// Everything here hits the NON-org-scoped `/api/personal-connections/*` plane —
|
|
5141
|
+
// a personal connection belongs to the human, not the org. It's the same
|
|
5142
|
+
// resource the console's Connectors tab drives. `call` executes as YOU against
|
|
5143
|
+
// YOUR own connection (an identity op, not an app), so it's how you smoke-test a
|
|
5144
|
+
// tool from the terminal before wiring it into an app or agent.
|
|
5145
|
+
// ---------------------------------------------------------------------------
|
|
5146
|
+
const PC_HELP = `railcode personal-connectors — your own connected accounts (Gmail, Slack, ...)
|
|
5147
|
+
|
|
5148
|
+
Usage:
|
|
5149
|
+
railcode personal-connectors list Toolkits this deployment brokers + your status
|
|
5150
|
+
railcode personal-connectors tools <toolkit> The callable tools of a toolkit (and their inputs)
|
|
5151
|
+
railcode personal-connectors connect <toolkit> Print the OAuth URL to connect it (open in a browser)
|
|
5152
|
+
railcode personal-connectors call <toolkit> <tool> [--args '<json>'] Run one tool on your own account
|
|
5153
|
+
|
|
5154
|
+
Aliases: personal-connector, pc
|
|
5155
|
+
|
|
5156
|
+
Discovery is what you build against: \`tools <toolkit> --json\` shows each tool's
|
|
5157
|
+
input schema, so you know what to put in an app's \`personal_connectors:\` and what
|
|
5158
|
+
\`personalConnections.call('<toolkit>', '<tool>', {...})\` expects.
|
|
5159
|
+
|
|
5160
|
+
Options:
|
|
5161
|
+
--json Print raw JSON instead of a table
|
|
5162
|
+
--args '<json>' Arguments object for \`call\` (default {})
|
|
5163
|
+
`;
|
|
5164
|
+
async function commandPersonalConnectors(args) {
|
|
5165
|
+
const sub = args.rest[0] ?? "";
|
|
5166
|
+
switch (sub) {
|
|
5167
|
+
case "list":
|
|
5168
|
+
await commandPcList(args);
|
|
5169
|
+
return;
|
|
5170
|
+
case "tools":
|
|
5171
|
+
await commandPcTools(args);
|
|
5172
|
+
return;
|
|
5173
|
+
case "connect":
|
|
5174
|
+
await commandPcConnect(args);
|
|
5175
|
+
return;
|
|
5176
|
+
case "call":
|
|
5177
|
+
await commandPcCall(args);
|
|
5178
|
+
return;
|
|
5179
|
+
case "":
|
|
5180
|
+
case "help":
|
|
5181
|
+
console.log(PC_HELP);
|
|
5182
|
+
return;
|
|
5183
|
+
default:
|
|
5184
|
+
throw new CliError(`Unknown personal-connectors command: ${sub}\n\n${PC_HELP}`, 2);
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
// A 503 here means the DEPLOYMENT has personal connectors turned off — a state to
|
|
5188
|
+
// report plainly, not a failure. Anything else is a real error.
|
|
5189
|
+
function pcUnavailable(status) {
|
|
5190
|
+
return status === 503;
|
|
5191
|
+
}
|
|
5192
|
+
async function commandPcList(args) {
|
|
5193
|
+
rejectUnknownOptions(args, ["json", "apiUrl"], "personal-connectors list", PC_HELP);
|
|
5194
|
+
const config = requireLogin(args);
|
|
5195
|
+
const resp = await authedFetch(config, `/api/personal-connections/toolkits`);
|
|
5196
|
+
if (resp.status === 401)
|
|
5197
|
+
await reLoginOrThrow();
|
|
5198
|
+
if (pcUnavailable(resp.status)) {
|
|
5199
|
+
console.log("Personal connectors are not enabled on this deployment.");
|
|
5200
|
+
return;
|
|
5201
|
+
}
|
|
5202
|
+
const toolkits = (await readJsonOrThrow(resp, "List toolkits"));
|
|
5203
|
+
if (args.options.json) {
|
|
5204
|
+
console.log(JSON.stringify(toolkits, null, 2));
|
|
5205
|
+
return;
|
|
5206
|
+
}
|
|
5207
|
+
if (toolkits.length === 0) {
|
|
5208
|
+
console.log("No connectors are available yet.");
|
|
5209
|
+
return;
|
|
5210
|
+
}
|
|
5211
|
+
console.log(formatTable(["TOOLKIT", "NAME", "STATUS"], toolkits.map((t) => [t.slug, t.name, t.connected ? "connected" : t.status ?? "not connected"])));
|
|
5212
|
+
}
|
|
5213
|
+
async function commandPcTools(args) {
|
|
5214
|
+
rejectUnknownOptions(args, ["json", "apiUrl"], "personal-connectors tools", PC_HELP);
|
|
5215
|
+
const toolkit = args.rest[1];
|
|
5216
|
+
if (!toolkit)
|
|
5217
|
+
throw new CliError("Usage: railcode personal-connectors tools <toolkit>", 2);
|
|
5218
|
+
const config = requireLogin(args);
|
|
5219
|
+
const resp = await authedFetch(config, `/api/personal-connections/toolkits/${encodeURIComponent(toolkit)}/tools`);
|
|
5220
|
+
if (resp.status === 401)
|
|
5221
|
+
await reLoginOrThrow();
|
|
5222
|
+
if (pcUnavailable(resp.status)) {
|
|
5223
|
+
console.log("Personal connectors are not enabled on this deployment.");
|
|
5224
|
+
return;
|
|
5225
|
+
}
|
|
5226
|
+
const tools = (await readJsonOrThrow(resp, `List ${toolkit} tools`));
|
|
5227
|
+
if (args.options.json) {
|
|
5228
|
+
// --json carries the input_schema, which is what you actually build against.
|
|
5229
|
+
console.log(JSON.stringify(tools, null, 2));
|
|
5230
|
+
return;
|
|
5231
|
+
}
|
|
5232
|
+
if (tools.length === 0) {
|
|
5233
|
+
console.log(`No tools found for "${toolkit}".`);
|
|
5234
|
+
return;
|
|
5235
|
+
}
|
|
5236
|
+
console.log(formatTable(["TOOL", "DESCRIPTION"], tools.map((t) => [t.slug, t.description])));
|
|
5237
|
+
console.log(`\n${tools.length} tool(s). Add \`--json\` to see each tool's input schema.`);
|
|
5238
|
+
}
|
|
5239
|
+
async function commandPcConnect(args) {
|
|
5240
|
+
rejectUnknownOptions(args, ["json", "apiUrl"], "personal-connectors connect", PC_HELP);
|
|
5241
|
+
const toolkit = args.rest[1];
|
|
5242
|
+
if (!toolkit)
|
|
5243
|
+
throw new CliError("Usage: railcode personal-connectors connect <toolkit>", 2);
|
|
5244
|
+
const config = requireLogin(args);
|
|
5245
|
+
// Connecting is browser-bound OAuth; the CLI can't complete it. It prints the
|
|
5246
|
+
// URL and leaves opening it to you — the same handoff `railcode login` uses.
|
|
5247
|
+
const resp = await authedFetch(config, `/api/personal-connections/${encodeURIComponent(toolkit)}/connect`, { method: "POST" });
|
|
5248
|
+
if (resp.status === 401)
|
|
5249
|
+
await reLoginOrThrow();
|
|
5250
|
+
if (pcUnavailable(resp.status)) {
|
|
5251
|
+
console.log("Personal connectors are not enabled on this deployment.");
|
|
5252
|
+
return;
|
|
5253
|
+
}
|
|
5254
|
+
const body = (await readJsonOrThrow(resp, `Connect ${toolkit}`));
|
|
5255
|
+
if (args.options.json) {
|
|
5256
|
+
console.log(JSON.stringify(body, null, 2));
|
|
5257
|
+
return;
|
|
5258
|
+
}
|
|
5259
|
+
console.log(`Open this URL to connect ${toolkit}, then re-run your command:\n\n ${body.redirect_url}\n`);
|
|
5260
|
+
}
|
|
5261
|
+
async function commandPcCall(args) {
|
|
5262
|
+
rejectUnknownOptions(args, ["json", "args", "apiUrl"], "personal-connectors call", PC_HELP);
|
|
5263
|
+
// Scoped by connector: name the toolkit AND the tool.
|
|
5264
|
+
const toolkit = args.rest[1];
|
|
5265
|
+
const tool = args.rest[2];
|
|
5266
|
+
if (!toolkit || !tool) {
|
|
5267
|
+
throw new CliError("Usage: railcode personal-connectors call <toolkit> <tool> [--args '<json>']", 2);
|
|
5268
|
+
}
|
|
5269
|
+
let toolArgs = {};
|
|
5270
|
+
if (typeof args.options.args === "string") {
|
|
5271
|
+
try {
|
|
5272
|
+
toolArgs = JSON.parse(args.options.args);
|
|
5273
|
+
}
|
|
5274
|
+
catch {
|
|
5275
|
+
throw new CliError(`--args must be a JSON object (got: ${args.options.args})`, 2);
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
const config = requireLogin(args);
|
|
5279
|
+
const resp = await authedFetch(config, `/api/personal-connections/call`, {
|
|
5280
|
+
method: "POST",
|
|
5281
|
+
headers: { "Content-Type": "application/json" },
|
|
5282
|
+
body: JSON.stringify({ toolkit, tool, arguments: toolArgs }),
|
|
5283
|
+
});
|
|
5284
|
+
if (resp.status === 401)
|
|
5285
|
+
await reLoginOrThrow();
|
|
5286
|
+
if (pcUnavailable(resp.status)) {
|
|
5287
|
+
console.log("Personal connectors are not enabled on this deployment.");
|
|
5288
|
+
return;
|
|
5289
|
+
}
|
|
5290
|
+
// A 409 means "connect this toolkit first" — surface the server's hint verbatim
|
|
5291
|
+
// rather than a generic failure, since it names the exact command to run.
|
|
5292
|
+
if (resp.status === 409) {
|
|
5293
|
+
throw new CliError(await errorDetail(resp), 1);
|
|
5294
|
+
}
|
|
5295
|
+
const body = await readJsonOrThrow(resp, `Call ${toolkit}/${tool}`);
|
|
5296
|
+
console.log(JSON.stringify(body, null, 2));
|
|
5297
|
+
}
|
|
5298
|
+
// ---------------------------------------------------------------------------
|
|
5106
5299
|
// Shared admin helpers (members / roles / apps / analytics / logs / connections)
|
|
5107
5300
|
//
|
|
5108
5301
|
// All of these hit the org-scoped `/api/organizations/{org}/...` management APIs
|
|
@@ -6182,6 +6375,7 @@ async function commandDev(args) {
|
|
|
6182
6375
|
fileStores: new Map(),
|
|
6183
6376
|
port: requestedPort,
|
|
6184
6377
|
config,
|
|
6378
|
+
personalConnectors: readAppPersonalConnectors(cwd),
|
|
6185
6379
|
};
|
|
6186
6380
|
// Asset mode needs its deps before we bind anything; the check is cheap and
|
|
6187
6381
|
// failing early avoids leaving the proxy port bound on a misconfigured app.
|
|
@@ -6682,6 +6876,10 @@ async function handleLocalApi(ctx, req, res, url) {
|
|
|
6682
6876
|
await handleFiles(ctx, req, res, path, url);
|
|
6683
6877
|
return;
|
|
6684
6878
|
}
|
|
6879
|
+
if (path === "/personal-connections" || path.startsWith("/personal-connections/")) {
|
|
6880
|
+
await handlePersonalConnections(ctx, req, res, path, url);
|
|
6881
|
+
return;
|
|
6882
|
+
}
|
|
6685
6883
|
// Proxied surfaces → forward to the real instance with the saved token.
|
|
6686
6884
|
if (path === "/connections" ||
|
|
6687
6885
|
path === "/queries" ||
|
|
@@ -7030,6 +7228,161 @@ export function devUpstreamAuthFallback(status, degradeOk) {
|
|
|
7030
7228
|
},
|
|
7031
7229
|
};
|
|
7032
7230
|
}
|
|
7231
|
+
// ── personal connectors in dev ────────────────────────────────────────────────
|
|
7232
|
+
//
|
|
7233
|
+
// The SDK's `personalConnections.*` methods hit the app plane in prod, which is
|
|
7234
|
+
// bounded by the app's ratified manifest. `railcode dev` has no ratified app, so
|
|
7235
|
+
// it reproduces that bound HERE from the local manifest.yaml and forwards the
|
|
7236
|
+
// actual work to the USER plane (`/api/personal-connections/*`) as the signed-in
|
|
7237
|
+
// developer — the same "run as you, not as a deployed app" model dev uses for
|
|
7238
|
+
// SQL and LLM. The result: declared tools run against your real connection,
|
|
7239
|
+
// undeclared ones are refused exactly as they will be in prod, so a bad call
|
|
7240
|
+
// fails locally instead of only after deploy.
|
|
7241
|
+
function readAppPersonalConnectors(cwd) {
|
|
7242
|
+
const path = join(cwd, APP_MANIFEST_NAME);
|
|
7243
|
+
if (!existsSync(path))
|
|
7244
|
+
return [];
|
|
7245
|
+
try {
|
|
7246
|
+
return parseManifestYaml(readFileSync(path, "utf8")).personal_connectors ?? [];
|
|
7247
|
+
}
|
|
7248
|
+
catch {
|
|
7249
|
+
// A malformed manifest is caught (loudly) at deploy; dev must not crash on it.
|
|
7250
|
+
// Treating it as "nothing declared" makes personal-connector calls 403 here,
|
|
7251
|
+
// which is the safe direction.
|
|
7252
|
+
return [];
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7255
|
+
// The toolkit slugs an app declared (any tool scope). Mirrors the server's
|
|
7256
|
+
// `manifest.personal_connector_toolkits`.
|
|
7257
|
+
function pcDeclaredToolkits(entries) {
|
|
7258
|
+
return new Set(entries.map((e) => e.split(":", 1)[0]));
|
|
7259
|
+
}
|
|
7260
|
+
// May this app call `tool` of `toolkit`? Mirrors the server's
|
|
7261
|
+
// `manifest.personal_connector_allowed`: a bare/`*` entry covers the whole
|
|
7262
|
+
// toolkit, else the tool must match exactly.
|
|
7263
|
+
function pcAllows(entries, toolkit, tool) {
|
|
7264
|
+
for (const entry of entries) {
|
|
7265
|
+
const idx = entry.indexOf(":");
|
|
7266
|
+
const entryToolkit = idx === -1 ? entry : entry.slice(0, idx);
|
|
7267
|
+
if (entryToolkit !== toolkit)
|
|
7268
|
+
continue;
|
|
7269
|
+
const entryTool = idx === -1 ? "" : entry.slice(idx + 1);
|
|
7270
|
+
if (entryTool === "" || entryTool === "*" || entryTool === tool)
|
|
7271
|
+
return true;
|
|
7272
|
+
}
|
|
7273
|
+
return false;
|
|
7274
|
+
}
|
|
7275
|
+
// Forward one request to the USER plane and return its parsed body. Kept separate
|
|
7276
|
+
// from forwardCompute because these need post-filtering by the local manifest,
|
|
7277
|
+
// not a raw pass-through.
|
|
7278
|
+
async function pcForward(ctx, method, apiPath, body) {
|
|
7279
|
+
const creds = devCreds(ctx.config);
|
|
7280
|
+
if (!creds) {
|
|
7281
|
+
return {
|
|
7282
|
+
status: 503,
|
|
7283
|
+
body: {
|
|
7284
|
+
error: "not_logged_in",
|
|
7285
|
+
message: "Run `railcode login` to use personal connectors in dev.",
|
|
7286
|
+
},
|
|
7287
|
+
};
|
|
7288
|
+
}
|
|
7289
|
+
const headers = {
|
|
7290
|
+
Authorization: `Bearer ${creds.token}`,
|
|
7291
|
+
"X-Railcode-Source": "local-dev",
|
|
7292
|
+
};
|
|
7293
|
+
if (body !== undefined)
|
|
7294
|
+
headers["Content-Type"] = "application/json";
|
|
7295
|
+
let upstream;
|
|
7296
|
+
try {
|
|
7297
|
+
upstream = await fetch(`${creds.apiUrl}/api${apiPath}`, {
|
|
7298
|
+
method,
|
|
7299
|
+
headers,
|
|
7300
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
7301
|
+
redirect: "manual",
|
|
7302
|
+
});
|
|
7303
|
+
}
|
|
7304
|
+
catch {
|
|
7305
|
+
return { status: 503, body: { error: "unreachable", message: `Could not reach ${creds.apiUrl}.` } };
|
|
7306
|
+
}
|
|
7307
|
+
const text = await upstream.text();
|
|
7308
|
+
let parsed = null;
|
|
7309
|
+
try {
|
|
7310
|
+
parsed = text ? JSON.parse(text) : null;
|
|
7311
|
+
}
|
|
7312
|
+
catch {
|
|
7313
|
+
parsed = text;
|
|
7314
|
+
}
|
|
7315
|
+
return { status: upstream.status, body: parsed };
|
|
7316
|
+
}
|
|
7317
|
+
const PC_NOT_IN_MANIFEST = (label) => ({
|
|
7318
|
+
detail: `personal connector "${label}" was not in the manifest`,
|
|
7319
|
+
});
|
|
7320
|
+
async function handlePersonalConnections(ctx, req, res, path, url) {
|
|
7321
|
+
const entries = ctx.personalConnectors;
|
|
7322
|
+
const declared = pcDeclaredToolkits(entries);
|
|
7323
|
+
const rest = path.slice("/personal-connections".length); // "", "/gmail/tools", "/call", ...
|
|
7324
|
+
// list() — forward, then filter to the toolkits this app declared (the app
|
|
7325
|
+
// plane never shows an app the user's OTHER connected accounts).
|
|
7326
|
+
if (rest === "" && req.method === "GET") {
|
|
7327
|
+
const { status, body } = await pcForward(ctx, "GET", "/personal-connections");
|
|
7328
|
+
const filtered = Array.isArray(body)
|
|
7329
|
+
? body.filter((c) => declared.has(c.toolkit))
|
|
7330
|
+
: body;
|
|
7331
|
+
return sendJson(res, status === 200 ? 200 : status, status === 200 ? filtered : body);
|
|
7332
|
+
}
|
|
7333
|
+
// tools(toolkit) — require the toolkit is declared, then filter to the declared
|
|
7334
|
+
// tool subset. Both mirror the app plane.
|
|
7335
|
+
const toolsMatch = rest.match(/^\/([^/]+)\/tools$/);
|
|
7336
|
+
if (toolsMatch && req.method === "GET") {
|
|
7337
|
+
const toolkit = decodeURIComponent(toolsMatch[1]);
|
|
7338
|
+
if (!declared.has(toolkit))
|
|
7339
|
+
return sendJson(res, 403, PC_NOT_IN_MANIFEST(toolkit));
|
|
7340
|
+
const { status, body } = await pcForward(ctx, "GET", `/personal-connections/toolkits/${encodeURIComponent(toolkit)}/tools`);
|
|
7341
|
+
const filtered = status === 200 && Array.isArray(body)
|
|
7342
|
+
? body.filter((t) => pcAllows(entries, toolkit, t.slug))
|
|
7343
|
+
: body;
|
|
7344
|
+
return sendJson(res, status, status === 200 ? filtered : body);
|
|
7345
|
+
}
|
|
7346
|
+
// connect(toolkit) — require declared (an app shouldn't walk you through
|
|
7347
|
+
// connecting an account it can never use), then forward.
|
|
7348
|
+
const connectMatch = rest.match(/^\/([^/]+)\/connect$/);
|
|
7349
|
+
if (connectMatch && req.method === "POST") {
|
|
7350
|
+
const toolkit = decodeURIComponent(connectMatch[1]);
|
|
7351
|
+
if (!declared.has(toolkit))
|
|
7352
|
+
return sendJson(res, 403, PC_NOT_IN_MANIFEST(toolkit));
|
|
7353
|
+
const { status, body } = await pcForward(ctx, "POST", `/personal-connections/${encodeURIComponent(toolkit)}/connect`);
|
|
7354
|
+
return sendJson(res, status, body);
|
|
7355
|
+
}
|
|
7356
|
+
// call(toolkit, tool, args) — the load-bearing gate. Scoped by connector, so
|
|
7357
|
+
// the bound is a direct match against the local manifest: the toolkit must be
|
|
7358
|
+
// declared AND the specific tool permitted, else 403 (never forwarded). No
|
|
7359
|
+
// declaration at all ⇒ refused, matching the prod chokepoint's "no manifest =
|
|
7360
|
+
// refuse". The user plane then verifies the (toolkit, tool) pair is real.
|
|
7361
|
+
if (rest === "/call" && req.method === "POST") {
|
|
7362
|
+
const raw = await readBody(req);
|
|
7363
|
+
let payload;
|
|
7364
|
+
try {
|
|
7365
|
+
payload = raw ? JSON.parse(raw.toString("utf8")) : {};
|
|
7366
|
+
}
|
|
7367
|
+
catch {
|
|
7368
|
+
return sendJson(res, 400, { detail: "body must be JSON" });
|
|
7369
|
+
}
|
|
7370
|
+
const { toolkit, tool } = payload;
|
|
7371
|
+
if (!toolkit || !tool) {
|
|
7372
|
+
return sendJson(res, 400, { detail: '"toolkit" and "tool" are required' });
|
|
7373
|
+
}
|
|
7374
|
+
if (!pcAllows(entries, toolkit, tool)) {
|
|
7375
|
+
return sendJson(res, 403, PC_NOT_IN_MANIFEST(`${toolkit}:${tool}`));
|
|
7376
|
+
}
|
|
7377
|
+
const { status, body } = await pcForward(ctx, "POST", "/personal-connections/call", {
|
|
7378
|
+
toolkit,
|
|
7379
|
+
tool,
|
|
7380
|
+
arguments: payload.arguments ?? {},
|
|
7381
|
+
});
|
|
7382
|
+
return sendJson(res, status, body);
|
|
7383
|
+
}
|
|
7384
|
+
sendJson(res, 404, { detail: "not found" });
|
|
7385
|
+
}
|
|
7033
7386
|
async function forwardCompute(ctx, req, res, path, url) {
|
|
7034
7387
|
// Never reply 401 to the browser — the SDK reloads the page on 401 (loop).
|
|
7035
7388
|
// 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/package.json
CHANGED
package/static/sdk.js
CHANGED
|
@@ -261,7 +261,7 @@
|
|
|
261
261
|
window.addEventListener("railcode:inspector", sync);
|
|
262
262
|
}
|
|
263
263
|
me().then((who) => {
|
|
264
|
-
if (who.user.is_admin) mount2();
|
|
264
|
+
if (who.user.is_admin || who.user.is_app_owner) mount2();
|
|
265
265
|
}).catch(() => {
|
|
266
266
|
});
|
|
267
267
|
|
|
@@ -711,6 +711,42 @@
|
|
|
711
711
|
var llm = { generate, stream };
|
|
712
712
|
var llmProviders = () => track("llm", "llmProviders()", () => call("GET", "/llm/providers"));
|
|
713
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(toolkit, tool, args = {}) {
|
|
740
|
+
return track(
|
|
741
|
+
"personalConnections",
|
|
742
|
+
`personalConnections.call(${q(toolkit)}, ${q(tool)})`,
|
|
743
|
+
() => call("POST", "/personal-connections/call", {
|
|
744
|
+
body: { toolkit, tool, arguments: args }
|
|
745
|
+
})
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
var personalConnections = { list: list2, connect, tools, call: callTool };
|
|
749
|
+
|
|
714
750
|
// ../sdk/src/queries.ts
|
|
715
751
|
var PARAMS_LABEL_MAX2 = 40;
|
|
716
752
|
var query = (name, params) => {
|
|
@@ -769,6 +805,7 @@
|
|
|
769
805
|
target.email = email;
|
|
770
806
|
target.llm = llm;
|
|
771
807
|
target.llmProviders = llmProviders;
|
|
808
|
+
target.personalConnections = personalConnections;
|
|
772
809
|
target.data = data;
|
|
773
810
|
target.postgres = postgres;
|
|
774
811
|
target.bigquery = bigquery;
|