@swmansion/argent 0.31.1-next.5 → 0.31.1-next.7

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/client.js CHANGED
@@ -7794,6 +7794,10 @@ var FLAG_REGISTRY = [
7794
7794
  name: "harmonyos",
7795
7795
  description: "HarmonyOS support: list, boot and drive HarmonyOS phones and DevEco Studio emulators over `hdc`. Off by default while the platform is experimental."
7796
7796
  },
7797
+ {
7798
+ name: "forward-secrets",
7799
+ description: "Over `argent link` or ARGENT_TOOLS_URL, resolve each `{{secret:NAME}}` a call names on this machine and send the value with the call, so a remote tool-server can type a secret it does not hold. The linked tool-server then receives every secret of this machine that a call names, so link only one you trust with all of them. Values go only over https:// with the certificate checked, or to a loopback address. Off by default."
7800
+ },
7797
7801
  {
7798
7802
  name: "video-watermark",
7799
7803
  description: "Overlay the argent corner watermark on recorded screen videos. On by default; turn it off with `argent disable video-watermark`.",
@@ -7936,6 +7940,108 @@ function getAtPath(obj, dottedKey) {
7936
7940
  var import_dotenv = __toESM(require_dist(), 1);
7937
7941
  import * as fs3 from "node:fs";
7938
7942
  import * as path4 from "node:path";
7943
+ var SECRET_ENV_PREFIX = "ARGENT_SECRET_";
7944
+ var SECRETS_FILE_NAME = "secrets.env";
7945
+ var SHARED_ENV_FILES = [".env.local", ".env"];
7946
+ var SECRET_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
7947
+ function envSource(env) {
7948
+ const values = /* @__PURE__ */ new Map();
7949
+ for (const [key, value] of Object.entries(env)) {
7950
+ if (value === void 0 || !key.startsWith(SECRET_ENV_PREFIX)) continue;
7951
+ const name = key.slice(SECRET_ENV_PREFIX.length);
7952
+ if (SECRET_NAME_RE.test(name)) values.set(name, value);
7953
+ }
7954
+ return {
7955
+ label: `environment (${SECRET_ENV_PREFIX}*)`,
7956
+ kind: "env",
7957
+ present: true,
7958
+ names: [...values.keys()].sort(),
7959
+ values
7960
+ };
7961
+ }
7962
+ function fileSource(filePath, exposure) {
7963
+ const label = filePath;
7964
+ const absent = {
7965
+ label,
7966
+ kind: "file",
7967
+ present: false,
7968
+ names: [],
7969
+ values: /* @__PURE__ */ new Map()
7970
+ };
7971
+ if (!fs3.statSync(filePath, { throwIfNoEntry: false })) return absent;
7972
+ let content;
7973
+ try {
7974
+ content = fs3.readFileSync(filePath, "utf8");
7975
+ } catch {
7976
+ return absent;
7977
+ }
7978
+ const values = /* @__PURE__ */ new Map();
7979
+ let skippedUnprefixed = false;
7980
+ for (const [key, value] of Object.entries((0, import_dotenv.parse)(content))) {
7981
+ const prefixed = key.startsWith(SECRET_ENV_PREFIX);
7982
+ if (exposure === "shared" && !prefixed) {
7983
+ skippedUnprefixed = true;
7984
+ continue;
7985
+ }
7986
+ const name = prefixed ? key.slice(SECRET_ENV_PREFIX.length) : key;
7987
+ if (SECRET_NAME_RE.test(name)) values.set(name, value);
7988
+ }
7989
+ return {
7990
+ label,
7991
+ kind: "file",
7992
+ present: true,
7993
+ names: [...values.keys()].sort(),
7994
+ values,
7995
+ ...values.size === 0 && skippedUnprefixed ? { needsPrefix: true } : {}
7996
+ };
7997
+ }
7998
+ function secretSources(options = {}) {
7999
+ const sources = [envSource(options.env ?? process.env)];
8000
+ const projectRoot = findProjectRoot(options.cwd ?? process.cwd());
8001
+ const filePaths = [];
8002
+ if (projectRoot) {
8003
+ filePaths.push({
8004
+ path: path4.join(projectRoot, ".argent", SECRETS_FILE_NAME),
8005
+ exposure: "dedicated"
8006
+ });
8007
+ for (const name of SHARED_ENV_FILES) {
8008
+ filePaths.push({ path: path4.join(projectRoot, name), exposure: "shared" });
8009
+ }
8010
+ }
8011
+ filePaths.push({
8012
+ path: path4.join(resolveHomeDir(options), ".argent", SECRETS_FILE_NAME),
8013
+ exposure: "dedicated"
8014
+ });
8015
+ const seen = /* @__PURE__ */ new Set();
8016
+ for (const entry of filePaths) {
8017
+ const resolved = path4.resolve(entry.path);
8018
+ if (seen.has(resolved)) continue;
8019
+ seen.add(resolved);
8020
+ sources.push(fileSource(resolved, entry.exposure));
8021
+ }
8022
+ return sources;
8023
+ }
8024
+ function encodeClientSecrets(values) {
8025
+ return Buffer.from(JSON.stringify(Object.fromEntries(values)), "utf8").toString("base64");
8026
+ }
8027
+ function lookupSecret(name, sources) {
8028
+ for (const source of sources) {
8029
+ const value = source.values.get(name);
8030
+ if (value !== void 0) return value;
8031
+ }
8032
+ return void 0;
8033
+ }
8034
+ var SECRET_PLACEHOLDER_RE = /\{\{secret:([A-Za-z_][A-Za-z0-9_]*)\}\}/g;
8035
+ function placeholderNames(text) {
8036
+ return Array.from(text.matchAll(SECRET_PLACEHOLDER_RE), (match) => match[1]);
8037
+ }
8038
+ var REDUNDANT_PREFIX_RE = /^argent_secret_/i;
8039
+ function lookupPlaceholderSecret(rawName, sources) {
8040
+ const value = lookupSecret(rawName, sources);
8041
+ if (value !== void 0 || !REDUNDANT_PREFIX_RE.test(rawName)) return { name: rawName, value };
8042
+ const name = rawName.replace(REDUNDANT_PREFIX_RE, "");
8043
+ return { name, value: lookupSecret(name, sources) };
8044
+ }
7939
8045
 
7940
8046
  // ../configuration-core/src/merge.ts
7941
8047
  function mergeRestrictive(local, global) {
@@ -8650,11 +8756,15 @@ async function ensureToolsServer(paths) {
8650
8756
  }
8651
8757
 
8652
8758
  // ../argent-tools-client/src/tools-client.ts
8759
+ var import_yaml2 = __toESM(require_dist2(), 1);
8760
+ import { readFile as readFile5 } from "node:fs/promises";
8761
+ import { maxHeaderSize } from "node:http";
8653
8762
  import * as path12 from "node:path";
8654
8763
 
8655
8764
  // ../registry/src/types.ts
8656
8765
  var LINKED_CALL_HEADER = "x-argent-linked";
8657
8766
  var E2E_DIR_CLIENT_HEADER = "x-argent-e2e-dir";
8767
+ var CLIENT_SECRETS_HEADER = "x-argent-client-secrets";
8658
8768
 
8659
8769
  // ../registry/src/artifacts.ts
8660
8770
  import { stat } from "node:fs/promises";
@@ -10307,6 +10417,67 @@ function carriesMembers(args) {
10307
10417
  function carriesUpload(args) {
10308
10418
  return carriesMembers(args) || wiresOf(args).some((wire) => typeof wire.uploadId === "string");
10309
10419
  }
10420
+ async function forwardedSecrets(args) {
10421
+ if (!isFeatureEnabled("forward-secrets")) return void 0;
10422
+ const names = /* @__PURE__ */ new Set();
10423
+ const scan = (text) => {
10424
+ for (const name of placeholderNames(text)) names.add(name);
10425
+ };
10426
+ const seen = /* @__PURE__ */ new WeakSet();
10427
+ const visit = async (value) => {
10428
+ if (typeof value === "string") return scan(value);
10429
+ if (typeof value !== "object" || value === null || seen.has(value)) return;
10430
+ seen.add(value);
10431
+ const wire = value;
10432
+ if (wire[FILE_INPUT_MARKER2] !== true) {
10433
+ for (const nested of Object.values(value)) await visit(nested);
10434
+ return;
10435
+ }
10436
+ if (!Array.isArray(wire.members)) return;
10437
+ const yaml = [
10438
+ .../\.ya?ml$/i.test(wire.path ?? "") ? [wire] : [],
10439
+ ...wire.members.filter((member) => member.role === "flow")
10440
+ ];
10441
+ for (const file of yaml) {
10442
+ let text;
10443
+ if (typeof file.content === "string") {
10444
+ text = Buffer.from(file.content, "base64").toString("utf8");
10445
+ } else if (file.uploadId !== void 0 && file.canonical !== void 0) {
10446
+ text = await readFile5(file.canonical, "utf8").catch(() => void 0);
10447
+ }
10448
+ let parsed;
10449
+ try {
10450
+ parsed = text === void 0 ? void 0 : (0, import_yaml2.parse)(text.trim(), { logLevel: "error" });
10451
+ } catch {
10452
+ continue;
10453
+ }
10454
+ await visit(parsed);
10455
+ }
10456
+ };
10457
+ await visit(args);
10458
+ if (names.size === 0) return void 0;
10459
+ const sources = secretSources();
10460
+ const values = /* @__PURE__ */ new Map();
10461
+ for (const rawName of names) {
10462
+ const { name, value } = lookupPlaceholderSecret(rawName, sources);
10463
+ if (value !== void 0) values.set(name, value);
10464
+ }
10465
+ return values;
10466
+ }
10467
+ function secretsTransportRefusal(url) {
10468
+ if (!URL.canParse(url)) return `${url} is not a URL`;
10469
+ const { protocol, hostname } = new URL(url);
10470
+ const host = hostname.replace(/^\[|\]$/g, "");
10471
+ if (["127.0.0.1", "localhost", "::1"].includes(host)) return void 0;
10472
+ if (protocol !== "https:") {
10473
+ return `${url} is reached neither over https:// nor at a loopback address. Link the tool-server through an https:// URL, or through an SSH tunnel to a 127.0.0.1 port`;
10474
+ }
10475
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") {
10476
+ return `NODE_TLS_REJECT_UNAUTHORIZED=0 turns off the certificate check of ${url}. Unset it, and trust a self-signed certificate through NODE_EXTRA_CA_CERTS instead`;
10477
+ }
10478
+ return void 0;
10479
+ }
10480
+ var FETCH_HEAD_BYTES = 256;
10310
10481
  function createToolsClient(options = {}) {
10311
10482
  let cached = null;
10312
10483
  const doFetch = options.fetchImpl ?? ((url, init) => fetch(url, init));
@@ -10337,7 +10508,7 @@ function createToolsClient(options = {}) {
10337
10508
  const { url, token } = await baseUrl();
10338
10509
  const res = await doFetch(
10339
10510
  `${url}/tools`,
10340
- { headers: authHeaders2(token), signal: opts?.signal },
10511
+ { headers: { ...options.headers?.(), ...authHeaders2(token) }, signal: opts?.signal },
10341
10512
  { longRunning: false, carriesUpload: false }
10342
10513
  );
10343
10514
  if (!res.ok) throw new Error(`GET /tools failed: ${res.status} ${res.statusText}`);
@@ -10395,25 +10566,51 @@ function createToolsClient(options = {}) {
10395
10566
  };
10396
10567
  const stream = opts?.onProgress !== void 0 || carriesMembers(finalArgs);
10397
10568
  const sentOnce = carriesUpload(finalArgs);
10569
+ const forwarded = remote ? await forwardedSecrets(finalArgs) : void 0;
10570
+ const encodedSecrets = forwarded ? encodeClientSecrets(forwarded) : void 0;
10571
+ const clientSecrets = forwarded?.size ? forwarded : void 0;
10572
+ const refusal = clientSecrets ? secretsTransportRefusal(url) : void 0;
10573
+ if (refusal !== void 0) {
10574
+ throw new Error(
10575
+ `forward-secrets sends secret values only over https:// with the certificate checked, or to a loopback address, so the call was not sent (it names ${[...clientSecrets.keys()].join(", ")}): ${refusal}.`
10576
+ );
10577
+ }
10578
+ const toolUrl = `${url}/tools/${encodeURIComponent(meta?.name ?? name)}`;
10579
+ const headers = {
10580
+ ...options.headers?.(),
10581
+ "Content-Type": "application/json",
10582
+ // Tells the tool-server the call came over a link, a link to
10583
+ // 127.0.0.1 included, which nothing else in the request shows.
10584
+ ...remote ? { [LINKED_CALL_HEADER]: "1" } : {},
10585
+ // Tells the tool-server this client writes recordings under
10586
+ // `.argent/e2e/`; an older client writes only `.argent/flows/`.
10587
+ [E2E_DIR_CLIENT_HEADER]: "1",
10588
+ ...encodedSecrets ? { [CLIENT_SECRETS_HEADER]: encodedSecrets } : {},
10589
+ // A proxy that compresses the stream holds each line until its buffer
10590
+ // fills. `identity` keeps the stream uncompressed end to end.
10591
+ ...stream ? { "Accept": "application/x-ndjson", "Accept-Encoding": "identity" } : {},
10592
+ ...authHeaders2(token)
10593
+ };
10594
+ if (clientSecrets !== void 0 && encodedSecrets !== void 0) {
10595
+ const headBytes = toolUrl.length + FETCH_HEAD_BYTES + Object.entries(headers).reduce(
10596
+ (sum, [key, value]) => sum + key.length + value.length + 4,
10597
+ 0
10598
+ );
10599
+ if (headBytes > maxHeaderSize) {
10600
+ throw new Error(
10601
+ `forward-secrets would send ${[...clientSecrets.keys()].join(", ")} in an ${CLIENT_SECRETS_HEADER} header of ${encodedSecrets.length} bytes, which takes the request headers past the ${maxHeaderSize} bytes a tool-server accepts, so the call was not sent. Define a secret this large on the machine running the tool-server instead.`
10602
+ );
10603
+ }
10604
+ }
10398
10605
  const res = await doFetch(
10399
- `${url}/tools/${encodeURIComponent(meta?.name ?? name)}`,
10606
+ toolUrl,
10400
10607
  {
10401
10608
  method: "POST",
10402
- headers: {
10403
- "Content-Type": "application/json",
10404
- // Tells the tool-server the call came over a link, a link to
10405
- // 127.0.0.1 included, which nothing else in the request shows.
10406
- ...remote ? { [LINKED_CALL_HEADER]: "1" } : {},
10407
- // Tells the tool-server this client writes recordings under
10408
- // `.argent/e2e/`; an older client writes only `.argent/flows/`.
10409
- [E2E_DIR_CLIENT_HEADER]: "1",
10410
- // A proxy that compresses the stream holds each line until its buffer
10411
- // fills. `identity` keeps the stream uncompressed end to end.
10412
- ...stream ? { "Accept": "application/x-ndjson", "Accept-Encoding": "identity" } : {},
10413
- ...authHeaders2(token)
10414
- },
10609
+ headers,
10415
10610
  body: JSON.stringify(finalArgs ?? {}),
10416
- signal: opts?.signal
10611
+ signal: opts?.signal,
10612
+ // A followed redirect would re-send the values to wherever it points.
10613
+ ...clientSecrets ? { redirect: "manual" } : {}
10417
10614
  },
10418
10615
  { longRunning: meta?.longRunning === true, carriesUpload: sentOnce }
10419
10616
  ).catch((err) => {
@@ -10451,6 +10648,17 @@ function createToolsClient(options = {}) {
10451
10648
  }
10452
10649
  json = {};
10453
10650
  }
10651
+ if (clientSecrets && res.status >= 300 && res.status < 400) {
10652
+ throw new ToolInvocationError(
10653
+ `The tool-server answered ${res.status} ${res.statusText} to ${name}. forward-secrets does not follow a redirect, so the call was not sent on; link the URL it points to instead.`
10654
+ );
10655
+ }
10656
+ const headersRefused = res.status === 431 || res.status === 400 && errorBodyMessage(json) === void 0;
10657
+ if (clientSecrets && headersRefused) {
10658
+ throw new ToolInvocationError(
10659
+ `The tool-server, or a proxy in front of it, refused the request headers (${res.status}), as it does when they are too large. The ${CLIENT_SECRETS_HEADER} header carried ${[...clientSecrets.keys()].join(", ")}; define a secret this large on the machine running the tool-server instead.`
10660
+ );
10661
+ }
10454
10662
  if (!res.ok) {
10455
10663
  throw new ToolInvocationError(errorBodyMessage(json) ?? `${res.status} ${res.statusText}`, {
10456
10664
  errorCode: json.error_code,
@@ -10470,7 +10678,7 @@ function createToolsClient(options = {}) {
10470
10678
  }
10471
10679
 
10472
10680
  // ../argent-tools-client/src/artifacts.ts
10473
- import { copyFile, mkdir as mkdir4, readFile as readFile5, realpath as realpath4, rm as rm3, stat as stat5, writeFile as writeFile4 } from "node:fs/promises";
10681
+ import { copyFile, mkdir as mkdir4, readFile as readFile6, realpath as realpath4, rm as rm3, stat as stat5, writeFile as writeFile4 } from "node:fs/promises";
10474
10682
  import { constants as fsConstants } from "node:fs";
10475
10683
  import { tmpdir as tmpdir2 } from "node:os";
10476
10684
  import { basename as basename7, dirname as dirname9, extname as extname2, isAbsolute as isAbsolute6, join as join13, normalize as normalize2, resolve as resolve8, sep as sep4 } from "node:path";
@@ -10659,7 +10867,7 @@ async function materializeArtifacts(result, ctx) {
10659
10867
  if (value.mimeType.startsWith("image/")) {
10660
10868
  images.push({
10661
10869
  localPath: finalPath2,
10662
- data: await readFile5(finalPath2),
10870
+ data: await readFile6(finalPath2),
10663
10871
  mimeType: value.mimeType
10664
10872
  });
10665
10873
  }
@@ -10691,7 +10899,7 @@ async function materializeArtifacts(result, ctx) {
10691
10899
  }
10692
10900
  if (localPath) {
10693
10901
  if (value.mimeType.startsWith("image/")) {
10694
- images.push({ localPath, data: await readFile5(localPath), mimeType: value.mimeType });
10902
+ images.push({ localPath, data: await readFile6(localPath), mimeType: value.mimeType });
10695
10903
  }
10696
10904
  return localPath;
10697
10905
  }
@@ -16912,7 +16912,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
16912
16912
  var SESSION_ID = randomUUID4();
16913
16913
  function readCliVersion() {
16914
16914
  if (true) {
16915
- return "0.31.1-next.5";
16915
+ return "0.31.1-next.7";
16916
16916
  }
16917
16917
  return "0.0.0";
16918
16918
  }
@@ -23739,6 +23739,9 @@ function couldBeOurToolServer(pid, marker) {
23739
23739
  }
23740
23740
 
23741
23741
  // ../argent-tools-client/src/tools-client.ts
23742
+ var import_yaml4 = __toESM(require_dist2(), 1);
23743
+ import { readFile as readFile5 } from "node:fs/promises";
23744
+ import { maxHeaderSize } from "node:http";
23742
23745
  import * as path25 from "node:path";
23743
23746
 
23744
23747
  // ../argent-tools-client/src/link-config.ts
@@ -23782,7 +23785,7 @@ import * as path24 from "node:path";
23782
23785
  var INLINE_MEMBERS_BYTES = 256 * 1024;
23783
23786
 
23784
23787
  // ../argent-tools-client/src/artifacts.ts
23785
- import { copyFile, mkdir as mkdir4, readFile as readFile5, realpath as realpath4, rm as rm3, stat as stat5, writeFile as writeFile4 } from "node:fs/promises";
23788
+ import { copyFile, mkdir as mkdir4, readFile as readFile6, realpath as realpath4, rm as rm3, stat as stat5, writeFile as writeFile4 } from "node:fs/promises";
23786
23789
  import { constants as fsConstants } from "node:fs";
23787
23790
  import { tmpdir as tmpdir2 } from "node:os";
23788
23791
  import { basename as basename7, dirname as dirname17, extname as extname2, isAbsolute as isAbsolute8, join as join26, normalize as normalize2, resolve as resolve15, sep as sep6 } from "node:path";
@@ -24326,7 +24329,7 @@ async function update(args) {
24326
24329
 
24327
24330
  // ../argent-installer/src/uninstall.ts
24328
24331
  var import_picocolors12 = __toESM(require_picocolors(), 1);
24329
- var import_yaml4 = __toESM(require_dist2(), 1);
24332
+ var import_yaml5 = __toESM(require_dist2(), 1);
24330
24333
  import * as fs21 from "node:fs";
24331
24334
  import * as path27 from "node:path";
24332
24335
  var UNINSTALL_TOOLSERVER_STOP_FAILED = {
@@ -24425,7 +24428,7 @@ function readBundledSkillName(skillFilePath, fallbackName) {
24425
24428
  const content = fs21.readFileSync(skillFilePath, "utf8");
24426
24429
  const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
24427
24430
  if (!frontmatter) return fallbackName;
24428
- const data = (0, import_yaml4.parse)(frontmatter);
24431
+ const data = (0, import_yaml5.parse)(frontmatter);
24429
24432
  const name = data?.name;
24430
24433
  return typeof name === "string" && name.trim() ? name.trim() : fallbackName;
24431
24434
  } catch {