@codyswann/lisa 4.54.7 → 4.54.9
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/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +18 -6
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +2 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +5 -3
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/block-managed-file-edits.sh +18 -6
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +99 -10
- package/plugins/lisa/hooks/block-managed-file-edits.sh +18 -6
- package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +99 -10
- package/plugins/lisa-agy/hooks/block-managed-file-edits.sh +18 -6
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +99 -10
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +18 -6
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +99 -10
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +18 -6
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +99 -10
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/block-managed-file-edits.sh +18 -6
- package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +99 -10
|
@@ -70,13 +70,25 @@
|
|
|
70
70
|
# propagate any further either — a path a followed script merely NAMES is data
|
|
71
71
|
# one file further out, not a third hop.
|
|
72
72
|
#
|
|
73
|
-
# ##
|
|
73
|
+
# ## Every agent surface reaches this guard
|
|
74
74
|
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
75
|
+
# There is no parity gap left to record. A note here used to record one for
|
|
76
|
+
# three surfaces; every one of those ports now exists, and the note outlived
|
|
77
|
+
# them. It is deleted rather than trimmed, because a stale gap note is
|
|
78
|
+
# worse than none: it OVERSTATES the gap, and a reader who trusts it goes
|
|
79
|
+
# looking for three missing ports, finds two present, and cannot tell which
|
|
80
|
+
# third is real without redoing the measurement from scratch. AGENTS.md asks
|
|
81
|
+
# for a gap to be documented instead of silently dropped — that only works
|
|
82
|
+
# while the document is true.
|
|
83
|
+
#
|
|
84
|
+
# Where each surface picks it up, so the next reader measures instead of
|
|
85
|
+
# guessing: Claude and Copilot from `.claude-plugin/plugin.json`, Cursor from
|
|
86
|
+
# `hooks/hooks.json`, Codex from `.codex-plugin/hooks.json` AND from the
|
|
87
|
+
# enforcement-fallback dispatcher, Antigravity from `hooks.json` via the
|
|
88
|
+
# `.agy.sh` adapter beside this file, and OpenCode from
|
|
89
|
+
# `src/opencode/plugin-templates/lisa-block-managed-file-edits.ts`.
|
|
90
|
+
# `tests/unit/hooks/managed-file-guard-parity-note.test.ts` fails if this
|
|
91
|
+
# paragraph goes stale in either direction.
|
|
80
92
|
#
|
|
81
93
|
# Exemptions (allowed):
|
|
82
94
|
# - `LISA_ALLOW_MANAGED_FILE_WRITE` set — the operator's explicit override,
|
package/package.json
CHANGED
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"zod-validation-error": "^4.0.0"
|
|
182
182
|
},
|
|
183
183
|
"name": "@codyswann/lisa",
|
|
184
|
-
"version": "4.54.
|
|
184
|
+
"version": "4.54.9",
|
|
185
185
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
186
186
|
"main": "dist/index.js",
|
|
187
187
|
"exports": {
|
|
@@ -329,7 +329,7 @@
|
|
|
329
329
|
"test": "tests"
|
|
330
330
|
},
|
|
331
331
|
"types": "./dist/index.d.ts",
|
|
332
|
-
"lisaReleaseCommit": "
|
|
333
|
-
"gitHead": "
|
|
334
|
-
"lisaReleaseTag": "v4.54.
|
|
332
|
+
"lisaReleaseCommit": "a2487db25e690de0b6220e88c01d999899fe52e8",
|
|
333
|
+
"gitHead": "a2487db25e690de0b6220e88c01d999899fe52e8",
|
|
334
|
+
"lisaReleaseTag": "v4.54.9"
|
|
335
335
|
}
|
|
@@ -13,6 +13,8 @@ import { listBootstrapFiles, readBootstrapFile } from "./bootstrap-store.mjs";
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
boundedChildOutput,
|
|
16
|
+
isChildTimeout,
|
|
17
|
+
SETUP_OPERATION_BUDGET_MS,
|
|
16
18
|
rethrowIfChildTimeout,
|
|
17
19
|
} from "../../lisa-setup-workstation/scripts/bounded-child.mjs";
|
|
18
20
|
|
|
@@ -393,7 +395,12 @@ export function fetchRaw(cfg) {
|
|
|
393
395
|
}
|
|
394
396
|
|
|
395
397
|
if (cfg.provider === "bitwarden") {
|
|
396
|
-
const raw = run(
|
|
398
|
+
const raw = run(
|
|
399
|
+
"bws",
|
|
400
|
+
["secret", "list", "--output", "json"],
|
|
401
|
+
env,
|
|
402
|
+
"reading secrets from Bitwarden"
|
|
403
|
+
);
|
|
397
404
|
return JSON.parse(raw || "[]").map(s => ({
|
|
398
405
|
key: s.key,
|
|
399
406
|
value: s.value,
|
|
@@ -407,7 +414,7 @@ export function fetchRaw(cfg) {
|
|
|
407
414
|
|
|
408
415
|
if (cfg.provider === "doppler") {
|
|
409
416
|
const args = ["secrets", "download", "--no-file", "--format", "json"];
|
|
410
|
-
const raw = run("doppler", args, env);
|
|
417
|
+
const raw = run("doppler", args, env, "reading secrets from Doppler");
|
|
411
418
|
return Object.entries(JSON.parse(raw || "{}")).map(([key, value]) => ({
|
|
412
419
|
key,
|
|
413
420
|
value: String(value),
|
|
@@ -429,12 +436,84 @@ export function fetchRaw(cfg) {
|
|
|
429
436
|
* @param {NodeJS.ProcessEnv} env Environment for the child.
|
|
430
437
|
* @returns {string} Captured stdout.
|
|
431
438
|
*/
|
|
432
|
-
function run(bin, args, env) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
439
|
+
function run(bin, args, env, operation = "talking to the secrets provider") {
|
|
440
|
+
try {
|
|
441
|
+
return boundedChildOutput(bin, args, {
|
|
442
|
+
encoding: "utf8",
|
|
443
|
+
env,
|
|
444
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
445
|
+
// THE OPERATION BUDGET, not the probe budget (#4045). Passing none fell
|
|
446
|
+
// back to `CHILD_BUDGET_MS`, which its own docstring calls "a hang
|
|
447
|
+
// detector, not a performance budget" — while the sibling budget's
|
|
448
|
+
// docstring names "secret-provider calls" explicitly. A round trip to a
|
|
449
|
+
// hosted vault over someone's network is neither a hang nor a local
|
|
450
|
+
// probe, and this is the one function every Bitwarden and Doppler call
|
|
451
|
+
// in the tree passes through.
|
|
452
|
+
timeout: PROVIDER_BUDGET_MS,
|
|
453
|
+
});
|
|
454
|
+
} catch (error) {
|
|
455
|
+
const timedOut = describeProviderTimeout(bin, operation, error);
|
|
456
|
+
// Rethrow UNCHANGED when it is not a timeout. Relabelling every failure as
|
|
457
|
+
// a deadline would be this defect with the sign flipped: a genuinely
|
|
458
|
+
// missing binary would then read as a slow one.
|
|
459
|
+
if (timedOut === null) throw error;
|
|
460
|
+
throw new Error(timedOut);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* The deadline a provider CLI call is given.
|
|
466
|
+
*
|
|
467
|
+
* `SETUP_OPERATION_BUDGET_MS` rather than `CHILD_BUDGET_MS`, because
|
|
468
|
+
* `bounded-child.mjs` documents that budget as covering "downloads, package
|
|
469
|
+
* installation, secret-provider calls, or a project-declared hook", and the
|
|
470
|
+
* probe budget as a hang detector. Re-exported under a local name so the value
|
|
471
|
+
* a message quotes and the value the call enforces cannot drift apart.
|
|
472
|
+
*/
|
|
473
|
+
export const PROVIDER_BUDGET_MS = SETUP_OPERATION_BUDGET_MS;
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* An operator-readable account of a provider CLI killed at its deadline.
|
|
477
|
+
*
|
|
478
|
+
* ## What the old message did
|
|
479
|
+
*
|
|
480
|
+
* The entire operator-facing output was `spawnSync bws ETIMEDOUT` — a binary
|
|
481
|
+
* name and an errno, which is the exact shape Node produces for `ENOENT`.
|
|
482
|
+
* MEASURED: every reader concluded the binary was missing or broken. In the
|
|
483
|
+
* reported session `bws 2.1.0` was installed and answering in 365–725 ms, a
|
|
484
|
+
* direct `bws secret list` succeeded moments later, and the operator was one
|
|
485
|
+
* step from routing live work to `blocked` over a working credential path.
|
|
486
|
+
*
|
|
487
|
+
* ## What this says instead, and why each part is there
|
|
488
|
+
*
|
|
489
|
+
* The operation, so the reader knows what was in flight. That the CLI RAN, in
|
|
490
|
+
* as many words, because that is the belief being corrected. The deadline and
|
|
491
|
+
* whose it is — an operator needs both: the number tells them whether a retry
|
|
492
|
+
* is plausible, and "Lisa's" tells them the vault reported nothing at all. A
|
|
493
|
+
* next step, because a message at a gate that a non-technical operator cannot
|
|
494
|
+
* act on is not finished.
|
|
495
|
+
*
|
|
496
|
+
* Returns null for anything that is not a killed child, so the caller rethrows
|
|
497
|
+
* the original untouched.
|
|
498
|
+
* @param {string} bin The provider executable that was run.
|
|
499
|
+
* @param {string} operation What the call was doing, in operator words.
|
|
500
|
+
* @param {unknown} error The caught value.
|
|
501
|
+
* @returns {string|null} The message, or null when this is not a timeout.
|
|
502
|
+
*/
|
|
503
|
+
export function describeProviderTimeout(bin, operation, error) {
|
|
504
|
+
if (!isChildTimeout(error)) return null;
|
|
505
|
+
const seconds = Math.round(PROVIDER_BUDGET_MS / 1000);
|
|
506
|
+
return (
|
|
507
|
+
`${operation}: the \`${bin}\` CLI ran for ${seconds}s and Lisa stopped it ` +
|
|
508
|
+
`at its own deadline. This is NOT a missing or broken \`${bin}\` — the ` +
|
|
509
|
+
`binary was found and started, which is what separates this from the ` +
|
|
510
|
+
`similar-looking "spawnSync ${bin} ENOENT" you get when it is absent. The ` +
|
|
511
|
+
`deadline is Lisa's, so the provider reported nothing either way: no ` +
|
|
512
|
+
`secret was read, and nothing says your vault is unreachable. Most often ` +
|
|
513
|
+
`this is transient network latency — retry the command, and if it keeps ` +
|
|
514
|
+
`hitting the deadline check that \`${bin}\` answers on its own before ` +
|
|
515
|
+
`concluding the credential path is unavailable.`
|
|
516
|
+
);
|
|
438
517
|
}
|
|
439
518
|
|
|
440
519
|
/**
|
|
@@ -621,7 +700,12 @@ export function writeSecret(cfg, id, value) {
|
|
|
621
700
|
const env = providerEnv(cfg);
|
|
622
701
|
|
|
623
702
|
if (cfg.provider === "bitwarden") {
|
|
624
|
-
run(
|
|
703
|
+
run(
|
|
704
|
+
"bws",
|
|
705
|
+
["secret", "edit", id, "--value", value],
|
|
706
|
+
env,
|
|
707
|
+
"updating a secret in Bitwarden"
|
|
708
|
+
);
|
|
625
709
|
return;
|
|
626
710
|
}
|
|
627
711
|
throw new Error(
|
|
@@ -687,7 +771,12 @@ export function removeCoordinationRecord(cfg, id) {
|
|
|
687
771
|
const env = providerEnv(cfg);
|
|
688
772
|
|
|
689
773
|
if (cfg.provider === "bitwarden") {
|
|
690
|
-
run(
|
|
774
|
+
run(
|
|
775
|
+
"bws",
|
|
776
|
+
["secret", "delete", id, "--output", "none"],
|
|
777
|
+
env,
|
|
778
|
+
"deleting a secret in Bitwarden"
|
|
779
|
+
);
|
|
691
780
|
return;
|
|
692
781
|
}
|
|
693
782
|
throw new Error(
|
|
@@ -70,13 +70,25 @@
|
|
|
70
70
|
# propagate any further either — a path a followed script merely NAMES is data
|
|
71
71
|
# one file further out, not a third hop.
|
|
72
72
|
#
|
|
73
|
-
# ##
|
|
73
|
+
# ## Every agent surface reaches this guard
|
|
74
74
|
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
75
|
+
# There is no parity gap left to record. A note here used to record one for
|
|
76
|
+
# three surfaces; every one of those ports now exists, and the note outlived
|
|
77
|
+
# them. It is deleted rather than trimmed, because a stale gap note is
|
|
78
|
+
# worse than none: it OVERSTATES the gap, and a reader who trusts it goes
|
|
79
|
+
# looking for three missing ports, finds two present, and cannot tell which
|
|
80
|
+
# third is real without redoing the measurement from scratch. AGENTS.md asks
|
|
81
|
+
# for a gap to be documented instead of silently dropped — that only works
|
|
82
|
+
# while the document is true.
|
|
83
|
+
#
|
|
84
|
+
# Where each surface picks it up, so the next reader measures instead of
|
|
85
|
+
# guessing: Claude and Copilot from `.claude-plugin/plugin.json`, Cursor from
|
|
86
|
+
# `hooks/hooks.json`, Codex from `.codex-plugin/hooks.json` AND from the
|
|
87
|
+
# enforcement-fallback dispatcher, Antigravity from `hooks.json` via the
|
|
88
|
+
# `.agy.sh` adapter beside this file, and OpenCode from
|
|
89
|
+
# `src/opencode/plugin-templates/lisa-block-managed-file-edits.ts`.
|
|
90
|
+
# `tests/unit/hooks/managed-file-guard-parity-note.test.ts` fails if this
|
|
91
|
+
# paragraph goes stale in either direction.
|
|
80
92
|
#
|
|
81
93
|
# Exemptions (allowed):
|
|
82
94
|
# - `LISA_ALLOW_MANAGED_FILE_WRITE` set — the operator's explicit override,
|
|
@@ -13,6 +13,8 @@ import { listBootstrapFiles, readBootstrapFile } from "./bootstrap-store.mjs";
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
boundedChildOutput,
|
|
16
|
+
isChildTimeout,
|
|
17
|
+
SETUP_OPERATION_BUDGET_MS,
|
|
16
18
|
rethrowIfChildTimeout,
|
|
17
19
|
} from "../../lisa-setup-workstation/scripts/bounded-child.mjs";
|
|
18
20
|
|
|
@@ -393,7 +395,12 @@ export function fetchRaw(cfg) {
|
|
|
393
395
|
}
|
|
394
396
|
|
|
395
397
|
if (cfg.provider === "bitwarden") {
|
|
396
|
-
const raw = run(
|
|
398
|
+
const raw = run(
|
|
399
|
+
"bws",
|
|
400
|
+
["secret", "list", "--output", "json"],
|
|
401
|
+
env,
|
|
402
|
+
"reading secrets from Bitwarden"
|
|
403
|
+
);
|
|
397
404
|
return JSON.parse(raw || "[]").map(s => ({
|
|
398
405
|
key: s.key,
|
|
399
406
|
value: s.value,
|
|
@@ -407,7 +414,7 @@ export function fetchRaw(cfg) {
|
|
|
407
414
|
|
|
408
415
|
if (cfg.provider === "doppler") {
|
|
409
416
|
const args = ["secrets", "download", "--no-file", "--format", "json"];
|
|
410
|
-
const raw = run("doppler", args, env);
|
|
417
|
+
const raw = run("doppler", args, env, "reading secrets from Doppler");
|
|
411
418
|
return Object.entries(JSON.parse(raw || "{}")).map(([key, value]) => ({
|
|
412
419
|
key,
|
|
413
420
|
value: String(value),
|
|
@@ -429,12 +436,84 @@ export function fetchRaw(cfg) {
|
|
|
429
436
|
* @param {NodeJS.ProcessEnv} env Environment for the child.
|
|
430
437
|
* @returns {string} Captured stdout.
|
|
431
438
|
*/
|
|
432
|
-
function run(bin, args, env) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
439
|
+
function run(bin, args, env, operation = "talking to the secrets provider") {
|
|
440
|
+
try {
|
|
441
|
+
return boundedChildOutput(bin, args, {
|
|
442
|
+
encoding: "utf8",
|
|
443
|
+
env,
|
|
444
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
445
|
+
// THE OPERATION BUDGET, not the probe budget (#4045). Passing none fell
|
|
446
|
+
// back to `CHILD_BUDGET_MS`, which its own docstring calls "a hang
|
|
447
|
+
// detector, not a performance budget" — while the sibling budget's
|
|
448
|
+
// docstring names "secret-provider calls" explicitly. A round trip to a
|
|
449
|
+
// hosted vault over someone's network is neither a hang nor a local
|
|
450
|
+
// probe, and this is the one function every Bitwarden and Doppler call
|
|
451
|
+
// in the tree passes through.
|
|
452
|
+
timeout: PROVIDER_BUDGET_MS,
|
|
453
|
+
});
|
|
454
|
+
} catch (error) {
|
|
455
|
+
const timedOut = describeProviderTimeout(bin, operation, error);
|
|
456
|
+
// Rethrow UNCHANGED when it is not a timeout. Relabelling every failure as
|
|
457
|
+
// a deadline would be this defect with the sign flipped: a genuinely
|
|
458
|
+
// missing binary would then read as a slow one.
|
|
459
|
+
if (timedOut === null) throw error;
|
|
460
|
+
throw new Error(timedOut);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* The deadline a provider CLI call is given.
|
|
466
|
+
*
|
|
467
|
+
* `SETUP_OPERATION_BUDGET_MS` rather than `CHILD_BUDGET_MS`, because
|
|
468
|
+
* `bounded-child.mjs` documents that budget as covering "downloads, package
|
|
469
|
+
* installation, secret-provider calls, or a project-declared hook", and the
|
|
470
|
+
* probe budget as a hang detector. Re-exported under a local name so the value
|
|
471
|
+
* a message quotes and the value the call enforces cannot drift apart.
|
|
472
|
+
*/
|
|
473
|
+
export const PROVIDER_BUDGET_MS = SETUP_OPERATION_BUDGET_MS;
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* An operator-readable account of a provider CLI killed at its deadline.
|
|
477
|
+
*
|
|
478
|
+
* ## What the old message did
|
|
479
|
+
*
|
|
480
|
+
* The entire operator-facing output was `spawnSync bws ETIMEDOUT` — a binary
|
|
481
|
+
* name and an errno, which is the exact shape Node produces for `ENOENT`.
|
|
482
|
+
* MEASURED: every reader concluded the binary was missing or broken. In the
|
|
483
|
+
* reported session `bws 2.1.0` was installed and answering in 365–725 ms, a
|
|
484
|
+
* direct `bws secret list` succeeded moments later, and the operator was one
|
|
485
|
+
* step from routing live work to `blocked` over a working credential path.
|
|
486
|
+
*
|
|
487
|
+
* ## What this says instead, and why each part is there
|
|
488
|
+
*
|
|
489
|
+
* The operation, so the reader knows what was in flight. That the CLI RAN, in
|
|
490
|
+
* as many words, because that is the belief being corrected. The deadline and
|
|
491
|
+
* whose it is — an operator needs both: the number tells them whether a retry
|
|
492
|
+
* is plausible, and "Lisa's" tells them the vault reported nothing at all. A
|
|
493
|
+
* next step, because a message at a gate that a non-technical operator cannot
|
|
494
|
+
* act on is not finished.
|
|
495
|
+
*
|
|
496
|
+
* Returns null for anything that is not a killed child, so the caller rethrows
|
|
497
|
+
* the original untouched.
|
|
498
|
+
* @param {string} bin The provider executable that was run.
|
|
499
|
+
* @param {string} operation What the call was doing, in operator words.
|
|
500
|
+
* @param {unknown} error The caught value.
|
|
501
|
+
* @returns {string|null} The message, or null when this is not a timeout.
|
|
502
|
+
*/
|
|
503
|
+
export function describeProviderTimeout(bin, operation, error) {
|
|
504
|
+
if (!isChildTimeout(error)) return null;
|
|
505
|
+
const seconds = Math.round(PROVIDER_BUDGET_MS / 1000);
|
|
506
|
+
return (
|
|
507
|
+
`${operation}: the \`${bin}\` CLI ran for ${seconds}s and Lisa stopped it ` +
|
|
508
|
+
`at its own deadline. This is NOT a missing or broken \`${bin}\` — the ` +
|
|
509
|
+
`binary was found and started, which is what separates this from the ` +
|
|
510
|
+
`similar-looking "spawnSync ${bin} ENOENT" you get when it is absent. The ` +
|
|
511
|
+
`deadline is Lisa's, so the provider reported nothing either way: no ` +
|
|
512
|
+
`secret was read, and nothing says your vault is unreachable. Most often ` +
|
|
513
|
+
`this is transient network latency — retry the command, and if it keeps ` +
|
|
514
|
+
`hitting the deadline check that \`${bin}\` answers on its own before ` +
|
|
515
|
+
`concluding the credential path is unavailable.`
|
|
516
|
+
);
|
|
438
517
|
}
|
|
439
518
|
|
|
440
519
|
/**
|
|
@@ -621,7 +700,12 @@ export function writeSecret(cfg, id, value) {
|
|
|
621
700
|
const env = providerEnv(cfg);
|
|
622
701
|
|
|
623
702
|
if (cfg.provider === "bitwarden") {
|
|
624
|
-
run(
|
|
703
|
+
run(
|
|
704
|
+
"bws",
|
|
705
|
+
["secret", "edit", id, "--value", value],
|
|
706
|
+
env,
|
|
707
|
+
"updating a secret in Bitwarden"
|
|
708
|
+
);
|
|
625
709
|
return;
|
|
626
710
|
}
|
|
627
711
|
throw new Error(
|
|
@@ -687,7 +771,12 @@ export function removeCoordinationRecord(cfg, id) {
|
|
|
687
771
|
const env = providerEnv(cfg);
|
|
688
772
|
|
|
689
773
|
if (cfg.provider === "bitwarden") {
|
|
690
|
-
run(
|
|
774
|
+
run(
|
|
775
|
+
"bws",
|
|
776
|
+
["secret", "delete", id, "--output", "none"],
|
|
777
|
+
env,
|
|
778
|
+
"deleting a secret in Bitwarden"
|
|
779
|
+
);
|
|
691
780
|
return;
|
|
692
781
|
}
|
|
693
782
|
throw new Error(
|
|
@@ -70,13 +70,25 @@
|
|
|
70
70
|
# propagate any further either — a path a followed script merely NAMES is data
|
|
71
71
|
# one file further out, not a third hop.
|
|
72
72
|
#
|
|
73
|
-
# ##
|
|
73
|
+
# ## Every agent surface reaches this guard
|
|
74
74
|
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
75
|
+
# There is no parity gap left to record. A note here used to record one for
|
|
76
|
+
# three surfaces; every one of those ports now exists, and the note outlived
|
|
77
|
+
# them. It is deleted rather than trimmed, because a stale gap note is
|
|
78
|
+
# worse than none: it OVERSTATES the gap, and a reader who trusts it goes
|
|
79
|
+
# looking for three missing ports, finds two present, and cannot tell which
|
|
80
|
+
# third is real without redoing the measurement from scratch. AGENTS.md asks
|
|
81
|
+
# for a gap to be documented instead of silently dropped — that only works
|
|
82
|
+
# while the document is true.
|
|
83
|
+
#
|
|
84
|
+
# Where each surface picks it up, so the next reader measures instead of
|
|
85
|
+
# guessing: Claude and Copilot from `.claude-plugin/plugin.json`, Cursor from
|
|
86
|
+
# `hooks/hooks.json`, Codex from `.codex-plugin/hooks.json` AND from the
|
|
87
|
+
# enforcement-fallback dispatcher, Antigravity from `hooks.json` via the
|
|
88
|
+
# `.agy.sh` adapter beside this file, and OpenCode from
|
|
89
|
+
# `src/opencode/plugin-templates/lisa-block-managed-file-edits.ts`.
|
|
90
|
+
# `tests/unit/hooks/managed-file-guard-parity-note.test.ts` fails if this
|
|
91
|
+
# paragraph goes stale in either direction.
|
|
80
92
|
#
|
|
81
93
|
# Exemptions (allowed):
|
|
82
94
|
# - `LISA_ALLOW_MANAGED_FILE_WRITE` set — the operator's explicit override,
|
|
@@ -13,6 +13,8 @@ import { listBootstrapFiles, readBootstrapFile } from "./bootstrap-store.mjs";
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
boundedChildOutput,
|
|
16
|
+
isChildTimeout,
|
|
17
|
+
SETUP_OPERATION_BUDGET_MS,
|
|
16
18
|
rethrowIfChildTimeout,
|
|
17
19
|
} from "../../lisa-setup-workstation/scripts/bounded-child.mjs";
|
|
18
20
|
|
|
@@ -393,7 +395,12 @@ export function fetchRaw(cfg) {
|
|
|
393
395
|
}
|
|
394
396
|
|
|
395
397
|
if (cfg.provider === "bitwarden") {
|
|
396
|
-
const raw = run(
|
|
398
|
+
const raw = run(
|
|
399
|
+
"bws",
|
|
400
|
+
["secret", "list", "--output", "json"],
|
|
401
|
+
env,
|
|
402
|
+
"reading secrets from Bitwarden"
|
|
403
|
+
);
|
|
397
404
|
return JSON.parse(raw || "[]").map(s => ({
|
|
398
405
|
key: s.key,
|
|
399
406
|
value: s.value,
|
|
@@ -407,7 +414,7 @@ export function fetchRaw(cfg) {
|
|
|
407
414
|
|
|
408
415
|
if (cfg.provider === "doppler") {
|
|
409
416
|
const args = ["secrets", "download", "--no-file", "--format", "json"];
|
|
410
|
-
const raw = run("doppler", args, env);
|
|
417
|
+
const raw = run("doppler", args, env, "reading secrets from Doppler");
|
|
411
418
|
return Object.entries(JSON.parse(raw || "{}")).map(([key, value]) => ({
|
|
412
419
|
key,
|
|
413
420
|
value: String(value),
|
|
@@ -429,12 +436,84 @@ export function fetchRaw(cfg) {
|
|
|
429
436
|
* @param {NodeJS.ProcessEnv} env Environment for the child.
|
|
430
437
|
* @returns {string} Captured stdout.
|
|
431
438
|
*/
|
|
432
|
-
function run(bin, args, env) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
439
|
+
function run(bin, args, env, operation = "talking to the secrets provider") {
|
|
440
|
+
try {
|
|
441
|
+
return boundedChildOutput(bin, args, {
|
|
442
|
+
encoding: "utf8",
|
|
443
|
+
env,
|
|
444
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
445
|
+
// THE OPERATION BUDGET, not the probe budget (#4045). Passing none fell
|
|
446
|
+
// back to `CHILD_BUDGET_MS`, which its own docstring calls "a hang
|
|
447
|
+
// detector, not a performance budget" — while the sibling budget's
|
|
448
|
+
// docstring names "secret-provider calls" explicitly. A round trip to a
|
|
449
|
+
// hosted vault over someone's network is neither a hang nor a local
|
|
450
|
+
// probe, and this is the one function every Bitwarden and Doppler call
|
|
451
|
+
// in the tree passes through.
|
|
452
|
+
timeout: PROVIDER_BUDGET_MS,
|
|
453
|
+
});
|
|
454
|
+
} catch (error) {
|
|
455
|
+
const timedOut = describeProviderTimeout(bin, operation, error);
|
|
456
|
+
// Rethrow UNCHANGED when it is not a timeout. Relabelling every failure as
|
|
457
|
+
// a deadline would be this defect with the sign flipped: a genuinely
|
|
458
|
+
// missing binary would then read as a slow one.
|
|
459
|
+
if (timedOut === null) throw error;
|
|
460
|
+
throw new Error(timedOut);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* The deadline a provider CLI call is given.
|
|
466
|
+
*
|
|
467
|
+
* `SETUP_OPERATION_BUDGET_MS` rather than `CHILD_BUDGET_MS`, because
|
|
468
|
+
* `bounded-child.mjs` documents that budget as covering "downloads, package
|
|
469
|
+
* installation, secret-provider calls, or a project-declared hook", and the
|
|
470
|
+
* probe budget as a hang detector. Re-exported under a local name so the value
|
|
471
|
+
* a message quotes and the value the call enforces cannot drift apart.
|
|
472
|
+
*/
|
|
473
|
+
export const PROVIDER_BUDGET_MS = SETUP_OPERATION_BUDGET_MS;
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* An operator-readable account of a provider CLI killed at its deadline.
|
|
477
|
+
*
|
|
478
|
+
* ## What the old message did
|
|
479
|
+
*
|
|
480
|
+
* The entire operator-facing output was `spawnSync bws ETIMEDOUT` — a binary
|
|
481
|
+
* name and an errno, which is the exact shape Node produces for `ENOENT`.
|
|
482
|
+
* MEASURED: every reader concluded the binary was missing or broken. In the
|
|
483
|
+
* reported session `bws 2.1.0` was installed and answering in 365–725 ms, a
|
|
484
|
+
* direct `bws secret list` succeeded moments later, and the operator was one
|
|
485
|
+
* step from routing live work to `blocked` over a working credential path.
|
|
486
|
+
*
|
|
487
|
+
* ## What this says instead, and why each part is there
|
|
488
|
+
*
|
|
489
|
+
* The operation, so the reader knows what was in flight. That the CLI RAN, in
|
|
490
|
+
* as many words, because that is the belief being corrected. The deadline and
|
|
491
|
+
* whose it is — an operator needs both: the number tells them whether a retry
|
|
492
|
+
* is plausible, and "Lisa's" tells them the vault reported nothing at all. A
|
|
493
|
+
* next step, because a message at a gate that a non-technical operator cannot
|
|
494
|
+
* act on is not finished.
|
|
495
|
+
*
|
|
496
|
+
* Returns null for anything that is not a killed child, so the caller rethrows
|
|
497
|
+
* the original untouched.
|
|
498
|
+
* @param {string} bin The provider executable that was run.
|
|
499
|
+
* @param {string} operation What the call was doing, in operator words.
|
|
500
|
+
* @param {unknown} error The caught value.
|
|
501
|
+
* @returns {string|null} The message, or null when this is not a timeout.
|
|
502
|
+
*/
|
|
503
|
+
export function describeProviderTimeout(bin, operation, error) {
|
|
504
|
+
if (!isChildTimeout(error)) return null;
|
|
505
|
+
const seconds = Math.round(PROVIDER_BUDGET_MS / 1000);
|
|
506
|
+
return (
|
|
507
|
+
`${operation}: the \`${bin}\` CLI ran for ${seconds}s and Lisa stopped it ` +
|
|
508
|
+
`at its own deadline. This is NOT a missing or broken \`${bin}\` — the ` +
|
|
509
|
+
`binary was found and started, which is what separates this from the ` +
|
|
510
|
+
`similar-looking "spawnSync ${bin} ENOENT" you get when it is absent. The ` +
|
|
511
|
+
`deadline is Lisa's, so the provider reported nothing either way: no ` +
|
|
512
|
+
`secret was read, and nothing says your vault is unreachable. Most often ` +
|
|
513
|
+
`this is transient network latency — retry the command, and if it keeps ` +
|
|
514
|
+
`hitting the deadline check that \`${bin}\` answers on its own before ` +
|
|
515
|
+
`concluding the credential path is unavailable.`
|
|
516
|
+
);
|
|
438
517
|
}
|
|
439
518
|
|
|
440
519
|
/**
|
|
@@ -621,7 +700,12 @@ export function writeSecret(cfg, id, value) {
|
|
|
621
700
|
const env = providerEnv(cfg);
|
|
622
701
|
|
|
623
702
|
if (cfg.provider === "bitwarden") {
|
|
624
|
-
run(
|
|
703
|
+
run(
|
|
704
|
+
"bws",
|
|
705
|
+
["secret", "edit", id, "--value", value],
|
|
706
|
+
env,
|
|
707
|
+
"updating a secret in Bitwarden"
|
|
708
|
+
);
|
|
625
709
|
return;
|
|
626
710
|
}
|
|
627
711
|
throw new Error(
|
|
@@ -687,7 +771,12 @@ export function removeCoordinationRecord(cfg, id) {
|
|
|
687
771
|
const env = providerEnv(cfg);
|
|
688
772
|
|
|
689
773
|
if (cfg.provider === "bitwarden") {
|
|
690
|
-
run(
|
|
774
|
+
run(
|
|
775
|
+
"bws",
|
|
776
|
+
["secret", "delete", id, "--output", "none"],
|
|
777
|
+
env,
|
|
778
|
+
"deleting a secret in Bitwarden"
|
|
779
|
+
);
|
|
691
780
|
return;
|
|
692
781
|
}
|
|
693
782
|
throw new Error(
|