@codyswann/lisa 2.333.1 → 2.333.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.
Files changed (60) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +2 -1
  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/materialize-secrets.mjs +57 -26
  8. package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +57 -26
  9. package/plugins/lisa-agy/plugin.json +1 -1
  10. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +57 -26
  11. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  14. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +57 -26
  18. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +57 -26
  20. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-expo-agy/plugin.json +1 -1
  23. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  33. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  38. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  43. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-rails-agy/plugin.json +1 -1
  48. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  53. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  58. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +57 -26
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.333.1",
118
+ "version": "2.333.2",
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.333.1",
3
+ "version": "2.333.2",
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.333.1",
3
+ "version": "2.333.2",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -80,7 +80,41 @@ const PROFILE_END = "# <<< lisa secrets (managed) <<<";
80
80
  * consumer while still being the thing that distinguishes "our file, refresh
81
81
  * it" from "someone else's file, leave it alone".
82
82
  */
83
- const MANAGED_MARKER = "# managed by lisa-secrets-access";
83
+ const MANAGED_MARKER = "# >>> managed by lisa-secrets-access >>>";
84
+
85
+ /** Closes the managed region of an `~/.aws` file. */
86
+ const MANAGED_END = "# <<< managed by lisa-secrets-access <<<";
87
+
88
+ /**
89
+ * Replace this module's delimited region in a file, preserving everything else.
90
+ *
91
+ * Written as a merge rather than a whole-file write because both `~/.aws` files
92
+ * routinely hold sections nobody here knows about — an operator's own profiles,
93
+ * or a container's bare `[default]`. Refusing on their account wrote nothing at
94
+ * all; overwriting would delete them. This does neither.
95
+ * @param {string} current Existing file contents, or "".
96
+ * @param {string} body The region this module owns.
97
+ * @returns {string} The merged file.
98
+ */
99
+ export function upsertManagedBlock(current, body) {
100
+ const block = `${MANAGED_MARKER}\n${body.trimEnd()}\n${MANAGED_END}`;
101
+ const start = current.indexOf(MANAGED_MARKER);
102
+
103
+ if (start === -1) {
104
+ const prefix =
105
+ current && !current.endsWith("\n") ? `${current}\n` : current;
106
+ return `${prefix}${prefix ? "\n" : ""}${block}\n`;
107
+ }
108
+
109
+ const endAt = current.indexOf(MANAGED_END, start);
110
+ // A truncated block (marker opened, never closed) would otherwise swallow the
111
+ // rest of the file on every subsequent write.
112
+ const after =
113
+ endAt === -1
114
+ ? ""
115
+ : current.slice(endAt + MANAGED_END.length).replace(/^\n/, "");
116
+ return `${current.slice(0, start)}${block}\n${after}`;
117
+ }
84
118
 
85
119
  /**
86
120
  * Make every shell in this container load the materialized secrets.
@@ -161,7 +195,7 @@ export function installAwsProfiles(bundle, options = {}) {
161
195
  const {
162
196
  home = process.env.HOME || homedir(),
163
197
  mkdir = mkdirSync,
164
- write = writeFileSync,
198
+ write = writeAtomic,
165
199
  read = readFileSync,
166
200
  exists = existsSync,
167
201
  chmod = chmodSync,
@@ -171,35 +205,32 @@ export function installAwsProfiles(bundle, options = {}) {
171
205
  if (!rendered) return [];
172
206
 
173
207
  const dir = join(home, ".aws");
208
+ mkdir(dir, { recursive: true, mode: 0o700 });
209
+ chmod(dir, 0o700);
174
210
 
175
- // Never overwrite an ~/.aws this did not write.
211
+ // Merge into a delimited block; never replace the file.
212
+ //
213
+ // Refusing whole files whenever one already existed sounded safe and was
214
+ // worse than useless: a container ships `~/.aws/config` containing a bare
215
+ // `[default]`, so the guard fired every time, wrote nothing, and returned
216
+ // silently — while AWS_PROFILE was still derived from the bundle. The session
217
+ // got a pointer to a profile that did not exist:
218
+ //
219
+ // The config profile (agent-dev) could not be found
176
220
  //
177
- // These are whole-file writes, so a pre-existing credentials file a
178
- // developer's own profiles, or something another tool set up would be
179
- // destroyed rather than merged. This only runs on a surface allowed to write
180
- // secrets to disk (a disposable container), but "usually disposable" is not a
181
- // reason to be able to delete someone's credentials. The marker makes our own
182
- // file re-writable while anything else is left alone and reported.
183
- for (const name of ["credentials", "config"]) {
221
+ // Merging keeps the operator's own sections intact AND writes ours, which is
222
+ // what the guard was actually for. Same delimited-block approach as the shell
223
+ // profile, and `#` is a comment in the shared-config format so the markers are
224
+ // inert to every consumer.
225
+ for (const [name, body] of [
226
+ ["credentials", rendered.credentials],
227
+ ["config", rendered.config],
228
+ ]) {
184
229
  const file = join(dir, name);
185
- if (exists(file) && !String(read(file, "utf8")).includes(MANAGED_MARKER)) {
186
- return [];
187
- }
230
+ const current = exists(file) ? String(read(file, "utf8")) : "";
231
+ write(file, upsertManagedBlock(current, body));
188
232
  }
189
233
 
190
- mkdir(dir, { recursive: true, mode: 0o700 });
191
- chmod(dir, 0o700);
192
- write(
193
- join(dir, "credentials"),
194
- `${MANAGED_MARKER}\n${rendered.credentials}`,
195
- {
196
- mode: 0o600,
197
- }
198
- );
199
- write(join(dir, "config"), `${MANAGED_MARKER}\n${rendered.config}`, {
200
- mode: 0o600,
201
- });
202
-
203
234
  return rendered.profiles;
204
235
  }
205
236
 
@@ -80,7 +80,41 @@ const PROFILE_END = "# <<< lisa secrets (managed) <<<";
80
80
  * consumer while still being the thing that distinguishes "our file, refresh
81
81
  * it" from "someone else's file, leave it alone".
82
82
  */
