railcode 0.1.25 → 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 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
@@ -3809,6 +3809,12 @@ Agent options:
3809
3809
  --input-file <path> Read invoke/test input JSON from a file
3810
3810
  --trace Include the run step trace in human output
3811
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.
3812
3818
 
3813
3819
  Schedule options:
3814
3820
  --name <name> Schedule name (default: default on create)
@@ -3881,9 +3887,10 @@ async function commandAgentList(args) {
3881
3887
  console.log("No managed agents in this organization.");
3882
3888
  return;
3883
3889
  }
3884
- console.log(formatTable(["name", "model", "uuid", "updated"], agents.map((agent) => [
3890
+ console.log(formatTable(["name", "model", "visibility", "uuid", "updated"], agents.map((agent) => [
3885
3891
  agent.name,
3886
3892
  typeof agent.manifest.model === "string" ? agent.manifest.model : "-",
3893
+ agent.visibility,
3887
3894
  agent.uuid,
3888
3895
  shortDate(agent.updated_at),
3889
3896
  ])));
@@ -3918,13 +3925,14 @@ async function commandAgentPull(args) {
3918
3925
  process.stdout.write(manifest);
3919
3926
  }
3920
3927
  async function commandAgentCreate(args) {
3921
- assertKnownAgentOptions(args, ["file", "json", "apiUrl"]);
3928
+ assertKnownAgentOptions(args, ["file", "json", "visibility", "apiUrl"]);
3922
3929
  const manifest = readAgentManifest(args);
3930
+ const visibility = agentVisibilityOption(args);
3923
3931
  const config = requireLogin(args);
3924
3932
  const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents`, {
3925
3933
  method: "POST",
3926
3934
  headers: { "Content-Type": "application/json" },
3927
- body: JSON.stringify({ manifest }),
3935
+ body: JSON.stringify({ manifest, ...(visibility !== undefined ? { visibility } : {}) }),
3928
3936
  });
3929
3937
  if (resp.status === 401)
3930
3938
  await reLoginOrThrow();
@@ -3932,14 +3940,15 @@ async function commandAgentCreate(args) {
3932
3940
  printAgentMutation(result, args.options.json === true, "created");
3933
3941
  }
3934
3942
  async function commandAgentUpdate(args) {
3935
- assertKnownAgentOptions(args, ["file", "json", "apiUrl"]);
3943
+ assertKnownAgentOptions(args, ["file", "json", "visibility", "apiUrl"]);
3936
3944
  const ref = agentArg(args, "update");
3937
3945
  const manifest = readAgentManifest(args);
3946
+ const visibility = agentVisibilityOption(args);
3938
3947
  const config = requireLogin(args);
3939
3948
  const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/${encodeURIComponent(ref)}`, {
3940
3949
  method: "PUT",
3941
3950
  headers: { "Content-Type": "application/json" },
3942
- body: JSON.stringify({ manifest }),
3951
+ body: JSON.stringify({ manifest, ...(visibility !== undefined ? { visibility } : {}) }),
3943
3952
  });
3944
3953
  if (resp.status === 401)
3945
3954
  await reLoginOrThrow();
@@ -3968,14 +3977,15 @@ async function commandAgentDelete(args) {
3968
3977
  console.log(`Agent "${ref}" deleted.`);
3969
3978
  }
3970
3979
  async function commandAgentTest(args) {
3971
- assertKnownAgentOptions(args, ["file", "input", "inputFile", "json", "trace", "apiUrl"]);
3980
+ assertKnownAgentOptions(args, ["file", "input", "inputFile", "json", "trace", "visibility", "apiUrl"]);
3972
3981
  const manifest = readAgentManifest(args);
3973
3982
  const input = readJsonInput(args);
3983
+ const visibility = agentVisibilityOption(args);
3974
3984
  const config = requireLogin(args);
3975
3985
  const resp = await authedFetch(config, `/api/organizations/${config.orgUuid}/agents/test`, {
3976
3986
  method: "POST",
3977
3987
  headers: { "Content-Type": "application/json" },
3978
- body: JSON.stringify({ manifest, input }),
3988
+ body: JSON.stringify({ manifest, input, ...(visibility !== undefined ? { visibility } : {}) }),
3979
3989
  });
3980
3990
  if (resp.status === 401)
3981
3991
  await reLoginOrThrow();
@@ -4178,6 +4188,17 @@ function readAgentManifest(args) {
4178
4188
  throw new CliError("Pass --file <agent-manifest.json>.", 2);
4179
4189
  return readJsonObjectFile(path, "Agent manifest");
4180
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
+ }
4181
4202
  function readJsonObjectFile(path, label) {
4182
4203
  const resolved = resolve(process.cwd(), path);
4183
4204
  if (!existsSync(resolved))
@@ -4264,6 +4285,7 @@ function printAgentMutation(result, json, verb) {
4264
4285
  }
4265
4286
  console.log(`Agent "${result.agent.name}" ${verb}.`);
4266
4287
  console.log(`UUID: ${result.agent.uuid}`);
4288
+ console.log(`Visibility: ${result.agent.visibility}`);
4267
4289
  if (result.warnings.length > 0) {
4268
4290
  console.log("");
4269
4291
  console.log("Warnings:");
@@ -4276,6 +4298,9 @@ function printAgent(agent) {
4276
4298
  console.log(`UUID: ${agent.uuid}`);
4277
4299
  console.log(`Description: ${agent.description || "-"}`);
4278
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 ?? "-"}`);
4279
4304
  console.log(`Updated: ${agent.updated_at}`);
4280
4305
  console.log(`Content hash: ${agent.content_hash}`);
4281
4306
  }
@@ -5124,13 +5149,13 @@ Usage:
5124
5149
  railcode personal-connectors list Toolkits this deployment brokers + your status
5125
5150
  railcode personal-connectors tools <toolkit> The callable tools of a toolkit (and their inputs)
5126
5151
  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
5152
+ railcode personal-connectors call <toolkit> <tool> [--args '<json>'] Run one tool on your own account
5128
5153
 
5129
5154
  Aliases: personal-connector, pc
5130
5155
 
5131
5156
  Discovery is what you build against: \`tools <toolkit> --json\` shows each tool's
5132
5157
  input schema, so you know what to put in an app's \`personal_connectors:\` and what
5133
- \`personalConnections.call('<tool>', {...})\` expects.
5158
+ \`personalConnections.call('<toolkit>', '<tool>', {...})\` expects.
5134
5159
 
5135
5160
  Options:
5136
5161
  --json Print raw JSON instead of a table
@@ -5235,9 +5260,12 @@ async function commandPcConnect(args) {
5235
5260
  }
5236
5261
  async function commandPcCall(args) {
5237
5262
  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);
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
+ }
5241
5269
  let toolArgs = {};
5242
5270
  if (typeof args.options.args === "string") {
5243
5271
  try {
@@ -5251,7 +5279,7 @@ async function commandPcCall(args) {
5251
5279
  const resp = await authedFetch(config, `/api/personal-connections/call`, {
5252
5280
  method: "POST",
5253
5281
  headers: { "Content-Type": "application/json" },
5254
- body: JSON.stringify({ tool, arguments: toolArgs }),
5282
+ body: JSON.stringify({ toolkit, tool, arguments: toolArgs }),
5255
5283
  });
5256
5284
  if (resp.status === 401)
5257
5285
  await reLoginOrThrow();
@@ -5264,7 +5292,7 @@ async function commandPcCall(args) {
5264
5292
  if (resp.status === 409) {
5265
5293
  throw new CliError(await errorDetail(resp), 1);
5266
5294
  }
5267
- const body = await readJsonOrThrow(resp, `Call ${tool}`);
5295
+ const body = await readJsonOrThrow(resp, `Call ${toolkit}/${tool}`);
5268
5296
  console.log(JSON.stringify(body, null, 2));
5269
5297
  }
5270
5298
  // ---------------------------------------------------------------------------
@@ -7286,17 +7314,6 @@ async function pcForward(ctx, method, apiPath, body) {
7286
7314
  }
7287
7315
  return { status: upstream.status, body: parsed };
7288
7316
  }
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
7317
  const PC_NOT_IN_MANIFEST = (label) => ({
7301
7318
  detail: `personal connector "${label}" was not in the manifest`,
7302
7319
  });
@@ -7336,10 +7353,11 @@ async function handlePersonalConnections(ctx, req, res, path, url) {
7336
7353
  const { status, body } = await pcForward(ctx, "POST", `/personal-connections/${encodeURIComponent(toolkit)}/connect`);
7337
7354
  return sendJson(res, status, body);
7338
7355
  }
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".
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.
7343
7361
  if (rest === "/call" && req.method === "POST") {
7344
7362
  const raw = await readBody(req);
7345
7363
  let payload;
@@ -7349,16 +7367,15 @@ async function handlePersonalConnections(ctx, req, res, path, url) {
7349
7367
  catch {
7350
7368
  return sendJson(res, 400, { detail: "body must be JSON" });
7351
7369
  }
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));
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}`));
7360
7376
  }
7361
7377
  const { status, body } = await pcForward(ctx, "POST", "/personal-connections/call", {
7378
+ toolkit,
7362
7379
  tool,
7363
7380
  arguments: payload.arguments ?? {},
7364
7381
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "railcode",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Developer CLI for the multi-tenant Railcode platform: log in, scaffold, and deploy static apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/static/sdk.js CHANGED
@@ -736,12 +736,12 @@
736
736
  () => call("GET", `/personal-connections/${encodeURIComponent(toolkit)}/tools`)
737
737
  );
738
738
  }
739
- function callTool(tool, args = {}) {
739
+ function callTool(toolkit, tool, args = {}) {
740
740
  return track(
741
741
  "personalConnections",
742
- `personalConnections.call(${q(tool)})`,
742
+ `personalConnections.call(${q(toolkit)}, ${q(tool)})`,
743
743
  () => call("POST", "/personal-connections/call", {
744
- body: { tool, arguments: args }
744
+ body: { toolkit, tool, arguments: args }
745
745
  })
746
746
  );
747
747
  }