withruntime 0.21.0 → 0.23.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/cli.js CHANGED
@@ -55,6 +55,10 @@ Account
55
55
  flag one selector, any may match); --only-sandbox:
56
56
  only that sandbox. A new value keeps the scope;
57
57
  --all-sandboxes gives it to every sandbox again
58
+ secrets set <NAME> --keep-value [--host <host>]... [--allow '[GET] /path']...
59
+ Narrow where it goes without its value: a subset of
60
+ its hosts, only requests its rules allow now.
61
+ Anything wider needs the value
58
62
  secrets set <NAME> --jobs [--host ...] A copy scheduled jobs put in their environment
59
63
  (job create --secret NAME); with --host, both
60
64
  secrets scope <NAME> --only-label k=v... | --only-sandbox <id>... | --all-sandboxes
@@ -63,6 +67,9 @@ Account
63
67
  secrets rotate <NAME> A new version of the jobs copy, from standard input
64
68
  secrets reveal <NAME> [--version N] Print the jobs copy's value. The sandboxes' copy is
65
69
  never readable, by anyone
70
+ secrets share <NAME> --agent <id>... | --all | --none
71
+ Which of your keys may use the jobs copy besides the one
72
+ that stored it (ids: from 'me'); --all: every key
66
73
  secrets rm <NAME> Delete a secret, every copy
67
74
  keys create [--name <name>] [--read-only] [--daily-limit <usd>] [--account-wide]
68
75
  A new API key for CI, approved by an owner in the browser
@@ -2422,7 +2429,17 @@ export async function secretsCommand(argv, env, out,
2422
2429
  /** Tests give the value here; the CLI reads standard input. */
2423
2430
  read = readSecret) {
2424
2431
  const [verb, ...rest] = argv;
2425
- const args = parse(rest, ["jobs", "all-sandboxes"], ["host", "header", "format", "allow", "value", "version", "only-label", "only-sandbox"]);
2432
+ const args = parse(rest, ["jobs", "all-sandboxes", "keep-value", "all", "none"], [
2433
+ "host",
2434
+ "header",
2435
+ "format",
2436
+ "allow",
2437
+ "value",
2438
+ "version",
2439
+ "only-label",
2440
+ "only-sandbox",
2441
+ "agent",
2442
+ ]);
2426
2443
  const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
2427
2444
  const rows = (list) => list.length
2428
2445
  ? table([
@@ -2464,13 +2481,38 @@ read = readSecret) {
2464
2481
  args.flags.has("only-sandbox") ||
2465
2482
  args.flags.has("all-sandboxes")))
2466
2483
  throw usage(`rotate replaces the copy for jobs. ${jobsOnly}; to change the sandboxes' copy, set it again with --host.`);
2484
+ if (verb === "set" && has(args, "keep-value")) {
2485
+ // Narrowing needs no value: it only takes places away (secrets.narrow).
2486
+ if (jobs ||
2487
+ args.flags.has("value") ||
2488
+ args.flags.has("header") ||
2489
+ args.flags.has("format") ||
2490
+ args.flags.has("only-label") ||
2491
+ args.flags.has("only-sandbox") ||
2492
+ args.flags.has("all-sandboxes"))
2493
+ throw usage(`--keep-value changes only --host and --allow, and only to narrower ones. Change who gets it with \`${me} secrets scope\`; anything else needs the value.`);
2494
+ const allow = args.flags.get("allow");
2495
+ if (!hosts.length && !allow)
2496
+ throw usage("With --keep-value, give the narrower --host and/or --allow it keeps.");
2497
+ const saved = await (await client(env)).secrets.narrow(name, {
2498
+ ...(hosts.length ? { hosts } : {}),
2499
+ ...(allow ? { rules: parseSecretRules(allow) } : {}),
2500
+ });
2501
+ print([
2502
+ `Narrowed ${saved.name}; its value is unchanged. It goes only to ${saved.hosts.join(", ")}${saved.rules?.length ? `, only in these requests: ${ruleText(saved.rules)}` : ""}.`,
2503
+ ...(saved.enforced
2504
+ ? []
2505
+ : ["A host is catching up; running sandboxes have it within a minute."]),
2506
+ ].join("\n"), saved);
2507
+ return 0;
2508
+ }
2467
2509
  if (!hosts.length && !jobs)
2468
2510
  throw usage("Give --host once for each host the value may go to, such as --host api.openai.com, and/or --jobs to let scheduled jobs use it.");
2469
2511
  if (args.flags.has("value"))
2470
2512
  throw usage('The value is read from standard input, never an argument: printf %s "$KEY" | runtime secrets set NAME --host ...');
2471
2513
  const value = await read();
2472
2514
  if (!value)
2473
- throw usage('The value is empty. Pipe it in: printf %s "$KEY" | runtime secrets set NAME --host ...');
2515
+ throw usage('The value is empty. Pipe it in: printf %s "$KEY" | runtime secrets set NAME --host ...; or, to narrow its hosts and rules without it, add --keep-value.');
2474
2516
  const runtime = await client(env);
2475
2517
  if (verb === "rotate") {
2476
2518
  const rotated = await runtime.secrets.rotate(name, value);
@@ -2524,6 +2566,17 @@ read = readSecret) {
2524
2566
  print(`${saved.name}: ${saved.only ? `only ${scopeText(saved.only)}` : "every sandbox"}.${saved.enforced ? "" : " A host is catching up; running sandboxes have it within a minute."}`, saved);
2525
2567
  return 0;
2526
2568
  }
2569
+ if (verb === "share") {
2570
+ const name = need(args.positional[0], "the secret's name");
2571
+ const named = args.flags.get("agent") ?? [];
2572
+ if (Number(named.length > 0) + Number(has(args, "all")) + Number(has(args, "none")) !== 1)
2573
+ throw usage("Say who may use it: --agent <id> (once per agent), --all for every key of yours, or --none for only the key that stored it.");
2574
+ const saved = await (await client(env)).secrets.share(name, has(args, "all") ? "all" : named);
2575
+ print(`${saved.name}: ${saved.usableBy === "all"
2576
+ ? "every key of yours"
2577
+ : `only the key that stored it${saved.usableBy.length ? `, and ${saved.usableBy.length} named agent${saved.usableBy.length === 1 ? "" : "s"}` : ""}`}.`, saved);
2578
+ return 0;
2579
+ }
2527
2580
  if (verb === "reveal") {
2528
2581
  const name = need(args.positional[0], "the secret's name");
2529
2582
  const version = flag(args, "version");
@@ -2539,7 +2592,7 @@ read = readSecret) {
2539
2592
  print(`Deleted ${name}${removed.from.length > 1 ? " for sandboxes and jobs" : removed.from[0] === "jobs" ? " for jobs" : ""}.`, removed);
2540
2593
  return 0;
2541
2594
  }
2542
- throw usage("secrets takes set, scope, ls, get, rotate, reveal or rm. Run `runtime help`.");
2595
+ throw usage("secrets takes set, scope, ls, get, rotate, share, reveal or rm. Run `runtime help`.");
2543
2596
  }
