@codyswann/lisa 2.321.1 → 2.321.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.
Files changed (90) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +7 -6
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +2 -2
  8. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +39 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/rotate-secret.mjs +10 -2
  10. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +2 -0
  11. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/assets/setup.sh +10 -0
  12. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +42 -8
  13. package/plugins/lisa/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +2 -2
  14. package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +39 -0
  15. package/plugins/lisa/skills/lisa-secrets-access/scripts/rotate-secret.mjs +10 -2
  16. package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +2 -0
  17. package/plugins/lisa/skills/lisa-setup-remote-env/assets/setup.sh +10 -0
  18. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +42 -8
  19. package/plugins/lisa-agy/plugin.json +1 -1
  20. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +2 -2
  21. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +39 -0
  22. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/rotate-secret.mjs +10 -2
  23. package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +2 -0
  24. package/plugins/lisa-agy/skills/lisa-setup-remote-env/assets/setup.sh +10 -0
  25. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +42 -8
  26. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  29. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +2 -2
  33. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +39 -0
  34. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/rotate-secret.mjs +10 -2
  35. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +2 -0
  36. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/assets/setup.sh +10 -0
  37. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +42 -8
  38. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +2 -2
  40. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +39 -0
  41. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/rotate-secret.mjs +10 -2
  42. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +2 -0
  43. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/assets/setup.sh +10 -0
  44. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +42 -8
  45. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-expo-agy/plugin.json +1 -1
  48. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  53. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  58. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  63. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  68. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-rails-agy/plugin.json +1 -1
  73. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  78. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  83. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/src/base/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +2 -2
  86. package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +39 -0
  87. package/plugins/src/base/skills/lisa-secrets-access/scripts/rotate-secret.mjs +10 -2
  88. package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +2 -0
  89. package/plugins/src/base/skills/lisa-setup-remote-env/assets/setup.sh +10 -0
  90. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +42 -8
package/package.json CHANGED
@@ -115,7 +115,7 @@
115
115
  "brace-expansion": ">=5.0.9"
116
116
  },
117
117
  "name": "@codyswann/lisa",
118
- "version": "2.321.1",
118
+ "version": "2.321.3",
119
119
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
120
120
  "main": "dist/index.js",
121
121
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.321.1",
3
+ "version": "2.321.3",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.321.1",
3
+ "version": "2.321.3",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -19,7 +19,7 @@
19
19
 
20
20
  import { createHash } from "node:crypto";
21
21
 
22
- import { fetchAll } from "./providers.mjs";
22
+ import { fetchAll, fetchRotatable } from "./providers.mjs";
23
23
  import { readMaterialized } from "./resolve-secret.mjs";
24
24
  import { readConfig } from "./surfaces.mjs";
25
25
 
