@lifeaitools/clauth 2.0.1 → 2.0.3

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.
@@ -167,7 +167,14 @@ function normalizeCommand(command, field) {
167
167
  const [cmd, ...args] = command;
168
168
  if (typeof cmd !== "string" || !cmd.trim()) throw new Error(`${field}[0] is required`);
169
169
  if (/[;&|<>]/.test(cmd)) throw new Error(`${field}[0] must be an executable path/name, not shell syntax`);
170
- const normalizedArgs = args.map(String);
170
+ // Expand the same path tokens `cwd` already gets. Until now expandPathToken()
171
+ // was applied ONLY to cwd, so a manifest naming a root inside a command
172
+ // argument shipped the literal string to the shell — dev-center's
173
+ // "$LIFEAI_ENV/services/restart-dev-center.ps1" reached pwsh unexpanded and
174
+ // exited 64. Expanding here, AFTER the shell-syntax checks on cmd and BEFORE
175
+ // the per-arg checks below, keeps the injection guard authoritative over the
176
+ // final value rather than the pre-expansion one.
177
+ const normalizedArgs = args.map((a) => expandPathToken(String(a)) ?? String(a));
171
178
  // shell:true (Windows-only, see execute() in runSurfaceAction) hands each
172
179
  // arg to cmd.exe verbatim — a shell metacharacter in an arg is exactly as
173
180
  // exploitable as one in cmd[0]. A legitimate CLI arg for the pm2/node
@@ -192,13 +199,24 @@ export function shellQuote(value, useShell) {
192
199
  function expandPathToken(value) {
193
200
  if (!value) return null;
194
201
  const root = process.env.REGEN_ROOT || process.env.LIFEAI_REPO_ROOT || "C:/Dev/regen-root";
202
+ // LIFEAI_ENV is the environment-harness checkout. A manifest that drives a
203
+ // service through a harness script (dev-center calls
204
+ // $LIFEAI_ENV/services/restart-dev-center.ps1) had no way to name it, so the
205
+ // literal token reached the shell and pwsh answered
206
+ // "not recognized as the name of a script file" with exit 64 — a restart that
207
+ // fails while the service stays up, which reads as a flaky action rather than
208
+ // an unresolved path.
209
+ const envRoot = process.env.LIFEAI_ENV || "C:/Dev/lifeai-env";
195
210
  return String(value)
196
211
  .replace(/\$\{REGEN_ROOT\}/g, root)
197
212
  .replace(/\$REGEN_ROOT/g, root)
198
213
  .replace(/%REGEN_ROOT%/gi, root)
199
214
  .replace(/\$\{LIFEAI_REPO_ROOT\}/g, root)
200
215
  .replace(/\$LIFEAI_REPO_ROOT/g, root)
201
- .replace(/%LIFEAI_REPO_ROOT%/gi, root);
216
+ .replace(/%LIFEAI_REPO_ROOT%/gi, root)
217
+ .replace(/\$\{LIFEAI_ENV\}/g, envRoot)
218
+ .replace(/\$LIFEAI_ENV/g, envRoot)
219
+ .replace(/%LIFEAI_ENV%/gi, envRoot);
202
220
  }
203
221
 
204
222
  function normalizeLifecycleOwner(owner) {
@@ -482,6 +500,23 @@ export function registerPlugin(manifestPath, actor = "localhost") {
482
500
  ok: false, state: "manifest_unreadable", error: error instanceof Error ? error.message : String(error),
483
501
  }, actor);
484
502
  }
503
+ // ${PACKAGE_ROOT} — "wherever this manifest actually landed" — resolved HERE
504
+ // and baked into the stored copy, which is the only point at which the true
505
+ // origin is still known. registerPlugin copies the manifest into the managed
506
+ // root, so by the time discoverPlugins() re-reads it, sourcePath is the
507
+ // managed directory and the package's real install location is gone. An npm
508
+ // package installs at node_modules/@scope/name/, a path nothing can hardcode.
509
+ //
510
+ // Without this a shipped manifest had to name an absolute path: rdc-skills
511
+ // carried cwd "C:/Dev/rdc-skills" with core+enable_default, so `npm i -g` on
512
+ // any other machine auto-enabled a CORE plugin pointing at a directory that
513
+ // does not exist there. The token, not the package, was the defect — the
514
+ // correct value was simply inexpressible.
515
+ const packageRoot = path.dirname(path.resolve(manifestPath)).replaceAll("\\", "/");
516
+ raw = raw
517
+ .replaceAll("${PACKAGE_ROOT}", packageRoot)
518
+ .replaceAll("$PACKAGE_ROOT", packageRoot)
519
+ .replace(/%PACKAGE_ROOT%/gi, packageRoot);
485
520
  let manifest;
486
521
  try {
487
522
  manifest = validatePluginManifest(JSON.parse(raw), manifestPath);
@@ -887,3 +887,78 @@ test("tunnel route add and remove produce reversible operation receipts", () =>
887
887
  assert.equal(removed.resulting_state.ok, true);
888
888
  assert.equal(removed.resulting_state.state, "route_removed");
889
889
  }));