2544
2597
  async function readInput(source) {
2545
2598
  if (source !== "-")
@@ -2701,9 +2754,11 @@ const KEYS_HELP = `runtime keys <command>
2701
2754
  stdout, and lasts until revoked. --json prints it as JSON, still alone on
2702
2755
  stdout. --account-wide makes a key that sees and uses every sandbox and
2703
2756
  other resource in the account; without it, a key sees only what it makes.
2704
- create --sandbox <id,...> --actions <action,...> [--name <name>] [--daily-limit <usd>]
2705
- A key limited to those sandboxes (up to 20, by id) and those actions on
2706
- them, made by the saved key: get reads them; create starts, wakes and
2757
+ create --sandbox <id,...> --actions <action,...> [--name <name>] [--daily-limit <usd>] [--browser]
2758
+ A key limited to those sandboxes (up to 20, by id, whichever key made
2759
+ them) and those actions on them, made by the saved key, or with no saved
2760
+ key (or --browser) approved by an owner or admin in the browser, as
2761
+ above: get reads them; create starts, wakes and
2707
2762
  restarts them, and needs --daily-limit when the making key has one; stop
2708
2763
  stops and pauses them; exec runs commands; read_file and write_file reach
2709
2764
  their files. It is served nothing else: no list, no other sandbox, no
@@ -2715,6 +2770,7 @@ const KEYS_HELP = `runtime keys <command>
2715
2770
  runtime keys create --name worker-1 --daily-limit 5
2716
2771
  runtime keys create --name ci --daily-limit 25 --browser | gh secret set RUNTIME_API_KEY
2717
2772
  runtime keys create --name watcher --sandbox sbx_id --actions get,create --daily-limit 2
2773
+ runtime keys create --name helper --sandbox sbx_a,sbx_b --actions get,exec,read_file,write_file --browser
2718
2774
 
2719
2775
  Making, listing and revoking keys with a key needs one an owner or admin made.
2720
2776
  Every key is also at https://withruntime.com/account/keys.
@@ -2972,12 +3028,14 @@ async function keys(argv, env, out) {
2972
3028
  return 0;
2973
3029
  }
2974
3030
  /** `runtime keys create --sandbox ... --actions ...`: a key limited to
2975
- * sandboxes, made by the saved key (never in the browser: the keys page
2976
- * makes none). */
3031
+ * sandboxes, made by the saved key, or, with no key on this machine or with
3032
+ * --browser, asked for and approved by an owner or admin in the browser
3033
+ * (0921): an agent whose person reaches Runtime only through a connector
3034
+ * holds no key to make one with. */
2977
3035
  async function limitedKey(args, env, out, limit) {
2978
- for (const refused of ["read-only", "account-wide", "browser"])
3036
+ for (const refused of ["read-only", "account-wide"])
2979
3037
  if (has(args, refused))
2980
- throw usage(`A key limited to sandboxes reaches only them and is made by this machine's key, so it takes no --${refused}.`);
3038
+ throw usage(`A key limited to sandboxes reaches only them, so it takes no --${refused}.`);
2981
3039
  const list = (name) => (flag(args, name) ?? "")
2982
3040
  .split(",")
2983
3041
  .map((each) => each.trim())
@@ -2986,15 +3044,30 @@ async function limitedKey(args, env, out, limit) {
2986
3044
  const actions = list("actions");
2987
3045
  if (!sandboxes.length || !actions.length)
2988
3046
  throw usage(`Usage: ${named("runtime keys create --sandbox <id,...> --actions <get,create,stop,exec,read_file,write_file> [--daily-limit <usd>]")}`);
2989
- const { dailyLimitMicros } = await import("./keys.js");
2990
- const made = await (await client(env)).keys.create({
3047
+ const { createKey, dailyLimitMicros } = await import("./keys.js");
3048
+ const request = {
2991
3049
  name: flag(args, "name") ?? "Sandbox key",
2992
3050
  sandboxes,
2993
3051
  actions: actions,
2994
3052
  dailyLimitMicros: limit === null ? null : dailyLimitMicros(limit),
2995
- });
2996
- out.write(out.json ? JSON.stringify(made) : made.key);
2997
- out.error(`Created key "${made.name}", limited to ${made.sandboxes?.length === 1 ? "sandbox" : "sandboxes"} ${(made.sandboxes ?? []).join(", ")} for ${(made.actions ?? []).join(", ")}${made.dailyLimitMicros === null ? "" : `, daily limit ${usdOf(made.dailyLimitMicros)}`}, made by this machine's key. Store it now: it is not shown again.\nIt calls those sandboxes by id and nothing else; revoke it with ${named("runtime keys revoke")} ${made.keyId}.`);
3053
+ };
3054
+ const saved = has(args, "browser")
3055
+ ? null
3056
+ : env.RUNTIME_API_KEY || (await connectionStore(env).read())?.key || null;
3057
+ const asked = saved
3058
+ ? null
3059
+ : await createKey({ ...request, access: "full" }, env, { notify: out.error });
3060
+ const made = asked
3061
+ ? { ...asked, sandboxes, actions }
3062
+ : await new Runtime({
3063
+ apiKey: saved,
3064
+ baseUrl: env.RUNTIME_API_URL,
3065
+ maxRetries: 4,
3066
+ }).keys.create(request);
3067
+ const { confirm, ...shown } = made;
3068
+ out.write(out.json ? JSON.stringify(shown) : shown.key);
3069
+ out.error(`Created key "${shown.name}", limited to ${shown.sandboxes?.length === 1 ? "sandbox" : "sandboxes"} ${(shown.sandboxes ?? []).join(", ")} for ${(shown.actions ?? []).join(", ")}${shown.dailyLimitMicros === null ? "" : `, daily limit ${usdOf(shown.dailyLimitMicros)}`}, ${saved ? "made by this machine's key" : "approved in the browser"}. Store it now: it is not shown again.\nIt calls those sandboxes by id and nothing else; revoke it at https://withruntime.com/account/keys${saved ? ` or with ${named("runtime keys revoke")} ${shown.keyId}` : ""}.`);
3070
+ await confirm?.();
2998
3071
  return 0;
2999
3072
  }
3000
3073
  const SNAPSHOT_HELP = `runtime snapshots <command>
package/dist/keys.d.ts CHANGED
@@ -11,6 +11,12 @@ export type KeyRequest = {
11
11
  reach?: KeyReach;
12
12
  /** The most its agent may spend in any 24 hours, or null for none. */
13
13
  dailyLimitMicros: number | null;
14
+ /** A key limited to these sandboxes (1 to 20, by id), whichever key made
15
+ * them, and to `actions` on them; access is then "full" and reach is not
16
+ * given. Only an owner or admin approves one. Without "create" it cannot
17
+ * start, wake or restart them; a running one is still billed as usual. */
18
+ sandboxes?: string[];
19
+ actions?: Array<"get" | "create" | "stop" | "exec" | "read_file" | "write_file">;
14
20
  };
15
21
  export type CreatedKey = KeyRequest & {
16
22
  key: string;
package/dist/keys.js CHANGED
@@ -60,6 +60,13 @@ export async function createKey(request, env, options = {}) {
60
60
  throw failure("usage", "A read-only key cannot spend, so it takes no --daily-limit.");
61
61
  if (request.access === "read" && request.reach === "account")
62
62
  throw failure("usage", "A read-only key already sees the whole account, so it takes no --account-wide.");
63
+ const limited = request.sandboxes !== undefined || request.actions !== undefined;
64
+ if (limited && (!request.sandboxes?.length || !request.actions?.length))
65
+ throw failure("usage", "A key limited to sandboxes names at least one sandbox and one action.");
66
+ if (limited && (request.access !== "full" || request.reach !== undefined))
67
+ throw failure("usage", "A key limited to sandboxes reaches only them, so it takes no --read-only or --account-wide.");
68
+ if (limited && !request.actions.includes("create") && request.dailyLimitMicros !== null)
69
+ throw failure("usage", "A key without create cannot spend, so it takes no --daily-limit.");
63
70
  const origins = connectionOrigins(env);
64
71
  const fetcher = options.fetch ?? envFetch;
65
72
  const notify = options.notify ?? ((message) => process.stderr.write(`${message}\n`));
@@ -105,6 +112,7 @@ export async function createKey(request, env, options = {}) {
105
112
  access: request.access,
106
113
  ...(request.reach === "account" ? { reach: "account" } : {}),
107
114
  dailyLimitMicros: request.dailyLimitMicros,
115
+ ...(limited ? { sandboxes: request.sandboxes, actions: request.actions } : {}),
108
116
  machine: options.machine ?? hostname(),
109
117
  publicKey: pair.publicKey,
110
118
  });
@@ -127,7 +135,7 @@ export async function createKey(request, env, options = {}) {
127
135
  throw failure("key_request_failed", "Runtime returned an unexpected approval address.");
128
136
  const deviceCode = started.deviceCode;
129
137
  const expiresAt = Math.min(started.expiresAt, Date.now() + 15 * 60_000);
130
- notify(`Approve this key in your browser: ${link.href}\nCheck that the page shows code ${started.userCode}. An account owner chooses Create key.\nWaiting for the approval, up to 15 minutes. Stopping this cancels the request.`);
138
+ notify(`Approve this key in your browser: ${link.href}\nCheck that the page shows code ${started.userCode}. An account ${limited ? "owner or admin" : "owner"} chooses Create key.\nWaiting for the approval, up to 15 minutes. Stopping this cancels the request.`);
131
139
  if (options.interactive ?? Boolean(process.stderr.isTTY))
132
140
  await (options.openBrowser ?? browser)(link.href).catch(() => notify("Open the link in a browser where an account owner is signed in."));
133
141
  const stop = (options.onInterrupt ?? interrupted)(async () => {
@@ -184,6 +192,7 @@ export async function createKey(request, env, options = {}) {
184
192
  access: request.access,
185
193
  reach: request.reach ?? "agent",
186
194
  dailyLimitMicros: request.dailyLimitMicros,
195
+ ...(limited ? { sandboxes: request.sandboxes, actions: request.actions } : {}),
187
196
  key,
188
197
  keyId: match[1],
189
198
  agentId: result.agentId,
@@ -36,6 +36,13 @@ export const PRODUCT_NAMES = [
36
36
  { name: "builds", title: "Builds", engine: "machine", singular: "build", old: [] },
37
37
  { name: "agents", title: "Agents", engine: "machine", singular: "agent", old: [] },
38
38
  { name: "ci", title: "CI/CD", engine: "machine", old: [] },
39
+ {
40
+ name: "actions",
41
+ title: "GitHub Actions runners",
42
+ engine: "machine",
43
+ singular: "runner",
44
+ old: [],
45
+ },
39
46
  // Disk
40
47
  { name: "volumes", title: "Volumes", engine: "disk", singular: "volume", old: was("volume") },
41
48
  {
@@ -75,6 +75,10 @@ export type JobSecret = {
75
75
  name: string;
76
76
  ownerId: string;
77
77
  createdBy: string;
78
+ /** Agents besides `createdBy` that may use it, as `principalId`s from
79
+ * `runtime.me()`, or `"all"`: every key of the person. A secret stored on
80
+ * the website is `"all"`. */
81
+ usableBy: string[] | "all";
78
82
  version: number;
79
83
  createdAt: number;
80
84
  updatedAt: number;
@@ -120,6 +124,16 @@ export declare function secrets(t: Transport): {
120
124
  scope: (name: string, only: SecretScope, options?: RequestOptions) => Promise<Secret & {
121
125
  enforced: boolean;
122
126
  }>;
127
+ /** Narrows where a secret goes, without its value: `hosts` a subset of
128
+ * its hosts, `rules` allowing only requests its rules allow now (a secret
129
+ * without rules allows every request). Either absent stays as it is.
130
+ * Anything wider is refused: widening needs the value, through `set`. */
131
+ narrow: (name: string, to: {
132
+ hosts?: string[];
133
+ rules?: SecretRule[];
134
+ }, options?: RequestOptions) => Promise<Secret & {
135
+ enforced: boolean;
136
+ }>;
123
137
  /** The sandboxes' secrets: names, hosts and placeholders. Never values.
124
138
  * `all()` includes the jobs copies. */
125
139
  list: (options?: RequestOptions) => Promise<Secret[]>;
@@ -140,6 +154,11 @@ export declare function secrets(t: Transport): {
140
154
  version: number;
141
155
  value: string;
142
156
  }>;
157
+ /** Choose which of your keys may use the jobs copy besides the one that
158
+ * stored it: agent ids (`principalId` from `me()`), `[]` for only that
159
+ * key, or `"all"` for every key of yours. Replaces the list. Only your
160
+ * person or the key that stored it may call this. */
161
+ share: (name: string, agents: string[] | "all", options?: RequestOptions) => Promise<JobSecret>;
143
162
  /** Delete a secret everywhere it is kept: the sandboxes' value is erased
144
163
  * and its placeholder stops working, and the jobs copy stops being given
145
164
  * to runs (its name stays taken). */
@@ -161,6 +161,16 @@ export function secrets(t) {
161
161
  body: only,
162
162
  ...options,
163
163
  }),
164
+ /** Narrows where a secret goes, without its value: `hosts` a subset of
165
+ * its hosts, `rules` allowing only requests its rules allow now (a secret
166
+ * without rules allows every request). Either absent stays as it is.
167
+ * Anything wider is refused: widening needs the value, through `set`. */
168
+ narrow: (name, to, options) => t.json({
169
+ method: "PUT",
170
+ path: `${path(name)}/rules`,
171
+ body: to,
172
+ ...options,
173
+ }),
164
174
  /** The sandboxes' secrets: names, hosts and placeholders. Never values.
165
175
  * `all()` includes the jobs copies. */
166
176
  list,
@@ -209,6 +219,26 @@ export function secrets(t) {
209
219
  ...options,
210
220
  });
211
221
  },
222
+ /** Choose which of your keys may use the jobs copy besides the one that
223
+ * stored it: agent ids (`principalId` from `me()`), `[]` for only that
224
+ * key, or `"all"` for every key of yours. Replaces the list. Only your
225
+ * person or the key that stored it may call this. */
226
+ share: async (name, agents, options) => {
227
+ const live = await liveCopy(name, options);
228
+ if (!live)
229
+ throw new NotFoundError({
230
+ message: `${name} has no copy for jobs to share.`,
231
+ code: "not_found",
232
+ status: 404,
233
+ hint: "Store one with set(name, { value, jobs: true }).",
234
+ });
235
+ return store({
236
+ method: "POST",
237
+ path: `${jobPath(live.id)}:share`,
238
+ body: { agents },
239
+ ...options,
240
+ });
241
+ },
212
242
  /** Delete a secret everywhere it is kept: the sandboxes' value is erased
213
243
  * and its placeholder stops working, and the jobs copy stops being given
214
244
  * to runs (its name stays taken). */
@@ -1,5 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
- export declare const VERSION = "0.21.0";
2
+ export declare const VERSION = "0.23.0";
3
3
  export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
4
4
  /** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
5
5
  * own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
package/dist/transport.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ConnectionError, DELIBERATE, errorFor, NOTHING_RAN, RuntimeError, WAITS_FOR_ROOM, } from "./errors.js";
2
2
  import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
3
3
  import { warningText } from "./product-renames.js";
4
- export const VERSION = "0.21.0";
4
+ export const VERSION = "0.23.0";
5
5
  export const DEFAULT_BASE_URL = "https://api.withruntime.com";
6
6
  /** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
7
7
  * own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "withruntime",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "license": "Apache-2.0",
5
5
  "mcpName": "com.withruntime/runtime",
6
6
  "type": "module",