83
- const MANAGED_MARKER = "# managed by lisa-secrets-access";
83
+ const MANAGED_MARKER = "# >>> managed by lisa-secrets-access >>>";
84
+
85
+ /** Closes the managed region of an `~/.aws` file. */
86
+ const MANAGED_END = "# <<< managed by lisa-secrets-access <<<";
87
+
88
+ /**
89
+ * Replace this module's delimited region in a file, preserving everything else.
90
+ *
91
+ * Written as a merge rather than a whole-file write because both `~/.aws` files
92
+ * routinely hold sections nobody here knows about — an operator's own profiles,
93
+ * or a container's bare `[default]`. Refusing on their account wrote nothing at
94
+ * all; overwriting would delete them. This does neither.
95
+ * @param {string} current Existing file contents, or "".
96
+ * @param {string} body The region this module owns.
97
+ * @returns {string} The merged file.
98
+ */
99
+ export function upsertManagedBlock(current, body) {
100
+ const block = `${MANAGED_MARKER}\n${body.trimEnd()}\n${MANAGED_END}`;
101
+ const start = current.indexOf(MANAGED_MARKER);
102
+
103
+ if (start === -1) {
104
+ const prefix =
105
+ current && !current.endsWith("\n") ? `${current}\n` : current;
106
+ return `${prefix}${prefix ? "\n" : ""}${block}\n`;
107
+ }
108
+
109
+ const endAt = current.indexOf(MANAGED_END, start);
110
+ // A truncated block (marker opened, never closed) would otherwise swallow the
111
+ // rest of the file on every subsequent write.
112
+ const after =
113
+ endAt === -1
114
+ ? ""
115
+ : current.slice(endAt + MANAGED_END.length).replace(/^\n/, "");
116
+ return `${current.slice(0, start)}${block}\n${after}`;
117
+ }
84
118
 
85
119
  /**
86
120
  * Make every shell in this container load the materialized secrets.
@@ -161,7 +195,7 @@ export function installAwsProfiles(bundle, options = {}) {
161
195
  const {
162
196
  home = process.env.HOME || homedir(),
163
197
  mkdir = mkdirSync,
164
- write = writeFileSync,
198
+ write = writeAtomic,
165
199
  read = readFileSync,
166
200
  exists = existsSync,
167
201
  chmod = chmodSync,
@@ -171,35 +205,32 @@ export function installAwsProfiles(bundle, options = {}) {
171
205
  if (!rendered) return [];
172
206
 
173
207
  const dir = join(home, ".aws");
208
+ mkdir(dir, { recursive: true, mode: 0o700 });
209
+ chmod(dir, 0o700);
174
210
 
175
- // Never overwrite an ~/.aws this did not write.
211
+ // Merge into a delimited block; never replace the file.
212
+ //
213
+ // Refusing whole files whenever one already existed sounded safe and was
214
+ // worse than useless: a container ships `~/.aws/config` containing a bare
215
+ // `[default]`, so the guard fired every time, wrote nothing, and returned
216
+ // silently — while AWS_PROFILE was still derived from the bundle. The session
217
+ // got a pointer to a profile that did not exist:
218
+ //
219
+ // The config profile (agent-dev) could not be found
176
220
  //
177
- // These are whole-file writes, so a pre-existing credentials file a
178
- // developer's own profiles, or something another tool set up would be
179
- // destroyed rather than merged. This only runs on a surface allowed to write
180
- // secrets to disk (a disposable container), but "usually disposable" is not a
181
- // reason to be able to delete someone's credentials. The marker makes our own
182
- // file re-writable while anything else is left alone and reported.
183
- for (const name of ["credentials", "config"]) {
221
+ // Merging keeps the operator's own sections intact AND writes ours, which is
222
+ // what the guard was actually for. Same delimited-block approach as the shell
223
+ // profile, and `#` is a comment in the shared-config format so the markers are
224
+ // inert to every consumer.
225
+ for (const [name, body] of [
226
+ ["credentials", rendered.credentials],
227
+ ["config", rendered.config],
228
+ ]) {
184
229
  const file = join(dir, name);
185
- if (exists(file) && !String(read(file, "utf8")).includes(MANAGED_MARKER)) {
186
- return [];
187
- }
230
+ const current = exists(file) ? String(read(file, "utf8")) : "";
231
+ write(file, upsertManagedBlock(current, body));
188
232
  }
189
233
 
190
- mkdir(dir, { recursive: true, mode: 0o700 });
191
- chmod(dir, 0o700);
192
- write(
193
- join(dir, "credentials"),
194
- `${MANAGED_MARKER}\n${rendered.credentials}`,
195
- {
196
- mode: 0o600,
197
- }
198
- );
199
- write(join(dir, "config"), `${MANAGED_MARKER}\n${rendered.config}`, {
200
- mode: 0o600,
201
- });
202
-
203
234
  return rendered.profiles;
204
235
  }
205
236
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.333.1",
3
+ "version": "2.333.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -80,7 +80,41 @@ const PROFILE_END = "# <<< lisa secrets (managed) <<<";
80
80
  * consumer while still being the thing that distinguishes "our file, refresh
81
81
  * it" from "someone else's file, leave it alone".
82
82
  */
83
- const MANAGED_MARKER = "# managed by lisa-secrets-access";
83
+ const MANAGED_MARKER = "# >>> managed by lisa-secrets-access >>>";
84
+
85
+ /** Closes the managed region of an `~/.aws` file. */
86
+ const MANAGED_END = "# <<< managed by lisa-secrets-access <<<";
87
+
88
+ /**
89
+ * Replace this module's delimited region in a file, preserving everything else.
90
+ *
91
+ * Written as a merge rather than a whole-file write because both `~/.aws` files
92
+ * routinely hold sections nobody here knows about — an operator's own profiles,
93
+ * or a container's bare `[default]`. Refusing on their account wrote nothing at
94
+ * all; overwriting would delete them. This does neither.
95
+ * @param {string} current Existing file contents, or "".
96
+ * @param {string} body The region this module owns.
97
+ * @returns {string} The merged file.
98
+ */
99
+ export function upsertManagedBlock(current, body) {
100
+ const block = `${MANAGED_MARKER}\n${body.trimEnd()}\n${MANAGED_END}`;
101
+ const start = current.indexOf(MANAGED_MARKER);
102
+
103
+ if (start === -1) {
104
+ const prefix =
105
+ current && !current.endsWith("\n") ? `${current}\n` : current;
106
+ return `${prefix}${prefix ? "\n" : ""}${block}\n`;
107
+ }
108
+
109
+ const endAt = current.indexOf(MANAGED_END, start);
110
+ // A truncated block (marker opened, never closed) would otherwise swallow the
111
+ // rest of the file on every subsequent write.
112
+ const after =
113
+ endAt === -1
114
+ ? ""
115
+ : current.slice(endAt + MANAGED_END.length).replace(/^\n/, "");
116
+ return `${current.slice(0, start)}${block}\n${after}`;
117
+ }
84
118
 
85
119
  /**
86
120
  * Make every shell in this container load the materialized secrets.
@@ -161,7 +195,7 @@ export function installAwsProfiles(bundle, options = {}) {
161
195
  const {
162
196
  home = process.env.HOME || homedir(),
163
197
  mkdir = mkdirSync,
164
- write = writeFileSync,
198
+ write = writeAtomic,
165
199
  read = readFileSync,
166
200
  exists = existsSync,
167
201
  chmod = chmodSync,
@@ -171,35 +205,32 @@ export function installAwsProfiles(bundle, options = {}) {
171
205
  if (!rendered) return [];
172
206
 
173
207
  const dir = join(home, ".aws");
208
+ mkdir(dir, { recursive: true, mode: 0o700 });
209
+ chmod(dir, 0o700);
174
210
 
175
- // Never overwrite an ~/.aws this did not write.
211
+ // Merge into a delimited block; never replace the file.
212
+ //
213
+ // Refusing whole files whenever one already existed sounded safe and was
214
+ // worse than useless: a container ships `~/.aws/config` containing a bare
215
+ // `[default]`, so the guard fired every time, wrote nothing, and returned
216
+ // silently — while AWS_PROFILE was still derived from the bundle. The session
217
+ // got a pointer to a profile that did not exist:
218
+ //
219
+ // The config profile (agent-dev) could not be found
176
220
  //
177
- // These are whole-file writes, so a pre-existing credentials file a
178
- // developer's own profiles, or something another tool set up would be
179
- // destroyed rather than merged. This only runs on a surface allowed to write
180
- // secrets to disk (a disposable container), but "usually disposable" is not a
181
- // reason to be able to delete someone's credentials. The marker makes our own
182
- // file re-writable while anything else is left alone and reported.
183
- for (const name of ["credentials", "config"]) {
221
+ // Merging keeps the operator's own sections intact AND writes ours, which is
222
+ // what the guard was actually for. Same delimited-block approach as the shell
223
+ // profile, and `#` is a comment in the shared-config format so the markers are
224
+ // inert to every consumer.
225
+ for (const [name, body] of [
226
+ ["credentials", rendered.credentials],
227
+ ["config", rendered.config],
228
+ ]) {
184
229
  const file = join(dir, name);
185
- if (exists(file) && !String(read(file, "utf8")).includes(MANAGED_MARKER)) {
186
- return [];
187
- }
230
+ const current = exists(file) ? String(read(file, "utf8")) : "";
231
+ write(file, upsertManagedBlock(current, body));
188
232
  }
189
233
 
190
- mkdir(dir, { recursive: true, mode: 0o700 });
191
- chmod(dir, 0o700);
192
- write(
193
- join(dir, "credentials"),
194
- `${MANAGED_MARKER}\n${rendered.credentials}`,
195
- {
196
- mode: 0o600,
197
- }
198
- );
199
- write(join(dir, "config"), `${MANAGED_MARKER}\n${rendered.config}`, {
200
- mode: 0o600,
201
- });
202
-
203
234
  return rendered.profiles;
204
235
  }
205
236
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.333.1",
3
+ "version": "2.333.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.333.1",
3
+ "version": "2.333.2",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.333.1",
3
+ "version": "2.333.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.333.1",
3
+ "version": "2.333.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.333.1",
3
+ "version": "2.333.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.333.1",
3
+ "version": "2.333.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -80,7 +80,41 @@ const PROFILE_END = "# <<< lisa secrets (managed) <<<";
80
80
  * consumer while still being the thing that distinguishes "our file, refresh
81
81
  * it" from "someone else's file, leave it alone".
82
82
  */
83
- const MANAGED_MARKER = "# managed by lisa-secrets-access";
83
+ const MANAGED_MARKER = "# >>> managed by lisa-secrets-access >>>";
84
+
85
+ /** Closes the managed region of an `~/.aws` file. */
86
+ const MANAGED_END = "# <<< managed by lisa-secrets-access <<<";
87
+
88
+ /**
89
+ * Replace this module's delimited region in a file, preserving everything else.
90
+ *
91
+ * Written as a merge rather than a whole-file write because both `~/.aws` files
92
+ * routinely hold sections nobody here knows about — an operator's own profiles,
93
+ * or a container's bare `[default]`. Refusing on their account wrote nothing at
94
+ * all; overwriting would delete them. This does neither.
95
+ * @param {string} current Existing file contents, or "".
96
+ * @param {string} body The region this module owns.
97
+ * @returns {string} The merged file.
98
+ */
99
+ export function upsertManagedBlock(current, body) {
100
+ const block = `${MANAGED_MARKER}\n${body.trimEnd()}\n${MANAGED_END}`;
101
+ const start = current.indexOf(MANAGED_MARKER);
102
+
103
+ if (start === -1) {
104
+ const prefix =
105
+ current && !current.endsWith("\n") ? `${current}\n` : current;
106
+ return `${prefix}${prefix ? "\n" : ""}${block}\n`;
107
+ }
108
+
109
+ const endAt = current.indexOf(MANAGED_END, start);
110
+ // A truncated block (marker opened, never closed) would otherwise swallow the
111
+ // rest of the file on every subsequent write.
112
+ const after =
113
+ endAt === -1
114
+ ? ""
115
+ : current.slice(endAt + MANAGED_END.length).replace(/^\n/, "");
116
+ return `${current.slice(0, start)}${block}\n${after}`;
117
+ }
84
118
 
85
119
  /**
86
120
  * Make every shell in this container load the materialized secrets.
@@ -161,7 +195,7 @@ export function installAwsProfiles(bundle, options = {}) {
161
195
  const {
162
196
  home = process.env.HOME || homedir(),
163
197
  mkdir = mkdirSync,
164
- write = writeFileSync,
198
+ write = writeAtomic,
165
199
  read = readFileSync,
166
200
  exists = existsSync,
167
201
  chmod = chmodSync,
@@ -171,35 +205,32 @@ export function installAwsProfiles(bundle, options = {}) {
171
205
  if (!rendered) return [];
172
206
 
173
207
  const dir = join(home, ".aws");
208
+ mkdir(dir, { recursive: true, mode: 0o700 });
209
+ chmod(dir, 0o700);
174
210
 
175
- // Never overwrite an ~/.aws this did not write.
211
+ // Merge into a delimited block; never replace the file.
212
+ //
213
+ // Refusing whole files whenever one already existed sounded safe and was
214
+ // worse than useless: a container ships `~/.aws/config` containing a bare
215
+ // `[default]`, so the guard fired every time, wrote nothing, and returned
216
+ // silently — while AWS_PROFILE was still derived from the bundle. The session
217
+ // got a pointer to a profile that did not exist:
218
+ //
219
+ // The config profile (agent-dev) could not be found
176
220
  //
177
- // These are whole-file writes, so a pre-existing credentials file a
178
- // developer's own profiles, or something another tool set up would be
179
- // destroyed rather than merged. This only runs on a surface allowed to write
180
- // secrets to disk (a disposable container), but "usually disposable" is not a
181
- // reason to be able to delete someone's credentials. The marker makes our own
182
- // file re-writable while anything else is left alone and reported.
183
- for (const name of ["credentials", "config"]) {
221
+ // Merging keeps the operator's own sections intact AND writes ours, which is
222
+ // what the guard was actually for. Same delimited-block approach as the shell
223
+ // profile, and `#` is a comment in the shared-config format so the markers are
224
+ // inert to every consumer.
225
+ for (const [name, body] of [
226
+ ["credentials", rendered.credentials],
227
+ ["config", rendered.config],
228
+ ]) {
184
229
  const file = join(dir, name);
185
- if (exists(file) && !String(read(file, "utf8")).includes(MANAGED_MARKER)) {
186
- return [];
187
- }
230
+ const current = exists(file) ? String(read(file, "utf8")) : "";
231
+ write(file, upsertManagedBlock(current, body));
188
232
  }
189
233
 
190
- mkdir(dir, { recursive: true, mode: 0o700 });
191
- chmod(dir, 0o700);
192
- write(
193
- join(dir, "credentials"),
194
- `${MANAGED_MARKER}\n${rendered.credentials}`,
195
- {
196
- mode: 0o600,
197
- }
198
- );
199
- write(join(dir, "config"), `${MANAGED_MARKER}\n${rendered.config}`, {
200
- mode: 0o600,
201
- });
202
-
203
234
  return rendered.profiles;
204
235
  }
205
236
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.333.1",
3
+ "version": "2.333.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -80,7 +80,41 @@ const PROFILE_END = "# <<< lisa secrets (managed) <<<";
80
80
  * consumer while still being the thing that distinguishes "our file, refresh
81
81
  * it" from "someone else's file, leave it alone".
82
82
  */