890
+
891
+ test("command arguments expand path tokens, not just cwd", () => {
892
+ // Regression: expandPathToken() was applied ONLY to `cwd`, so a manifest that
893
+ // named a root inside a COMMAND ARGUMENT shipped the literal string to the
894
+ // shell. dev-center's restart is
895
+ // ["pwsh","-NoProfile","-File","$LIFEAI_ENV/services/restart-dev-center.ps1"]
896
+ // and pwsh answered "not recognized as the name of a script file" with exit
897
+ // 64 -- a restart that fails while the service stays up, which reads as a
898
+ // flaky action rather than an unresolved path.
899
+ const oldEnv = process.env.LIFEAI_ENV;
900
+ const oldRoot = process.env.REGEN_ROOT;
901
+ process.env.LIFEAI_ENV = "C:/tmp/env-root";
902
+ process.env.REGEN_ROOT = "C:/tmp/regen-root";
903
+ try {
904
+ const plugin = validatePluginManifest(baseManifest("token-expansion", {
905
+ surfaces: [{
906
+ id: "primary",
907
+ destination: "local/clauth/pm2",
908
+ lifecycle_owner: "clauth",
909
+ port: 3003,
910
+ health: "/health",
911
+ restart: ["pwsh", "-NoProfile", "-File", "$LIFEAI_ENV/services/restart-dev-center.ps1"],
912
+ start: ["node", "${REGEN_ROOT}/scripts/start.mjs"],
913
+ }],
914
+ }), "clauth-plugin.json");
915
+ const s = plugin.surfaces[0];
916
+ assert.ok(!s.restart.some((a) => a.includes("$LIFEAI_ENV")),
917
+ `LIFEAI_ENV left unexpanded: ${JSON.stringify(s.restart)}`);
918
+ assert.ok(s.restart.some((a) => a.includes("C:/tmp/env-root")),
919
+ `LIFEAI_ENV did not expand to its value: ${JSON.stringify(s.restart)}`);
920
+ assert.ok(s.start.some((a) => a.includes("C:/tmp/regen-root")),
921
+ `REGEN_ROOT did not expand in a command arg: ${JSON.stringify(s.start)}`);
922
+ } finally {
923
+ if (oldEnv === undefined) delete process.env.LIFEAI_ENV; else process.env.LIFEAI_ENV = oldEnv;
924
+ if (oldRoot === undefined) delete process.env.REGEN_ROOT; else process.env.REGEN_ROOT = oldRoot;
925
+ }
926
+ });
927
+
928
+ test("registerPlugin resolves ${PACKAGE_ROOT} to the manifest's real origin", () => withTempSupervisor((root) => {
929
+ // An npm package installs at node_modules/@scope/name/ — a path no shipped
930
+ // manifest can hardcode. registerPlugin COPIES the manifest into the managed
931
+ // root, so by the time discoverPlugins() re-reads it the true origin is gone;
932
+ // the token therefore has to be resolved at registration and baked in.
933
+ //
934
+ // Without it, rdc-skills shipped cwd "C:/Dev/rdc-skills" with core +
935
+ // enable_default, so `npm i -g` on any other machine auto-enabled a CORE
936
+ // plugin pointing at a directory that does not exist there.
937
+ const origin = path.join(root, "pretend-node-modules", "@lifeaitools", "some-mcp");
938
+ fs.mkdirSync(origin, { recursive: true });
939
+ const manifestPath = path.join(origin, "clauth-plugin.json");
940
+ fs.writeFileSync(manifestPath, JSON.stringify(baseManifest("package-root-demo", {
941
+ surfaces: [{
942
+ id: "local",
943
+ destination: "local/clauth/pm2",
944
+ lifecycle_owner: "clauth",
945
+ port: 39114,
946
+ health: "/health",
947
+ cwd: "${PACKAGE_ROOT}",
948
+ start: ["node", "${PACKAGE_ROOT}/bin/server.mjs"],
949
+ }],
950
+ }), null, 2), "utf8");
951
+
952
+ const receipt = registerPlugin(manifestPath, "test");
953
+ assert.equal(receipt.resulting_state.ok, true, JSON.stringify(receipt.resulting_state));
954
+
955
+ const surface = listSurfaces().find((s) => s.plugin_id === "package-root-demo");
956
+ assert.ok(surface, "surface was not registered");
957
+ const expected = origin.replaceAll("\\", "/");
958
+ assert.equal(surface.cwd.replaceAll("\\", "/"), expected,
959
+ `cwd did not resolve to the manifest origin: ${surface.cwd}`);
960
+ assert.ok(surface.start.some((a) => a.replaceAll("\\", "/").includes(expected)),
961
+ `command arg did not resolve to the manifest origin: ${JSON.stringify(surface.start)}`);
962
+ assert.ok(!JSON.stringify(surface).includes("PACKAGE_ROOT"),
963
+ "an unexpanded PACKAGE_ROOT token survived into registered state");
964
+ }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {