@codyswann/lisa 4.72.5 → 4.72.6
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/worktree-binding-guard.mjs +176 -53
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +4 -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 +7 -4
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/nestjs/package-lisa/package.lisa.json +1 -1
- package/package.json +6 -6
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/worktree-binding-guard.mjs +176 -53
- package/plugins/lisa-agy/plugin.json +1 -1
- 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/worktree-binding-guard.mjs +176 -53
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +176 -53
- 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/worktree-binding-guard.mjs +176 -53
- package/typescript/package-lisa/package.lisa.json +2 -2
|
@@ -39,14 +39,16 @@
|
|
|
39
39
|
* or accept a linked target this session was explicitly offered. A selected
|
|
40
40
|
* target does not prove a move: the next guarded call must actually be there.
|
|
41
41
|
*
|
|
42
|
-
* FAILING OPEN, LOUDLY.
|
|
43
|
-
* cwd, not a git repository, an unreadable state file —
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
42
|
+
* FAILING OPEN, LOUDLY. Missing input or unavailable I/O — no session id, no
|
|
43
|
+
* cwd, not a git repository, an unreadable state file — retains the measured
|
|
44
|
+
* fail-open behavior. A readable state file with missing, mismatched or damaged
|
|
45
|
+
* ownership is a different fact: it cannot authorize a baseline or be silently
|
|
46
|
+
* overwritten. Legacy ambiguity requires explicit intent; a conflicting reserved
|
|
47
|
+
* slot requires a new session identity.
|
|
47
48
|
*/
|
|
48
49
|
|
|
49
50
|
import { execFileSync } from "node:child_process";
|
|
51
|
+
import { createHash } from "node:crypto";
|
|
50
52
|
import {
|
|
51
53
|
mkdirSync,
|
|
52
54
|
readFileSync,
|
|
@@ -69,6 +71,10 @@ const GUARDED_TOOLS = new Set(["Bash", "Write", "Edit", "MultiEdit"]);
|
|
|
69
71
|
*/
|
|
70
72
|
const ACCEPT_PREFIX = "lisa-worktree-binding: accept";
|
|
71
73
|
const ACK_AND_SEPARATOR = Symbol("acknowledgement AND separator");
|
|
74
|
+
const BINDING_VERSION = 4;
|
|
75
|
+
const CURRENT_BINDING = Symbol("current binding state");
|
|
76
|
+
const UNAVAILABLE_BINDING = Symbol("unavailable binding read");
|
|
77
|
+
const BINDING_WRITE_CONFLICT = Symbol("binding write conflict");
|
|
72
78
|
|
|
73
79
|
/**
|
|
74
80
|
* Resolve a path the way git reports one.
|
|
@@ -583,62 +589,114 @@ function keyPart(value) {
|
|
|
583
589
|
* its writes were refused, or an acknowledgement rebound the file out from
|
|
584
590
|
* under the sibling (CodySwannGT/lisa#4277). The payload's `agent_id` is what
|
|
585
591
|
* distinguishes a subagent from the main agent, so a caller carrying one gets
|
|
586
|
-
* its own state file. The main agent carries no `agent_id` and
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
592
|
+
* its own state file. The main agent carries no `agent_id` and uses a null
|
|
593
|
+
* agent component, also the fallback for runtimes that never send the field.
|
|
594
|
+
*
|
|
595
|
+
* v4 hashes the JSON tuple into a lowercase, bounded filename in its own
|
|
596
|
+
* directory. The digest fixes filename size and casing; the persisted exact
|
|
597
|
+
* tuple remains the authority. A conflicting owner in a reserved slot is
|
|
598
|
+
* refused, never overwritten. Earlier flat filenames were ambiguous across
|
|
599
|
+
* formats (CodySwannGT/lisa#4311).
|
|
600
|
+
* @param {object} payload - Native hook identity
|
|
601
|
+
* @returns {[string, string|null]} Session and normalized optional agent
|
|
596
602
|
*/
|
|
597
|
-
function
|
|
603
|
+
function bindingIdentity(payload) {
|
|
598
604
|
const agent = payload?.agent_id;
|
|
599
|
-
|
|
600
|
-
|
|
605
|
+
return [
|
|
606
|
+
payload.session_id,
|
|
607
|
+
typeof agent === "string" && agent ? agent : null,
|
|
608
|
+
];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Resolve the reserved slot without exposing filename-unsafe identity bytes.
|
|
613
|
+
* @param {object} payload - Native hook identity
|
|
614
|
+
* @returns {string} Versioned, bounded relative state-file stem
|
|
615
|
+
*/
|
|
616
|
+
function bindingKey(payload) {
|
|
617
|
+
const digest = createHash("sha256")
|
|
618
|
+
.update(JSON.stringify(bindingIdentity(payload)))
|
|
619
|
+
.digest("hex");
|
|
620
|
+
return join(`v${BINDING_VERSION}`, digest);
|
|
601
621
|
}
|
|
602
622
|
|
|
603
623
|
/**
|
|
604
624
|
* The keys earlier versions wrote for this payload, newest first.
|
|
605
625
|
*
|
|
606
626
|
* The v1 key was the raw `session_id` (single shared file per session). v2
|
|
607
|
-
* joined
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
627
|
+
* joined sanitized session and agent on `--`; v3 escaped both components.
|
|
628
|
+
* All are inspected on upgrade, but only a matching v4 owner record grants
|
|
629
|
+
* authority. An unstamped file cannot distinguish its possible owners, so the
|
|
630
|
+
* guard requires explicit intent rather than guessing or silently re-baselining.
|
|
631
|
+
* Legacy files stay untouched and only safe filename stems are inspected.
|
|
612
632
|
* @param {object} payload - The hook payload
|
|
613
633
|
* @returns {string[]} Zero or more legacy keys this payload may have written
|
|
614
634
|
*/
|
|
615
635
|
function legacyKeys(payload) {
|
|
616
636
|
const agent = payload?.agent_id;
|
|
617
637
|
const session = payload.session_id;
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
638
|
+
const perAgent = typeof agent === "string" && agent;
|
|
639
|
+
const san = value => value.replaceAll(/[^A-Za-z0-9._-]/g, "_");
|
|
640
|
+
const older = perAgent ? `${san(session)}--${san(agent)}` : session;
|
|
641
|
+
let escaped = null;
|
|
642
|
+
try {
|
|
643
|
+
escaped = perAgent
|
|
644
|
+
? `${keyPart(session)}--${keyPart(agent)}`
|
|
645
|
+
: keyPart(session);
|
|
646
|
+
} catch {
|
|
647
|
+
// A lone surrogate could not have produced an escaped v3 filename.
|
|
624
648
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
649
|
+
return [...new Set([escaped, older])].filter(
|
|
650
|
+
key => typeof key === "string" && key && !/[\\/]/u.test(key)
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Require the producer's version, exact tuple and an absolute binding root.
|
|
656
|
+
* A matching identity with a damaged root cannot authorize re-baselining.
|
|
657
|
+
* @param {object|null} state - Parsed local state
|
|
658
|
+
* @param {object} payload - Caller claiming the slot
|
|
659
|
+
* @returns {boolean} Whether the record identifies this caller
|
|
660
|
+
*/
|
|
661
|
+
function ownsState(state, payload) {
|
|
662
|
+
const expected = bindingIdentity(payload);
|
|
663
|
+
return (
|
|
664
|
+
typeof state?.boundRoot === "string" &&
|
|
665
|
+
isAbsolute(state.boundRoot) &&
|
|
666
|
+
!state.boundRoot.includes("\0") &&
|
|
667
|
+
state?.bindingVersion === BINDING_VERSION &&
|
|
668
|
+
Array.isArray(state.bindingIdentity) &&
|
|
669
|
+
state.bindingIdentity.length === expected.length &&
|
|
670
|
+
state.bindingIdentity.every((part, index) => part === expected[index])
|
|
671
|
+
);
|
|
631
672
|
}
|
|
632
673
|
|
|
633
674
|
function readState(bindingKeyValue, payload) {
|
|
634
|
-
|
|
675
|
+
try {
|
|
676
|
+
const state = JSON.parse(readFileSync(stateFile(bindingKeyValue), "utf8"));
|
|
677
|
+
return ownsState(state, payload)
|
|
678
|
+
? { ...state, [CURRENT_BINDING]: true }
|
|
679
|
+
: { unverifiedOwner: true, canonicalConflict: true };
|
|
680
|
+
} catch (error) {
|
|
681
|
+
if (error instanceof SyntaxError)
|
|
682
|
+
return { unverifiedOwner: true, canonicalConflict: true };
|
|
683
|
+
// Unavailable I/O proves neither absence nor conflicting ownership. Keep
|
|
684
|
+
// it separate so an exclusive write cannot mistake EACCES for a race.
|
|
685
|
+
if (error.code !== "ENOENT")
|
|
686
|
+
return { [UNAVAILABLE_BINDING]: error.message };
|
|
687
|
+
}
|
|
688
|
+
let ambiguous = false;
|
|
689
|
+
for (const key of legacyKeys(payload)) {
|
|
635
690
|
try {
|
|
636
|
-
|
|
637
|
-
|
|
691
|
+
const state = JSON.parse(readFileSync(stateFile(key), "utf8"));
|
|
692
|
+
if (ownsState(state, payload)) return state;
|
|
693
|
+
ambiguous = true;
|
|
694
|
+
} catch (error) {
|
|
695
|
+
if (error instanceof SyntaxError) ambiguous = true;
|
|
638
696
|
// Not written under this key — try the next older one.
|
|
639
697
|
}
|
|
640
698
|
}
|
|
641
|
-
return null;
|
|
699
|
+
return ambiguous ? { unverifiedOwner: true } : null;
|
|
642
700
|
}
|
|
643
701
|
|
|
644
702
|
/**
|
|
@@ -654,23 +712,45 @@ function readState(bindingKeyValue, payload) {
|
|
|
654
712
|
* @param state - Fields to write over the recorded state
|
|
655
713
|
* @param payload - The hook payload, for legacy-key reads while a binding is
|
|
656
714
|
* migrating to the current key format
|
|
657
|
-
* @
|
|
715
|
+
* @param options - Validated acknowledgement or create-only baseline intent
|
|
716
|
+
* @returns True on success, false for unavailable I/O, or a conflict sentinel
|
|
658
717
|
*/
|
|
659
|
-
function writeState(bindingKeyValue, state, payload) {
|
|
718
|
+
function writeState(bindingKeyValue, state, payload, options = {}) {
|
|
660
719
|
const file = stateFile(bindingKeyValue);
|
|
661
720
|
const previous = readState(bindingKeyValue, payload);
|
|
721
|
+
if (previous?.[UNAVAILABLE_BINDING]) {
|
|
722
|
+
say(
|
|
723
|
+
`could not read the binding (${previous[UNAVAILABLE_BINDING]}); NOT enforcing`
|
|
724
|
+
);
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
if (
|
|
728
|
+
(options.createOnly && previous) ||
|
|
729
|
+
previous?.canonicalConflict ||
|
|
730
|
+
(previous?.unverifiedOwner && !options.acceptUnowned)
|
|
731
|
+
)
|
|
732
|
+
return BINDING_WRITE_CONFLICT;
|
|
733
|
+
const proven = previous?.unverifiedOwner ? null : previous;
|
|
734
|
+
// A runtime notice alone cannot establish a binding after a failed baseline.
|
|
735
|
+
// Every record we produce must satisfy the same authority check as a read.
|
|
736
|
+
const next = {
|
|
737
|
+
...proven,
|
|
738
|
+
...state,
|
|
739
|
+
bindingVersion: BINDING_VERSION,
|
|
740
|
+
bindingIdentity: bindingIdentity(payload),
|
|
741
|
+
};
|
|
742
|
+
if (!ownsState(next, payload)) return false;
|
|
662
743
|
const originalRoot =
|
|
663
|
-
|
|
744
|
+
proven?.originalRoot ?? proven?.boundRoot ?? state.boundRoot;
|
|
664
745
|
const originalCommonDir =
|
|
665
|
-
|
|
746
|
+
proven?.originalCommonDir ?? repositoryIdentity(originalRoot);
|
|
666
747
|
try {
|
|
667
748
|
mkdirSync(dirname(file), { recursive: true });
|
|
668
749
|
writeFileSync(
|
|
669
750
|
file,
|
|
670
751
|
`${JSON.stringify(
|
|
671
752
|
{
|
|
672
|
-
...
|
|
673
|
-
...state,
|
|
753
|
+
...next,
|
|
674
754
|
// Preserve the initial root after intentional rebinds. A legacy
|
|
675
755
|
// state's established root is its only proven recovery anchor.
|
|
676
756
|
originalRoot,
|
|
@@ -678,10 +758,12 @@ function writeState(bindingKeyValue, state, payload) {
|
|
|
678
758
|
},
|
|
679
759
|
null,
|
|
680
760
|
2
|
|
681
|
-
)}\n
|
|
761
|
+
)}\n`,
|
|
762
|
+
{ flag: previous?.[CURRENT_BINDING] ? "w" : "wx" }
|
|
682
763
|
);
|
|
683
764
|
return true;
|
|
684
765
|
} catch (error) {
|
|
766
|
+
if (error.code === "EEXIST") return BINDING_WRITE_CONFLICT;
|
|
685
767
|
say(`could not record the binding (${error.message}); NOT enforcing`);
|
|
686
768
|
return false;
|
|
687
769
|
}
|
|
@@ -761,7 +843,8 @@ function recordBaseline(bindingKeyValue, observed, payload) {
|
|
|
761
843
|
// Never overwrite. A resumed session, a reconnect, or anything else that
|
|
762
844
|
// fires the event twice must not be able to launder a displaced binding into
|
|
763
845
|
// a fresh baseline — which is the whole failure this exists to close.
|
|
764
|
-
|
|
846
|
+
const previous = readState(bindingKeyValue, payload);
|
|
847
|
+
if (previous?.boundRoot || previous?.unverifiedOwner) return;
|
|
765
848
|
writeState(
|
|
766
849
|
bindingKeyValue,
|
|
767
850
|
{
|
|
@@ -769,7 +852,8 @@ function recordBaseline(bindingKeyValue, observed, payload) {
|
|
|
769
852
|
claimedRoot: null,
|
|
770
853
|
updatedAt: new Date().toISOString(),
|
|
771
854
|
},
|
|
772
|
-
payload
|
|
855
|
+
payload,
|
|
856
|
+
{ createOnly: true }
|
|
773
857
|
);
|
|
774
858
|
}
|
|
775
859
|
|
|
@@ -785,6 +869,7 @@ function recordClaim(payload, observed) {
|
|
|
785
869
|
const claimed = claimedRoot(payload.tool_input, payload.cwd);
|
|
786
870
|
if (!claimed || claimed === observed) return;
|
|
787
871
|
const previous = readState(bindingKey(payload), payload);
|
|
872
|
+
if (previous?.unverifiedOwner) return;
|
|
788
873
|
writeState(
|
|
789
874
|
bindingKey(payload),
|
|
790
875
|
{
|
|
@@ -792,7 +877,8 @@ function recordClaim(payload, observed) {
|
|
|
792
877
|
claimedRoot: claimed,
|
|
793
878
|
updatedAt: new Date().toISOString(),
|
|
794
879
|
},
|
|
795
|
-
payload
|
|
880
|
+
payload,
|
|
881
|
+
{ createOnly: !previous }
|
|
796
882
|
);
|
|
797
883
|
}
|
|
798
884
|
|
|
@@ -801,11 +887,41 @@ function refuse(lines) {
|
|
|
801
887
|
return 2;
|
|
802
888
|
}
|
|
803
889
|
|
|
890
|
+
/** Refuse an action whose binding changed while its creation was in flight. */
|
|
891
|
+
function bindingWriteConflict() {
|
|
892
|
+
return refuse([
|
|
893
|
+
"worktree-binding-guard: another hook recorded this session's binding during this call.",
|
|
894
|
+
"The existing file has been preserved. Retry the call to check its established binding.",
|
|
895
|
+
]);
|
|
896
|
+
}
|
|
897
|
+
|
|
804
898
|
function acceptanceLine(observed) {
|
|
805
899
|
const literal = `${ACCEPT_PREFIX} ${observed}`.replaceAll("'", "'\\''");
|
|
806
900
|
return ` echo '${literal}'`;
|
|
807
901
|
}
|
|
808
902
|
|
|
903
|
+
/**
|
|
904
|
+
* Explain a real ownership ambiguity without treating a stranger's root as fact.
|
|
905
|
+
* @param {object} state - Unverified-state sentinel, containing no imported fields
|
|
906
|
+
* @param {string|null} observed - Measured checkout
|
|
907
|
+
* @returns {number} Blocking native-hook status
|
|
908
|
+
*/
|
|
909
|
+
function unownedBinding(state, observed) {
|
|
910
|
+
return refuse([
|
|
911
|
+
"worktree-binding-guard: cannot prove this binding belongs to this session.",
|
|
912
|
+
"The existing file has no matching owner record. It has not been imported or changed.",
|
|
913
|
+
...(state.canonicalConflict
|
|
914
|
+
? [
|
|
915
|
+
"This session's reserved state file identifies another owner or an unknown format.",
|
|
916
|
+
"Start a new session with its own identity; acknowledging cannot overwrite that file.",
|
|
917
|
+
]
|
|
918
|
+
: [
|
|
919
|
+
"Choose the worktree you intend to use by acknowledging its measured path:",
|
|
920
|
+
acceptanceLine(observed),
|
|
921
|
+
]),
|
|
922
|
+
]);
|
|
923
|
+
}
|
|
924
|
+
|
|
809
925
|
/** Read literal shell words and AND separators without evaluating expansions. */
|
|
810
926
|
function acknowledgementWords(text) {
|
|
811
927
|
const words = [];
|
|
@@ -923,6 +1039,7 @@ function handleAcceptance(payload, observed) {
|
|
|
923
1039
|
if (!request) return null;
|
|
924
1040
|
const { path, validMove } = request;
|
|
925
1041
|
const previous = readState(bindingKey(payload), payload);
|
|
1042
|
+
if (previous?.canonicalConflict) return unownedBinding(previous, observed);
|
|
926
1043
|
const original = previous?.originalRoot ?? previous?.boundRoot;
|
|
927
1044
|
const recovering = isOriginalRoot(previous, path);
|
|
928
1045
|
const offered =
|
|
@@ -949,8 +1066,10 @@ function handleAcceptance(payload, observed) {
|
|
|
949
1066
|
offeredRoot: null,
|
|
950
1067
|
updatedAt: new Date().toISOString(),
|
|
951
1068
|
},
|
|
952
|
-
payload
|
|
1069
|
+
payload,
|
|
1070
|
+
{ acceptUnowned: true }
|
|
953
1071
|
);
|
|
1072
|
+
if (written === BINDING_WRITE_CONFLICT) return bindingWriteConflict();
|
|
954
1073
|
if (written)
|
|
955
1074
|
say(
|
|
956
1075
|
path === observed
|
|
@@ -1185,6 +1304,7 @@ function evaluate(payload) {
|
|
|
1185
1304
|
if (!GUARDED_TOOLS.has(payload.tool_name)) return 0;
|
|
1186
1305
|
|
|
1187
1306
|
const state = readState(binding, payload);
|
|
1307
|
+
if (state?.unverifiedOwner) return unownedBinding(state, observed);
|
|
1188
1308
|
if (!state?.boundRoot) {
|
|
1189
1309
|
// No baseline means SessionStart did not run for this session — an older
|
|
1190
1310
|
// install, a runtime that fires no such event, a harness that skips it.
|
|
@@ -1193,15 +1313,17 @@ function evaluate(payload) {
|
|
|
1193
1313
|
// refusing. Treating it as a refusal would turn a floor into a wall on
|
|
1194
1314
|
// every surface where the event does not fire, and a wall gets switched
|
|
1195
1315
|
// off, which costs the whole guard.
|
|
1196
|
-
writeState(
|
|
1316
|
+
const written = writeState(
|
|
1197
1317
|
binding,
|
|
1198
1318
|
{
|
|
1199
1319
|
boundRoot: observed,
|
|
1200
1320
|
claimedRoot: null,
|
|
1201
1321
|
updatedAt: new Date().toISOString(),
|
|
1202
1322
|
},
|
|
1203
|
-
payload
|
|
1323
|
+
payload,
|
|
1324
|
+
{ createOnly: true }
|
|
1204
1325
|
);
|
|
1326
|
+
if (written === BINDING_WRITE_CONFLICT) return bindingWriteConflict();
|
|
1205
1327
|
return 0;
|
|
1206
1328
|
}
|
|
1207
1329
|
if (
|
|
@@ -1213,7 +1335,7 @@ function evaluate(payload) {
|
|
|
1213
1335
|
return unconfirmedSwitch(state.claimedRoot, observed);
|
|
1214
1336
|
}
|
|
1215
1337
|
if (state.claimedRoot === observed) {
|
|
1216
|
-
writeState(
|
|
1338
|
+
const written = writeState(
|
|
1217
1339
|
binding,
|
|
1218
1340
|
{
|
|
1219
1341
|
boundRoot: observed,
|
|
@@ -1223,6 +1345,7 @@ function evaluate(payload) {
|
|
|
1223
1345
|
},
|
|
1224
1346
|
payload
|
|
1225
1347
|
);
|
|
1348
|
+
if (written === BINDING_WRITE_CONFLICT) return bindingWriteConflict();
|
|
1226
1349
|
return 0;
|
|
1227
1350
|
}
|
|
1228
1351
|
if (state.boundRoot !== observed) return displaced(state.boundRoot, observed);
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@isaacs/brace-expansion": "^5.0.1",
|
|
43
43
|
"axios": ">=1.20.0",
|
|
44
44
|
"esbuild": ">=0.28.1",
|
|
45
|
-
"handlebars": ">=4.7.
|
|
45
|
+
"handlebars": ">=4.7.10",
|
|
46
46
|
"lodash": ">=4.18.1",
|
|
47
47
|
"multer": ">=2.3.0",
|
|
48
48
|
"undici": "^6.28.1",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@isaacs/brace-expansion": "^5.0.1",
|
|
58
58
|
"axios": ">=1.20.0",
|
|
59
59
|
"esbuild": ">=0.28.1",
|
|
60
|
-
"handlebars": ">=4.7.
|
|
60
|
+
"handlebars": ">=4.7.10",
|
|
61
61
|
"lodash": ">=4.18.1",
|
|
62
62
|
"multer": ">=2.3.0",
|
|
63
63
|
"undici": "^6.28.1",
|