83
- const MANAGED_MARKER = "# managed by lisa-secrets-access";
83
+ const MANAGED_MARKER = "# >>> managed by lisa-secrets-access >>>";
84
+
85
+ /** Closes the managed region of an `~/.aws` file. */
86
+ const MANAGED_END = "# <<< managed by lisa-secrets-access <<<";
87
+
88
+ /**
89
+ * Replace this module's delimited region in a file, preserving everything else.
90
+ *
91
+ * Written as a merge rather than a whole-file write because both `~/.aws` files
92
+ * routinely hold sections nobody here knows about — an operator's own profiles,
93
+ * or a container's bare `[default]`. Refusing on their account wrote nothing at
94
+ * all; overwriting would delete them. This does neither.
95
+ * @param {string} current Existing file contents, or "".
96
+ * @param {string} body The region this module owns.
97
+ * @returns {string} The merged file.
98
+ */
99
+ export function upsertManagedBlock(current, body) {
100
+ const block = `${MANAGED_MARKER}\n${body.trimEnd()}\n${MANAGED_END}`;
101
+ const start = current.indexOf(MANAGED_MARKER);
102
+
103
+ if (start === -1) {
104
+ const prefix =
105
+ current && !current.endsWith("\n") ? `${current}\n` : current;
106
+ return `${prefix}${prefix ? "\n" : ""}${block}\n`;
107
+ }
108
+
109
+ const endAt = current.indexOf(MANAGED_END, start);
110
+ // A truncated block (marker opened, never closed) would otherwise swallow the
111
+ // rest of the file on every subsequent write.
112
+ const after =
113
+ endAt === -1
114
+ ? ""
115
+ : current.slice(endAt + MANAGED_END.length).replace(/^\n/, "");
116
+ return `${current.slice(0, start)}${block}\n${after}`;
117
+ }
84
118
 
85
119
  /**
86
120
  * Make every shell in this container load the materialized secrets.
@@ -161,7 +195,7 @@ export function installAwsProfiles(bundle, options = {}) {
161
195
  const {
162
196
  home = process.env.HOME || homedir(),
163
197
  mkdir = mkdirSync,
164
- write = writeFileSync,
198
+ write = writeAtomic,
165
199
  read = readFileSync,
166
200
  exists = existsSync,
167
201
  chmod = chmodSync,
@@ -171,35 +205,32 @@ export function installAwsProfiles(bundle, options = {}) {
171
205
  if (!rendered) return [];
172
206
 
173
207
  const dir = join(home, ".aws");
208
+ mkdir(dir, { recursive: true, mode: 0o700 });
209
+ chmod(dir, 0o700);
174
210
 
175
- // Never overwrite an ~/.aws this did not write.
211
+ // Merge into a delimited block; never replace the file.
212
+ //
213
+ // Refusing whole files whenever one already existed sounded safe and was
214
+ // worse than useless: a container ships `~/.aws/config` containing a bare
215
+ // `[default]`, so the guard fired every time, wrote nothing, and returned
216
+ // silently — while AWS_PROFILE was still derived from the bundle. The session
217
+ // got a pointer to a profile that did not exist:
218
+ //
219
+ // The config profile (agent-dev) could not be found
176
220
  //
177
- // These are whole-file writes, so a pre-existing credentials file a
178
- // developer's own profiles, or something another tool set up would be
179
- // destroyed rather than merged. This only runs on a surface allowed to write
180
- // secrets to disk (a disposable container), but "usually disposable" is not a
181
- // reason to be able to delete someone's credentials. The marker makes our own
182
- // file re-writable while anything else is left alone and reported.
183
- for (const name of ["credentials", "config"]) {
221
+ // Merging keeps the operator's own sections intact AND writes ours, which is
222
+ // what the guard was actually for. Same delimited-block approach as the shell
223
+ // profile, and `#` is a comment in the shared-config format so the markers are
224
+ // inert to every consumer.
225
+ for (const [name, body] of [
226
+ ["credentials", rendered.credentials],
227
+ ["config", rendered.config],
228
+ ]) {
184
229
  const file = join(dir, name);
185
- if (exists(file) && !String(read(file, "utf8")).includes(MANAGED_MARKER)) {
186
- return [];
187
- }
230
+ const current = exists(file) ? String(read(file, "utf8")) : "";
231
+ write(file, upsertManagedBlock(current, body));
188
232
  }
189
233
 
190
- mkdir(dir, { recursive: true, mode: 0o700 });
191
- chmod(dir, 0o700);
192
- write(
193
- join(dir, "credentials"),
194
- `${MANAGED_MARKER}\n${rendered.credentials}`,
195
- {
196
- mode: 0o600,
197
- }
198
- );
199
- write(join(dir, "config"), `${MANAGED_MARKER}\n${rendered.config}`, {
200
- mode: 0o600,
201
- });
202
-
203
234
  return rendered.profiles;
204
235
  }
205
236