@@ -220,7 +220,7 @@ function main() {
220
220
  checkRequired(cfg, provider, file, report);
221
221
  checkNaming(provider, report);
222
222
  checkNotes(provider, report);
223
- checkRotating(cfg, provider, report);
223
+ checkRotating(cfg, fetchRotatable(cfg), report);
224
224
  checkTwoStores(provider, file, report);
225
225
 
226
226
  const order = { error: 0, warn: 1, ok: 2 };
@@ -243,6 +243,45 @@ export function fetchAll(cfg) {
243
243
  return normalizeRows(fetchRaw(cfg), cfg.narrow);
244
244
  }
245
245
 
246
+ /**
247
+ * The provider view the rotation path reads.
248
+ *
249
+ * `excludeKeys` keeps a credential off a **surface's disk**. Applying it here
250
+ * too hid the provider record, making `rotating` and `excludeKeys` mutually
251
+ * exclusive — and a credential you cannot see is one you cannot write back to.
252
+ * That is backwards for a consumable credential: the kind you least want
253
+ * materialized is the kind that most needs a proven write path.
254
+ *
255
+ * Nothing here materializes anything. `fetchAll` remains the only view feeding
256
+ * a surface, so an excluded rotating credential still never reaches disk.
257
+ * @param {object} cfg Resolved configuration.
258
+ * @returns {Map<string, {value: string, note: string, id: string|null}>} Selected secrets by name.
259
+ */
260
+ export function fetchRotatable(cfg) {
261
+ return normalizeRows(fetchRaw(cfg), rotationNarrow(cfg));
262
+ }
263
+
264
+ /**
265
+ * Narrowing for the rotation view: project grants intact, `excludeKeys` waived
266
+ * for declared rotating names only.
267
+ *
268
+ * The waiver reaches only names in `secrets.rotating` — config, and therefore
269
+ * reviewed, the same declared-never-inferred rule the write path enforces.
270
+ * Project narrowing is the provider's own boundary and is never widened. Split
271
+ * from {@link fetchRotatable} so the decision is testable without a provider.
272
+ * @param {object} cfg Resolved configuration.
273
+ * @returns {{projectIds: string[], excludeKeys: string[]}} Narrowing controls.
274
+ */
275
+ export function rotationNarrow(cfg) {
276
+ const rotating = new Set(cfg.rotating ?? []);
277
+ return {
278
+ projectIds: cfg.narrow?.projectIds ?? [],
279
+ excludeKeys: (cfg.narrow?.excludeKeys ?? []).filter(
280
+ key => !rotating.has(key)
281
+ ),
282
+ };
283
+ }
284
+
246
285
  /**
247
286
  * Find one raw row by exact key, bypassing the exposure boundary.
248
287
  *
@@ -30,7 +30,12 @@
30
30
  import { readFileSync } from "node:fs";
31
31
  import { hostname } from "node:os";
32
32
 
33
- import { LEASE_KEY, fetchAll, rawByKey, writeSecret } from "./providers.mjs";
33
+ import {
34
+ LEASE_KEY,
35
+ fetchRotatable,
36
+ rawByKey,
37
+ writeSecret,
38
+ } from "./providers.mjs";
34
39
  import { readConfig } from "./surfaces.mjs";
35
40
 
36
41
  /** How long a lease is honoured before it is treated as abandoned. */
@@ -63,7 +68,10 @@ function assertRotating(name, cfg) {
63
68
  * @returns {{value: string, id: string}} The current entry.
64
69
  */
65
70
  function entryFor(name, cfg) {
66
- const hit = fetchAll(cfg).get(name);
71
+ // Rotation view, not the materialization view: a name may be excluded from
72
+ // every surface and still need its replacement persisted. assertRotating has
73
+ // already proved this name is declared in config.
74
+ const hit = fetchRotatable(cfg).get(name);
67
75
  if (!hit) throw new Error(`${name} is not available to this account`);
68
76
  if (!hit.id) {
69
77
  throw new Error(
@@ -161,6 +161,8 @@ It reads the project's own install command from its lockfile and the bootstrap n
161
161
 
162
162
  - **`gh` is not pre-installed.** If the project's flows shell out to it, add it to `remoteEnv.tools.install`, pinned and checksummed like anything else.
163
163
  - **A proxied credential reads as the literal string `proxy-injected`.** Tools that authenticate through the GitHub proxy work; a script that reads the variable directly gets the placeholder. The read-back asserts this rather than leaving it to be discovered against a live service.
164
+ - **The setup field runs from `$HOME`, not from the checkout.** The emitted setup command starts with `cd <repo-name>` on purpose; without it the package install runs one directory above the clone and cannot find `package.json`.
165
+ - **Trusted network access is not enough for provider CLIs.** Use Custom and include package registries, GitHub, cloud SDK hosts, and the bootstrap credential manager API.
164
166
 
165
167
  ### One environment per project, pinned locally
166
168
 
@@ -13,6 +13,16 @@
13
13
  # and emphatically not in a vendor settings field.
14
14
  set -euo pipefail
15
15
 
16
+ # Claude Code web runs the environment setup field from $HOME, while the
17
+ # checkout lives at $HOME/<repo>. If this installed copy is invoked by absolute
18
+ # path, move back to the repository root before resolving project-local files.
19
+ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
20
+ case "$script_dir" in
21
+ */scripts/lisa-remote-env)
22
+ cd "$script_dir/../.."
23
+ ;;
24
+ esac
25
+
16
26
  # Node is the one thing that cannot be installed by the installer, since the
17
27
  # installer is written in it. Fail with an actionable message rather than a
18
28
  # "command not found" forty lines deep.
@@ -37,7 +37,7 @@ import {
37
37
  rmSync,
38
38
  writeFileSync,
39
39
  } from "node:fs";
40
- import { dirname, join, resolve } from "node:path";
40
+ import { basename, dirname, join, resolve } from "node:path";
41
41
  import { fileURLToPath, pathToFileURL } from "node:url";
42
42
 
43
43
  import { assertPinned, extractVersion, planToolchain } from "./toolchain.mjs";
@@ -337,11 +337,12 @@ export function pinEnvironment(environmentId, cwd = process.cwd()) {
337
337
  * settings page, no direct URL, and no endpoint. Emit is not a degraded option
338
338
  * here, it is the only one — so the read-back in `verify-remote-env.mjs` is
339
339
  * what makes the result trustworthy, exactly as it would be at any other tier.
340
- * @param {{bootstrapKey: string|null, install: string}} options Project details.
340
+ * @param {{bootstrapKey: string|null, install: string, repoDir?: string}} options Project details.
341
341
  * @returns {string} Text to show the operator.
342
342
  */
343
- export function emitClaudeWeb({ bootstrapKey, install }) {
343
+ export function emitClaudeWeb({ bootstrapKey, install, repoDir = "lisa" }) {
344
344
  const key = bootstrapKey ?? "<secrets.bootstrap.key is not configured>";
345
+ const setup = `cd ${shellQuote(repoDir)} && ${install} && bash scripts/lisa-remote-env/setup.sh`;
345
346
  return [
346
347
  "Provisioning tier: EMIT — and for this surface that is the only tier.",
347
348
  " A Claude cloud environment is account-scoped configuration edited in the",
@@ -350,7 +351,8 @@ export function emitClaudeWeb({ bootstrapKey, install }) {
350
351
  "",
351
352
  "Paste into the environment dialog",
352
353
  "---------------------------------",
353
- " Network access: Trusted, or Custom plus any host your project needs",
354
+ " Network access: Custom plus your package registries, GitHub, cloud SDK",
355
+ " hosts, and the bootstrap credential manager API",
354
356
  "",
355
357
  " Environment variables:",
356
358
  ` ${key}=<read this from your credential manager>`,
@@ -362,11 +364,13 @@ export function emitClaudeWeb({ bootstrapKey, install }) {
362
364
  " is why exactly one value needs to live here.",
363
365
  "",
364
366
  " Setup script:",
365
- ` ${install} && bash scripts/lisa-remote-env/setup.sh`,
367
+ ` ${setup}`,
366
368
  "",
367
- " The install must come first. On a fresh container node_modules is the",
368
- " only copy of the Lisa skills present, because Claude receives them as",
369
- " an installed plugin rather than as part of the clone.",
369
+ ` Claude runs this field from $HOME, while the checkout is $HOME/${repoDir}.`,
370
+ " The `cd` must come first, and the install must run before the setup",
371
+ " script. On a fresh container node_modules is the only copy of the Lisa",
372
+ " skills present, because Claude receives them as an installed plugin",
373
+ " rather than as part of the clone.",
370
374
  "",
371
375
  "Commit to the repository",
372
376
  "------------------------",
@@ -467,6 +471,35 @@ export function detectInstallCommand(cwd = process.cwd()) {
467
471
  return "<your install command>";
468
472
  }
469
473
 
474
+ /**
475
+ * Name the directory Claude creates under $HOME when it clones this repository.
476
+ *
477
+ * Claude cloud runs the environment setup field from $HOME, not from the
478
+ * checkout. The setup line therefore has to `cd` into the clone before running
479
+ * the project install. Prefer the configured GitHub repo name because it is the
480
+ * thing Claude clones; fall back to the current directory for non-GitHub tests
481
+ * and local dry runs.
482
+ * @param {string} [cwd] Repository root.
483
+ * @returns {string} Directory name to place after `cd`.
484
+ */
485
+ export function detectRepositoryDirectory(cwd = process.cwd()) {
486
+ const path = join(cwd, ".lisa.config.json");
487
+ if (existsSync(path)) {
488
+ const repo = JSON.parse(readFileSync(path, "utf8")).github?.repo;
489
+ if (repo && !repo.includes("/") && !repo.includes("\0")) return repo;
490
+ }
491
+ return basename(cwd);
492
+ }
493
+
494
+ /**
495
+ * Quote a value for POSIX shell usage.
496
+ * @param {string} value Raw value.
497
+ * @returns {string} Single-quoted shell literal.
498
+ */
499
+ function shellQuote(value) {
500
+ return "'" + String(value).replaceAll("'", "'\\''") + "'";
501
+ }
502
+
470
503
  /**
471
504
  * Read the bootstrap key name, which is the one value the operator must paste.
472
505
  * @param {string} [cwd] Repository root.
@@ -519,6 +552,7 @@ async function main() {
519
552
  emitClaudeWeb({
520
553
  bootstrapKey: readBootstrapKey(),
521
554
  install: detectInstallCommand(),
555
+ repoDir: detectRepositoryDirectory(),
522
556
  })
523
557
  );
524
558
  return;
@@ -19,7 +19,7 @@
19
19
 
20
20
  import { createHash } from "node:crypto";
21
21
 
22
- import { fetchAll } from "./providers.mjs";
22
+ import { fetchAll, fetchRotatable } from "./providers.mjs";
23
23
  import { readMaterialized } from "./resolve-secret.mjs";
24
24
  import { readConfig } from "./surfaces.mjs";
25
25
 
@@ -220,7 +220,7 @@ function main() {
220
220
  checkRequired(cfg, provider, file, report);
221
221
  checkNaming(provider, report);
222
222
  checkNotes(provider, report);
223
- checkRotating(cfg, provider, report);
223
+ checkRotating(cfg, fetchRotatable(cfg), report);
224
224
  checkTwoStores(provider, file, report);
225
225
 
226
226
  const order = { error: 0, warn: 1, ok: 2 };
@@ -243,6 +243,45 @@ export function fetchAll(cfg) {
243
243
  return normalizeRows(fetchRaw(cfg), cfg.narrow);
244
244
  }
245
245
 
246
+ /**
247
+ * The provider view the rotation path reads.
248
+ *
249
+ * `excludeKeys` keeps a credential off a **surface's disk**. Applying it here
250
+ * too hid the provider record, making `rotating` and `excludeKeys` mutually
251
+ * exclusive — and a credential you cannot see is one you cannot write back to.
252
+ * That is backwards for a consumable credential: the kind you least want
253
+ * materialized is the kind that most needs a proven write path.
254
+ *
255
+ * Nothing here materializes anything. `fetchAll` remains the only view feeding
256
+ * a surface, so an excluded rotating credential still never reaches disk.
257
+ * @param {object} cfg Resolved configuration.
258
+ * @returns {Map<string, {value: string, note: string, id: string|null}>} Selected secrets by name.
259
+ */
260
+ export function fetchRotatable(cfg) {
261
+ return normalizeRows(fetchRaw(cfg), rotationNarrow(cfg));
262
+ }
263
+
264
+ /**
265
+ * Narrowing for the rotation view: project grants intact, `excludeKeys` waived
266
+ * for declared rotating names only.
267
+ *
268
+ * The waiver reaches only names in `secrets.rotating` — config, and therefore
269
+ * reviewed, the same declared-never-inferred rule the write path enforces.
270
+ * Project narrowing is the provider's own boundary and is never widened. Split
271
+ * from {@link fetchRotatable} so the decision is testable without a provider.
272
+ * @param {object} cfg Resolved configuration.
273
+ * @returns {{projectIds: string[], excludeKeys: string[]}} Narrowing controls.
274
+ */
275
+ export function rotationNarrow(cfg) {
276
+ const rotating = new Set(cfg.rotating ?? []);
277
+ return {
278
+ projectIds: cfg.narrow?.projectIds ?? [],
279
+ excludeKeys: (cfg.narrow?.excludeKeys ?? []).filter(
280
+ key => !rotating.has(key)
281
+ ),
282
+ };
283
+ }
284
+
246
285
  /**
247
286
  * Find one raw row by exact key, bypassing the exposure boundary.
248
287
  *
@@ -30,7 +30,12 @@
30
30
  import { readFileSync } from "node:fs";
31
31
  import { hostname } from "node:os";
32
32
 
33
- import { LEASE_KEY, fetchAll, rawByKey, writeSecret } from "./providers.mjs";
33
+ import {
34
+ LEASE_KEY,
35
+ fetchRotatable,
36
+ rawByKey,
37
+ writeSecret,
38
+ } from "./providers.mjs";
34
39
  import { readConfig } from "./surfaces.mjs";
35
40
 
36
41
  /** How long a lease is honoured before it is treated as abandoned. */
@@ -63,7 +68,10 @@ function assertRotating(name, cfg) {
63
68
  * @returns {{value: string, id: string}} The current entry.
64
69
  */
65
70
  function entryFor(name, cfg) {
66
- const hit = fetchAll(cfg).get(name);
71
+ // Rotation view, not the materialization view: a name may be excluded from
72
+ // every surface and still need its replacement persisted. assertRotating has
73
+ // already proved this name is declared in config.
74
+ const hit = fetchRotatable(cfg).get(name);
67
75
  if (!hit) throw new Error(`${name} is not available to this account`);
68
76
  if (!hit.id) {
69
77
  throw new Error(
@@ -161,6 +161,8 @@ It reads the project's own install command from its lockfile and the bootstrap n
161
161
 
162
162
  - **`gh` is not pre-installed.** If the project's flows shell out to it, add it to `remoteEnv.tools.install`, pinned and checksummed like anything else.
163
163
  - **A proxied credential reads as the literal string `proxy-injected`.** Tools that authenticate through the GitHub proxy work; a script that reads the variable directly gets the placeholder. The read-back asserts this rather than leaving it to be discovered against a live service.
164
+ - **The setup field runs from `$HOME`, not from the checkout.** The emitted setup command starts with `cd <repo-name>` on purpose; without it the package install runs one directory above the clone and cannot find `package.json`.
165
+ - **Trusted network access is not enough for provider CLIs.** Use Custom and include package registries, GitHub, cloud SDK hosts, and the bootstrap credential manager API.
164
166
 
165
167
  ### One environment per project, pinned locally
166
168
 
@@ -13,6 +13,16 @@
13
13
  # and emphatically not in a vendor settings field.
14
14
  set -euo pipefail
15
15
 
16
+ # Claude Code web runs the environment setup field from $HOME, while the
17
+ # checkout lives at $HOME/<repo>. If this installed copy is invoked by absolute
18
+ # path, move back to the repository root before resolving project-local files.
19
+ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
20
+ case "$script_dir" in
21
+ */scripts/lisa-remote-env)
22
+ cd "$script_dir/../.."
23
+ ;;
24
+ esac
25
+
16
26
  # Node is the one thing that cannot be installed by the installer, since the
17
27
  # installer is written in it. Fail with an actionable message rather than a
18
28
  # "command not found" forty lines deep.
@@ -37,7 +37,7 @@ import {
37
37
  rmSync,
38
38
  writeFileSync,
39
39
  } from "node:fs";
40
- import { dirname, join, resolve } from "node:path";
40
+ import { basename, dirname, join, resolve } from "node:path";
41
41
  import { fileURLToPath, pathToFileURL } from "node:url";
42
42
 
43
43
  import { assertPinned, extractVersion, planToolchain } from "./toolchain.mjs";
@@ -337,11 +337,12 @@ export function pinEnvironment(environmentId, cwd = process.cwd()) {
337
337
  * settings page, no direct URL, and no endpoint. Emit is not a degraded option
338
338
  * here, it is the only one — so the read-back in `verify-remote-env.mjs` is
339
339
  * what makes the result trustworthy, exactly as it would be at any other tier.
340
- * @param {{bootstrapKey: string|null, install: string}} options Project details.
340
+ * @param {{bootstrapKey: string|null, install: string, repoDir?: string}} options Project details.
341
341
  * @returns {string} Text to show the operator.
342
342
  */
343
- export function emitClaudeWeb({ bootstrapKey, install }) {
343
+ export function emitClaudeWeb({ bootstrapKey, install, repoDir = "lisa" }) {
344
344
  const key = bootstrapKey ?? "<secrets.bootstrap.key is not configured>";
345
+ const setup = `cd ${shellQuote(repoDir)} && ${install} && bash scripts/lisa-remote-env/setup.sh`;
345
346
  return [
346
347
  "Provisioning tier: EMIT — and for this surface that is the only tier.",
347
348
  " A Claude cloud environment is account-scoped configuration edited in the",
@@ -350,7 +351,8 @@ export function emitClaudeWeb({ bootstrapKey, install }) {
350
351
  "",
351
352
  "Paste into the environment dialog",
352
353
  "---------------------------------",
353
- " Network access: Trusted, or Custom plus any host your project needs",
354
+ " Network access: Custom plus your package registries, GitHub, cloud SDK",
355
+ " hosts, and the bootstrap credential manager API",
354
356
  "",
355
357
  " Environment variables:",
356
358
  ` ${key}=<read this from your credential manager>`,
@@ -362,11 +364,13 @@ export function emitClaudeWeb({ bootstrapKey, install }) {
362
364
  " is why exactly one value needs to live here.",
363
365
  "",
364
366
  " Setup script:",
365
- ` ${install} && bash scripts/lisa-remote-env/setup.sh`,
367
+ ` ${setup}`,
366
368
  "",
367
- " The install must come first. On a fresh container node_modules is the",
368
- " only copy of the Lisa skills present, because Claude receives them as",
369
- " an installed plugin rather than as part of the clone.",
369
+ ` Claude runs this field from $HOME, while the checkout is $HOME/${repoDir}.`,
370
+ " The `cd` must come first, and the install must run before the setup",
371
+ " script. On a fresh container node_modules is the only copy of the Lisa",
372
+ " skills present, because Claude receives them as an installed plugin",
373
+ " rather than as part of the clone.",
370
374
  "",
371
375
  "Commit to the repository",
372
376
  "------------------------",
@@ -467,6 +471,35 @@ export function detectInstallCommand(cwd = process.cwd()) {
467
471
  return "<your install command>";
468
472
  }
469
473
 
474
+ /**
475
+ * Name the directory Claude creates under $HOME when it clones this repository.
476
+ *
477
+ * Claude cloud runs the environment setup field from $HOME, not from the
478
+ * checkout. The setup line therefore has to `cd` into the clone before running
479
+ * the project install. Prefer the configured GitHub repo name because it is the
480
+ * thing Claude clones; fall back to the current directory for non-GitHub tests
481
+ * and local dry runs.
482
+ * @param {string} [cwd] Repository root.
483
+ * @returns {string} Directory name to place after `cd`.
484
+ */
485
+ export function detectRepositoryDirectory(cwd = process.cwd()) {
486
+ const path = join(cwd, ".lisa.config.json");
487
+ if (existsSync(path)) {
488
+ const repo = JSON.parse(readFileSync(path, "utf8")).github?.repo;
489
+ if (repo && !repo.includes("/") && !repo.includes("\0")) return repo;
490
+ }
491
+ return basename(cwd);
492
+ }
493
+
494
+ /**
495
+ * Quote a value for POSIX shell usage.
496
+ * @param {string} value Raw value.
497
+ * @returns {string} Single-quoted shell literal.
498
+ */
499
+ function shellQuote(value) {
500
+ return "'" + String(value).replaceAll("'", "'\\''") + "'";
501
+ }
502
+
470
503
  /**
471
504
  * Read the bootstrap key name, which is the one value the operator must paste.
472
505
  * @param {string} [cwd] Repository root.
@@ -519,6 +552,7 @@ async function main() {
519
552
  emitClaudeWeb({
520
553
  bootstrapKey: readBootstrapKey(),
521
554
  install: detectInstallCommand(),
555
+ repoDir: detectRepositoryDirectory(),
522
556
  })
523
557
  );
524
558
  return;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.321.1",
3
+ "version": "2.321.3",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -19,7 +19,7 @@
19
19
 
20
20
  import { createHash } from "node:crypto";
21
21
 
22
- import { fetchAll } from "./providers.mjs";
22
+ import { fetchAll, fetchRotatable } from "./providers.mjs";
23
23
  import { readMaterialized } from "./resolve-secret.mjs";
24
24
  import { readConfig } from "./surfaces.mjs";
25
25
 
@@ -220,7 +220,7 @@ function main() {
220
220
  checkRequired(cfg, provider, file, report);
221
221
  checkNaming(provider, report);
222
222
  checkNotes(provider, report);
223
- checkRotating(cfg, provider, report);
223
+ checkRotating(cfg, fetchRotatable(cfg), report);
224
224
  checkTwoStores(provider, file, report);
225
225
 
226
226
  const order = { error: 0, warn: 1, ok: 2 };
@@ -243,6 +243,45 @@ export function fetchAll(cfg) {
243
243
  return normalizeRows(fetchRaw(cfg), cfg.narrow);
244
244
  }
245
245
 
246
+ /**
247
+ * The provider view the rotation path reads.
248
+ *
249
+ * `excludeKeys` keeps a credential off a **surface's disk**. Applying it here
250
+ * too hid the provider record, making `rotating` and `excludeKeys` mutually
251
+ * exclusive — and a credential you cannot see is one you cannot write back to.
252
+ * That is backwards for a consumable credential: the kind you least want
253
+ * materialized is the kind that most needs a proven write path.
254
+ *
255
+ * Nothing here materializes anything. `fetchAll` remains the only view feeding
256
+ * a surface, so an excluded rotating credential still never reaches disk.
257
+ * @param {object} cfg Resolved configuration.
258
+ * @returns {Map<string, {value: string, note: string, id: string|null}>} Selected secrets by name.
259
+ */
260
+ export function fetchRotatable(cfg) {
261
+ return normalizeRows(fetchRaw(cfg), rotationNarrow(cfg));
262
+ }
263
+
264
+ /**
265
+ * Narrowing for the rotation view: project grants intact, `excludeKeys` waived
266
+ * for declared rotating names only.
267
+ *
268
+ * The waiver reaches only names in `secrets.rotating` — config, and therefore
269
+ * reviewed, the same declared-never-inferred rule the write path enforces.
270
+ * Project narrowing is the provider's own boundary and is never widened. Split
271
+ * from {@link fetchRotatable} so the decision is testable without a provider.
272
+ * @param {object} cfg Resolved configuration.
273
+ * @returns {{projectIds: string[], excludeKeys: string[]}} Narrowing controls.
274
+ */
275
+ export function rotationNarrow(cfg) {
276
+ const rotating = new Set(cfg.rotating ?? []);
277
+ return {
278
+ projectIds: cfg.narrow?.projectIds ?? [],
279
+ excludeKeys: (cfg.narrow?.excludeKeys ?? []).filter(
280
+ key => !rotating.has(key)
281
+ ),
282
+ };
283
+ }
284
+
246
285
  /**
247
286
  * Find one raw row by exact key, bypassing the exposure boundary.
248
287
  *
@@ -30,7 +30,12 @@
30
30
  import { readFileSync } from "node:fs";
31
31
  import { hostname } from "node:os";
32
32
 
33
- import { LEASE_KEY, fetchAll, rawByKey, writeSecret } from "./providers.mjs";
33
+ import {
34
+ LEASE_KEY,
35
+ fetchRotatable,
36
+ rawByKey,
37
+ writeSecret,
38
+ } from "./providers.mjs";
34
39
  import { readConfig } from "./surfaces.mjs";
35
40
 
36
41
  /** How long a lease is honoured before it is treated as abandoned. */
@@ -63,7 +68,10 @@ function assertRotating(name, cfg) {
63
68
  * @returns {{value: string, id: string}} The current entry.
64
69
  */
65
70
  function entryFor(name, cfg) {
66
- const hit = fetchAll(cfg).get(name);
71
+ // Rotation view, not the materialization view: a name may be excluded from
72
+ // every surface and still need its replacement persisted. assertRotating has
73
+ // already proved this name is declared in config.
74
+ const hit = fetchRotatable(cfg).get(name);
67
75
  if (!hit) throw new Error(`${name} is not available to this account`);
68
76
  if (!hit.id) {
69
77
  throw new Error(
@@ -161,6 +161,8 @@ It reads the project's own install command from its lockfile and the bootstrap n
161
161
 
162
162
  - **`gh` is not pre-installed.** If the project's flows shell out to it, add it to `remoteEnv.tools.install`, pinned and checksummed like anything else.
163
163
  - **A proxied credential reads as the literal string `proxy-injected`.** Tools that authenticate through the GitHub proxy work; a script that reads the variable directly gets the placeholder. The read-back asserts this rather than leaving it to be discovered against a live service.
164
+ - **The setup field runs from `$HOME`, not from the checkout.** The emitted setup command starts with `cd <repo-name>` on purpose; without it the package install runs one directory above the clone and cannot find `package.json`.
165
+ - **Trusted network access is not enough for provider CLIs.** Use Custom and include package registries, GitHub, cloud SDK hosts, and the bootstrap credential manager API.
164
166
 
165
167
  ### One environment per project, pinned locally
166
168
 
@@ -13,6 +13,16 @@
13
13
  # and emphatically not in a vendor settings field.
14
14
  set -euo pipefail
15
15
 
16
+ # Claude Code web runs the environment setup field from $HOME, while the
17
+ # checkout lives at $HOME/<repo>. If this installed copy is invoked by absolute
18
+ # path, move back to the repository root before resolving project-local files.
19
+ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
20
+ case "$script_dir" in
21
+ */scripts/lisa-remote-env)
22
+ cd "$script_dir/../.."
23
+ ;;
24
+ esac
25
+
16
26
  # Node is the one thing that cannot be installed by the installer, since the
17
27
  # installer is written in it. Fail with an actionable message rather than a
18
28
  # "command not found" forty lines deep.