@kubb/studio 5.12.0 → 5.12.2

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/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { F as PublishedSnapshot, G as SnapshotUploadMetadata, O as InstalledExtensions, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-Dn1jdnFB.cjs";
1
+ import { A as InstalledExtensions, L as PublishedSnapshot, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, q as SnapshotUploadMetadata, s as AgentPermission, u as ConfigEdit } from "./protocol-DhNrwhqp.cjs";
2
2
  import { Config, Hookable, KubbHooks } from "@kubb/core";
3
3
  //#region src/connect.d.ts
4
4
  /**
@@ -45,7 +45,7 @@ declare global {
45
45
  * A finished command and what it did, such as `applied 2/3 edits to kubb.config.ts`.
46
46
  */
47
47
  'studio:command': [ctx: {
48
- command: 'generate' | 'saveConfig' | 'readFiles' | 'readInput' | 'readDiff';
48
+ command: 'generate' | 'saveConfig' | 'installExtension' | 'updateExtensions' | 'readFiles' | 'readInput' | 'readDiff';
49
49
  info: string;
50
50
  /**
51
51
  * The Studio user who asked for the command, when Studio names one.
@@ -97,6 +97,8 @@ type ConnectAgentOptions = {
97
97
  * What Studio may do in this project. Every permission is off unless granted.
98
98
  */
99
99
  permissions?: Partial<AgentPermissions>;
100
+ /** The CLI entry `installExtension` runs `kubb add` with. Found through the project's `kubb` or `@kubb/cli` dependency when omitted. */
101
+ kubbBin?: string;
100
102
  /**
101
103
  * Receives the `studio:*` hooks and the `kubb:*` hooks of every generation.
102
104
  */
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { F as PublishedSnapshot, G as SnapshotUploadMetadata, O as InstalledExtensions, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-Dn1jdnFB.js";
1
+ import { A as InstalledExtensions, L as PublishedSnapshot, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, q as SnapshotUploadMetadata, s as AgentPermission, u as ConfigEdit } from "./protocol-DhNrwhqp.js";
2
2
  import { Config, Hookable, KubbHooks } from "@kubb/core";
3
3
  //#region src/connect.d.ts
4
4
  /**
@@ -45,7 +45,7 @@ declare global {
45
45
  * A finished command and what it did, such as `applied 2/3 edits to kubb.config.ts`.
46
46
  */
47
47
  'studio:command': [ctx: {
48
- command: 'generate' | 'saveConfig' | 'readFiles' | 'readInput' | 'readDiff';
48
+ command: 'generate' | 'saveConfig' | 'installExtension' | 'updateExtensions' | 'readFiles' | 'readInput' | 'readDiff';
49
49
  info: string;
50
50
  /**
51
51
  * The Studio user who asked for the command, when Studio names one.
@@ -97,6 +97,8 @@ type ConnectAgentOptions = {
97
97
  * What Studio may do in this project. Every permission is off unless granted.
98
98
  */
99
99
  permissions?: Partial<AgentPermissions>;
100
+ /** The CLI entry `installExtension` runs `kubb add` with. Found through the project's `kubb` or `@kubb/cli` dependency when omitted. */
101
+ kubbBin?: string;
100
102
  /**
101
103
  * Receives the `studio:*` hooks and the `kubb:*` hooks of every generation.
102
104
  */
package/dist/index.js CHANGED
@@ -1,19 +1,25 @@
1
1
  import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { createHash, hash, randomUUID } from "node:crypto";
4
+ import { existsSync } from "node:fs";
4
5
  import { tmpdir } from "node:os";
5
6
  import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
7
  import process$1 from "node:process";
7
8
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
8
- import { pathToFileURL } from "node:url";
9
- import { existsSync } from "node:fs";
9
+ import { x } from "tinyexec";
10
10
  import { mkdir, mkdir as mkdir$1, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
11
- import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, getInputKind, isPackageName, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
11
+ import { Diagnostics, Hookable, cacheStorage, checkoutGitInput, createKubb, fsStorage, getInputKind, isPackageName, isPublicAddress, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
12
12
  import { RpcTarget, newWebSocketRpcSession } from "capnweb";
13
13
  import { detectCodeFormat, parseExpression, parseModule } from "magicast";
14
+ import { pathToFileURL } from "node:url";
14
15
  import { structuredPatch } from "diff";
15
16
  import { stripVTControlCharacters } from "node:util";
16
17
  import { ast } from "@kubb/ast";
18
+ import dns from "node:dns";
19
+ import http from "node:http";
20
+ import https from "node:https";
21
+ import net from "node:net";
22
+ import { Readable, Transform } from "node:stream";
17
23
  import { ofetch } from "ofetch";
18
24
  import { gzipSync } from "node:zlib";
19
25
  //#endregion
@@ -196,7 +202,7 @@ function getElapsedMs(hrStart) {
196
202
  }
197
203
  //#endregion
198
204
  //#region package.json
199
- var version = "5.12.0";
205
+ var version = "5.12.2";
200
206
  //#endregion
201
207
  //#region src/resolveConfig.ts
202
208
  /**
@@ -1842,6 +1848,106 @@ function createGenerationStream(hooks, jobId) {
1842
1848
  };
1843
1849
  }
1844
1850
  //#endregion
1851
+ //#region src/publicFetch.ts
1852
+ const MAX_REDIRECTS = 5;
1853
+ const MAX_BYTES = 52428800;
1854
+ const REDIRECTS = /* @__PURE__ */ new Set([
1855
+ 301,
1856
+ 302,
1857
+ 303,
1858
+ 307,
1859
+ 308
1860
+ ]);
1861
+ const NO_BODY = /* @__PURE__ */ new Set([
1862
+ 204,
1863
+ 205,
1864
+ 304
1865
+ ]);
1866
+ function notPublic(host) {
1867
+ return /* @__PURE__ */ new Error(`${host} is not a public address, so a managed agent does not fetch from it`);
1868
+ }
1869
+ function createLookup(blocked) {
1870
+ return (hostname, options, callback) => {
1871
+ dns.lookup(hostname, {
1872
+ ...options,
1873
+ all: true
1874
+ }, (error, addresses) => {
1875
+ if (error) return callback(error, "", 0);
1876
+ const found = addresses;
1877
+ if (!found.length || found.some(({ address }) => !isPublicAddress(address, blocked))) return callback(notPublic(hostname), "", 0);
1878
+ return options.all ? callback(null, found) : callback(null, found[0].address, found[0].family);
1879
+ });
1880
+ };
1881
+ }
1882
+ function request({ url, blocked, signal }) {
1883
+ if (url.protocol !== "http:" && url.protocol !== "https:") return Promise.reject(/* @__PURE__ */ new Error(`Only http and https URLs are fetched, not ${url.protocol}`));
1884
+ const host = url.hostname.replace(/^\[|\]$/g, "");
1885
+ if (net.isIP(host) && !isPublicAddress(host, blocked)) return Promise.reject(notPublic(host));
1886
+ return new Promise((resolve, reject) => {
1887
+ const req = (url.protocol === "https:" ? https.request : http.request)(url, {
1888
+ method: "GET",
1889
+ lookup: createLookup(blocked),
1890
+ signal: signal ?? void 0,
1891
+ headers: { "accept-encoding": "identity" }
1892
+ }, (res) => {
1893
+ const headers = new Headers();
1894
+ for (const [key, value] of Object.entries(res.headers)) for (const item of [value].flat()) if (item !== void 0) headers.append(key, item);
1895
+ let total = 0;
1896
+ const limit = new Transform({ transform(chunk, _encoding, done) {
1897
+ total += chunk.length;
1898
+ done(total > MAX_BYTES ? /* @__PURE__ */ new Error("The response is larger than 50 MB") : null, chunk);
1899
+ } });
1900
+ res.on("error", (error) => limit.destroy(error));
1901
+ res.pipe(limit);
1902
+ const body = NO_BODY.has(res.statusCode ?? 0) ? null : Readable.toWeb(limit);
1903
+ resolve(new Response(body, {
1904
+ status: res.statusCode,
1905
+ statusText: res.statusMessage,
1906
+ headers
1907
+ }));
1908
+ });
1909
+ req.on("error", reject);
1910
+ req.end();
1911
+ });
1912
+ }
1913
+ /**
1914
+ * Builds a `fetch` that only reaches addresses `blocked` does not list. It follows redirects itself
1915
+ * and checks every hop, and takes GET requests only, since that is all a spec load needs.
1916
+ */
1917
+ function createPublicFetch(blocked) {
1918
+ const publicFetch = async (input, init) => {
1919
+ if (typeof input !== "string" && !(input instanceof URL)) throw new TypeError("A managed agent fetches by URL only");
1920
+ if (init?.method && init.method.toUpperCase() !== "GET") throw new TypeError("A managed agent only sends GET requests");
1921
+ let url = new URL(input);
1922
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
1923
+ const response = await request({
1924
+ url,
1925
+ blocked,
1926
+ signal: init?.signal
1927
+ });
1928
+ const location = response.headers.get("location");
1929
+ if (!REDIRECTS.has(response.status) || !location) return response;
1930
+ await response.body?.cancel();
1931
+ url = new URL(location, url);
1932
+ }
1933
+ throw new Error(`Stopped after ${MAX_REDIRECTS} redirects from ${String(input)}`);
1934
+ };
1935
+ return publicFetch;
1936
+ }
1937
+ /**
1938
+ * Runs `run` with the global `fetch` limited to public addresses, then puts the original back.
1939
+ * A managed agent runs one job at a time, so nothing else shares the window.
1940
+ */
1941
+ async function withPublicFetch(run) {
1942
+ const original = globalThis.fetch;
1943
+ globalThis.fetch = createPublicFetch();
1944
+ try {
1945
+ return await run();
1946
+ } finally {
1947
+ globalThis.fetch = original;
1948
+ }
1949
+ }
1950
+ //#endregion
1845
1951
  //#region src/url.ts
1846
1952
  /**
1847
1953
  * Hosted Kubb Studio URL, used when a host passes none.
@@ -1878,6 +1984,11 @@ const OPEN_TIMEOUT_MS = 5e3;
1878
1984
  const MAX_RETRY_DELAY_MS = 3e4;
1879
1985
  const DISK_SNAPSHOT_MAX_FILES = 1e4;
1880
1986
  const SANDBOX_GENERATION_TTL_MS = 9e5;
1987
+ const MANAGED_INPUT_KINDS = /* @__PURE__ */ new Set([
1988
+ "inline",
1989
+ "url",
1990
+ "git"
1991
+ ]);
1881
1992
  /** Extensions `kubb` ships with, so they count as installed without being a direct dependency. */
1882
1993
  const BUILT_IN_PACKAGES = [
1883
1994
  "@kubb/adapter-oas",
@@ -1903,6 +2014,43 @@ var IncompatibleAgentError = class extends Error {
1903
2014
  this.name = "IncompatibleAgentError";
1904
2015
  }
1905
2016
  };
2017
+ const INSTALL_TIMEOUT_MS = 3e5;
2018
+ /** `owner/slug` or `slug`, as `kubb add` takes it. Checked before it becomes an argument. */
2019
+ const REGISTRY_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)?$/;
2020
+ /** The CLI the project depends on, so `kubb add` installs next to what the config imports. */
2021
+ function findKubbBin(root) {
2022
+ const require = createRequire(path.resolve(root, "package.json"));
2023
+ const names = ["kubb", "@kubb/cli"];
2024
+ return (require.resolve.paths("kubb") ?? []).flatMap((dir) => names.map((name) => path.join(dir, name, "bin", "kubb.js"))).find((bin) => existsSync(bin));
2025
+ }
2026
+ /** Runs a `kubb` command without a shell, and keeps the end of its output for the failure reason. */
2027
+ async function runKubb({ bin, args, cwd, signal }) {
2028
+ try {
2029
+ const { stdout, stderr, exitCode } = await x(process$1.execPath, [bin, ...args], {
2030
+ signal,
2031
+ timeout: INSTALL_TIMEOUT_MS,
2032
+ throwOnError: false,
2033
+ nodeOptions: {
2034
+ cwd,
2035
+ env: {
2036
+ ...process$1.env,
2037
+ NO_COLOR: "1",
2038
+ FORCE_COLOR: "0"
2039
+ }
2040
+ }
2041
+ });
2042
+ const output = `${stdout}\n${stderr}`.trim().split("\n").slice(-20).join("\n");
2043
+ return {
2044
+ failed: exitCode !== 0,
2045
+ output: output || `kubb ${args[0]} exited with ${exitCode ?? "a signal"}`
2046
+ };
2047
+ } catch (error) {
2048
+ return {
2049
+ failed: true,
2050
+ output: `kubb ${args[0]} did not run: ${getErrorMessage(error)}`
2051
+ };
2052
+ }
2053
+ }
1906
2054
  function emit(hooks, name, ...args) {
1907
2055
  return Promise.resolve(hooks.callHook(name, ...args)).catch(() => {});
1908
2056
  }
@@ -1964,12 +2112,17 @@ var Agent = class extends RpcTarget {
1964
2112
  get #sandbox() {
1965
2113
  return this.#session.info?.sandbox === true;
1966
2114
  }
2115
+ get #managed() {
2116
+ return this.#session.info?.managed === true;
2117
+ }
1967
2118
  get #permissions() {
1968
2119
  const { permissions } = this.#options;
1969
2120
  return this.#sandbox ? {
1970
2121
  ...permissions,
1971
2122
  allowWrite: false,
1972
2123
  allowConfigEdit: false,
2124
+ allowInstall: false,
2125
+ allowUpdate: false,
1973
2126
  allowRead: true,
1974
2127
  allowReadAst: true
1975
2128
  } : permissions;
@@ -2121,6 +2274,8 @@ var Agent = class extends RpcTarget {
2121
2274
  message: "Connection is closed"
2122
2275
  });
2123
2276
  if (this.#session.active) return this.#refuse({ message: AgentMessage.GENERATION_IN_PROGRESS });
2277
+ if (this.#session.install) return this.#refuse({ message: AgentMessage.INSTALL_IN_PROGRESS });
2278
+ if (this.#session.saving) return this.#refuse({ message: AgentMessage.SAVE_IN_PROGRESS });
2124
2279
  const settled = Promise.withResolvers();
2125
2280
  this.#session.active = {
2126
2281
  jobId: input.jobId,
@@ -2159,8 +2314,9 @@ var Agent = class extends RpcTarget {
2159
2314
  */
2160
2315
  async #generate({ input, root, outputRoot, signal }) {
2161
2316
  const { jobId, config: patch } = input;
2162
- const studioInput = this.#sandbox ? patch?.input : void 0;
2317
+ const studioInput = this.#sandbox || this.#managed ? patch?.input : void 0;
2163
2318
  if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || getInputKind(studioInput) !== "inline")) return this.#refuse({ message: AgentMessage.SANDBOX_INPUT_ONLY });
2319
+ if (this.#managed && studioInput !== void 0 && !MANAGED_INPUT_KINDS.has(getInputKind(studioInput))) return this.#refuse({ message: AgentMessage.MANAGED_INPUT_KIND });
2164
2320
  const [configs, extensions] = await Promise.all([this.#options.loadConfigs(), this.#installedExtensions()]);
2165
2321
  const config = pickConfig(configs, patch?.config);
2166
2322
  if (!config) return this.#refuse({ message: AgentMessage.CONFIG_ENTRY_NOT_FOUND });
@@ -2189,12 +2345,13 @@ var Agent = class extends RpcTarget {
2189
2345
  throw error;
2190
2346
  });
2191
2347
  const activePlugins = plugins ?? config.plugins;
2192
- if (patch?.input && !this.#sandbox) await emit(this.#options.hooks, "kubb:warn", { message: "Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox agent" });
2193
- const files = await generate({
2348
+ if (patch?.input && !this.#sandbox && !this.#managed) await emit(this.#options.hooks, "kubb:warn", { message: "Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox or managed agent" });
2349
+ const checkout = this.#managed && studioInput !== void 0 && getInputKind(studioInput) === "git" ? await checkoutGitInput(studioInput, signal, { publicOnly: true }).catch((error) => this.#refuse({ message: getErrorMessage(error) })) : void 0;
2350
+ const run = () => generate({
2194
2351
  config: {
2195
2352
  ...config,
2196
2353
  root,
2197
- input: studioInput ?? config.input,
2354
+ input: checkout?.path ?? studioInput ?? config.input,
2198
2355
  storage: outputRoot ? stagingStorage({
2199
2356
  root,
2200
2357
  directory: outputRoot
@@ -2206,11 +2363,12 @@ var Agent = class extends RpcTarget {
2206
2363
  hooks: this.#options.hooks,
2207
2364
  signal,
2208
2365
  allowExec: this.#permissions.allowWrite && this.#permissions.allowExec
2209
- }).catch(async (error) => {
2366
+ });
2367
+ const files = await (this.#managed ? withPublicFetch(run) : run()).catch(async (error) => {
2210
2368
  await this.#generations.drop(jobId);
2211
2369
  await this.#logCommand("generate", getErrorMessage(error), input.requestedBy, true);
2212
2370
  throw error;
2213
- });
2371
+ }).finally(() => checkout?.dispose());
2214
2372
  const output = await this.#generations.keep({
2215
2373
  jobId,
2216
2374
  files,
@@ -2258,7 +2416,9 @@ var Agent = class extends RpcTarget {
2258
2416
  await this.#warn({ message: "Ignored save: editing kubb.config.ts was not granted" });
2259
2417
  return refuse("the agent was not granted permission to edit kubb.config.ts");
2260
2418
  }
2261
- if (this.#session.active) return refuse("a generation is in progress");
2419
+ const busy = this.#busy();
2420
+ if (busy) return refuse(busy);
2421
+ this.#session.saving = true;
2262
2422
  try {
2263
2423
  const { outcomes, changed } = await writeConfigEdits({
2264
2424
  filePath: configFile,
@@ -2274,6 +2434,110 @@ var Agent = class extends RpcTarget {
2274
2434
  } catch (error) {
2275
2435
  await emit(this.#options.hooks, "studio:error", { error: toError(error) });
2276
2436
  return refuse(getErrorMessage(error));
2437
+ } finally {
2438
+ this.#session.saving = false;
2439
+ }
2440
+ }
2441
+ /**
2442
+ * Installs a registry extension into the project with `kubb add <id> --yes`, which also adds a
2443
+ * plugin to `kubb.config.ts`. Studio calls `connect()` again afterwards to see the result.
2444
+ */
2445
+ async installExtension(input) {
2446
+ this.#assertConnected();
2447
+ const { root, configFile, kubbBin = findKubbBin(root) } = this.#options;
2448
+ const refuse = async (reason, permission) => {
2449
+ await this.#warn({
2450
+ message: `Ignored install: ${reason}`,
2451
+ permission
2452
+ });
2453
+ return {
2454
+ installed: false,
2455
+ reason
2456
+ };
2457
+ };
2458
+ if (typeof input?.id !== "string" || !REGISTRY_ID.test(input.id)) return refuse("the message carried no registry id");
2459
+ if (!this.#permissions.allowInstall) return refuse("installing extensions was not granted", AgentPermission.INSTALL);
2460
+ if (!this.#permissions.allowConfigEdit) return refuse("editing kubb.config.ts was not granted", AgentPermission.CONFIG_EDIT);
2461
+ const busy = this.#busy();
2462
+ if (busy) return refuse(busy);
2463
+ if (!kubbBin) return refuse("kubb is not installed in the project");
2464
+ const { failed, output } = await this.#runKubb({
2465
+ bin: kubbBin,
2466
+ args: [
2467
+ "add",
2468
+ input.id,
2469
+ "--yes",
2470
+ "--config",
2471
+ configFile
2472
+ ]
2473
+ });
2474
+ await this.#logCommand("installExtension", `${failed ? "failed to install" : "installed"} ${input.id}`, input.requestedBy, failed);
2475
+ return failed ? {
2476
+ installed: false,
2477
+ reason: output
2478
+ } : { installed: true };
2479
+ }
2480
+ /**
2481
+ * Moves installed extensions to the registry's version with `kubb update [ids] --yes`, which
2482
+ * installs packages and leaves `kubb.config.ts` alone. Studio calls `connect()` again afterwards.
2483
+ */
2484
+ async updateExtensions(input) {
2485
+ this.#assertConnected();
2486
+ const { root, kubbBin = findKubbBin(root) } = this.#options;
2487
+ const refuse = async (reason, permission) => {
2488
+ await this.#warn({
2489
+ message: `Ignored update: ${reason}`,
2490
+ permission
2491
+ });
2492
+ return {
2493
+ updated: false,
2494
+ reason
2495
+ };
2496
+ };
2497
+ const ids = input?.ids ?? [];
2498
+ if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && REGISTRY_ID.test(id))) return refuse("the message carried an invalid registry id");
2499
+ if (!this.#permissions.allowUpdate) return refuse("updating extensions was not granted", AgentPermission.UPDATE);
2500
+ const busy = this.#busy();
2501
+ if (busy) return refuse(busy);
2502
+ if (!kubbBin) return refuse("kubb is not installed in the project");
2503
+ const { failed, output } = await this.#runKubb({
2504
+ bin: kubbBin,
2505
+ args: [
2506
+ "update",
2507
+ ...ids,
2508
+ "--yes"
2509
+ ]
2510
+ });
2511
+ await this.#logCommand("updateExtensions", `${failed ? "failed to update" : "updated"} ${ids.join(", ") || "all extensions"}`, input?.requestedBy, failed);
2512
+ return failed ? {
2513
+ updated: false,
2514
+ reason: output
2515
+ } : { updated: true };
2516
+ }
2517
+ /** Why a command that touches the project cannot start right now. */
2518
+ #busy() {
2519
+ if (this.#session.active) return "a generation is in progress";
2520
+ if (this.#session.install) return "an install or update is in progress";
2521
+ if (this.#session.saving) return "a save is in progress";
2522
+ }
2523
+ /** Runs `kubb` in the project while holding off saves and generations, and ends it when the socket closes. */
2524
+ async #runKubb({ bin, args }) {
2525
+ const controller = new AbortController();
2526
+ const settled = Promise.withResolvers();
2527
+ this.#session.install = {
2528
+ abort: () => controller.abort(),
2529
+ settled: settled.promise
2530
+ };
2531
+ try {
2532
+ return await runKubb({
2533
+ bin,
2534
+ args,
2535
+ cwd: this.#options.root,
2536
+ signal: controller.signal
2537
+ });
2538
+ } finally {
2539
+ this.#session.install = void 0;
2540
+ settled.resolve();
2277
2541
  }
2278
2542
  }
2279
2543
  /**
@@ -2458,6 +2722,8 @@ async function runSession({ options, openRpc, url, instanceId }) {
2458
2722
  await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
2459
2723
  }
2460
2724
  await active?.settled;
2725
+ session.install?.abort();
2726
+ await session.install?.settled;
2461
2727
  await agent.clear();
2462
2728
  return {
2463
2729
  ...close,
@@ -2526,6 +2792,8 @@ async function runAgent({ options, openRpc }) {
2526
2792
  permissions: {
2527
2793
  allowWrite: false,
2528
2794
  allowConfigEdit: false,
2795
+ allowInstall: false,
2796
+ allowUpdate: false,
2529
2797
  allowExec: false,
2530
2798
  allowRead: false,
2531
2799
  allowReadAst: false,