@goodandready/dsh-approval-gate 0.1.21 → 0.1.23
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/CHANGELOG.md +16 -0
- package/lib/audit-log.js +348 -345
- package/lib/dossier.js +122 -0
- package/lib/drift.js +214 -0
- package/lib/dualtrack.js +101 -0
- package/lib/engine.js +85 -81
- package/lib/escalation.js +151 -0
- package/lib/guidance.js +119 -0
- package/lib/index.js +50 -12
- package/lib/ledger.js +264 -164
- package/lib/lineage.js +182 -0
- package/lib/measure-kinds.js +3 -0
- package/lib/messages.js +27 -0
- package/lib/session-modes.js +146 -0
- package/package.json +2 -2
package/lib/ledger.js
CHANGED
|
@@ -1,164 +1,264 @@
|
|
|
1
|
-
// lib/ledger.js
|
|
2
|
-
// Quality and Anomaly Ledger for dsh-approval-gate (#88).
|
|
3
|
-
// Tracks session health, consecutive denials, and execution anomalies.
|
|
4
|
-
|
|
5
|
-
export const MAX_LEDGER_EVENTS_PER_SESSION = 50;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* @
|
|
18
|
-
* @
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
ledger
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
ledger.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
1
|
+
// lib/ledger.js
|
|
2
|
+
// Quality and Anomaly Ledger for dsh-approval-gate (#88, #73).
|
|
3
|
+
// Tracks session health, consecutive denials, and execution anomalies with schema versioning.
|
|
4
|
+
|
|
5
|
+
export const MAX_LEDGER_EVENTS_PER_SESSION = 50;
|
|
6
|
+
|
|
7
|
+
export const CURRENT_SCHEMA_VERSION = 3;
|
|
8
|
+
export const SUPPORTED_SCHEMA_VERSIONS = Object.freeze([1, 2, 3]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Migrates a verdict record across schema versions (v1 -> v2 -> v3) (#73).
|
|
12
|
+
* - v1: { id, verdict, ruleId, timestamp, riskBucket }
|
|
13
|
+
* - v2: + { reviewDecision, bandVerdict, mitigation, auditSummary }
|
|
14
|
+
* - v3: + { track, dossier, driftScore, driftWarning, schema_version: 3 }
|
|
15
|
+
*
|
|
16
|
+
* @param {Record<string, any>} record
|
|
17
|
+
* @param {number} [targetVersion=CURRENT_SCHEMA_VERSION]
|
|
18
|
+
* @returns {Record<string, any>}
|
|
19
|
+
*/
|
|
20
|
+
export function migrateVerdictRecord(record, targetVersion = CURRENT_SCHEMA_VERSION) {
|
|
21
|
+
if (!record || typeof record !== "object") return record;
|
|
22
|
+
|
|
23
|
+
let currentVersion = record.schema_version;
|
|
24
|
+
if (!currentVersion) {
|
|
25
|
+
// Infer version from fields
|
|
26
|
+
if (record.reviewDecision !== undefined || record.bandVerdict !== undefined) {
|
|
27
|
+
currentVersion = 2;
|
|
28
|
+
} else {
|
|
29
|
+
currentVersion = 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let migrated = { ...record };
|
|
34
|
+
|
|
35
|
+
// v1 -> v2 migration
|
|
36
|
+
if (currentVersion < 2 && targetVersion >= 2) {
|
|
37
|
+
migrated.reviewDecision = migrated.reviewDecision || migrated.verdict || "allow";
|
|
38
|
+
migrated.bandVerdict = migrated.bandVerdict || migrated.verdict || "allow";
|
|
39
|
+
migrated.mitigation = migrated.mitigation || null;
|
|
40
|
+
migrated.auditSummary = migrated.auditSummary || null;
|
|
41
|
+
migrated.schema_version = 2;
|
|
42
|
+
currentVersion = 2;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// v2 -> v3 migration
|
|
46
|
+
if (currentVersion < 3 && targetVersion >= 3) {
|
|
47
|
+
migrated.track = migrated.track || "external";
|
|
48
|
+
migrated.dossier = migrated.dossier || null;
|
|
49
|
+
migrated.driftScore = typeof migrated.driftScore === "number" ? migrated.driftScore : 0;
|
|
50
|
+
migrated.driftWarning = Boolean(migrated.driftWarning);
|
|
51
|
+
migrated.schema_version = 3;
|
|
52
|
+
currentVersion = 3;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
migrated.schema_version = currentVersion;
|
|
56
|
+
return migrated;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {Object} LedgerEvent
|
|
61
|
+
* @property {number} timestamp
|
|
62
|
+
* @property {string} type - "allow" | "deny" | "ask" | "exhaustion" | "breaker" | "error"
|
|
63
|
+
* @property {string} reason
|
|
64
|
+
* @property {string} [detail]
|
|
65
|
+
* @property {string} [tool]
|
|
66
|
+
* @property {number} [schema_version]
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {Object} SessionLedger
|
|
71
|
+
* @property {string} sessionId
|
|
72
|
+
* @property {number} totalDecisions
|
|
73
|
+
* @property {number} allowedCount
|
|
74
|
+
* @property {number} deniedCount
|
|
75
|
+
* @property {number} askCount
|
|
76
|
+
* @property {number} anomalyCount
|
|
77
|
+
* @property {number} consecutiveDenies
|
|
78
|
+
* @property {boolean} breakerTripped
|
|
79
|
+
* @property {boolean} retryExhausted
|
|
80
|
+
* @property {LedgerEvent[]} recentEvents
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
export class QualityLedger {
|
|
84
|
+
constructor() {
|
|
85
|
+
/** @type {Map<string, SessionLedger>} */
|
|
86
|
+
this.sessions = new Map();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
_getOrCreate(sessionId) {
|
|
93
|
+
const key = sessionId || "default";
|
|
94
|
+
let ledger = this.sessions.get(key);
|
|
95
|
+
if (!ledger) {
|
|
96
|
+
ledger = {
|
|
97
|
+
sessionId: key,
|
|
98
|
+
totalDecisions: 0,
|
|
99
|
+
allowedCount: 0,
|
|
100
|
+
deniedCount: 0,
|
|
101
|
+
askCount: 0,
|
|
102
|
+
anomalyCount: 0,
|
|
103
|
+
consecutiveDenies: 0,
|
|
104
|
+
breakerTripped: false,
|
|
105
|
+
retryExhausted: false,
|
|
106
|
+
recentEvents: [],
|
|
107
|
+
};
|
|
108
|
+
this.sessions.set(key, ledger);
|
|
109
|
+
}
|
|
110
|
+
return ledger;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Record a decision or system event in the ledger.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} sessionId
|
|
117
|
+
* @param {string} type - "allow" | "deny" | "ask" | "exhaustion" | "breaker" | "error"
|
|
118
|
+
* @param {{ reason?: string, detail?: string, tool?: string, track?: string, dossier?: any }} [details]
|
|
119
|
+
*/
|
|
120
|
+
record(sessionId, type, details = {}) {
|
|
121
|
+
const ledger = this._getOrCreate(sessionId);
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
|
|
124
|
+
const rawEvent = {
|
|
125
|
+
timestamp: now,
|
|
126
|
+
type,
|
|
127
|
+
reason: details.reason || "",
|
|
128
|
+
detail: details.detail ? String(details.detail).slice(0, 200) : undefined,
|
|
129
|
+
tool: details.tool || "",
|
|
130
|
+
track: details.track,
|
|
131
|
+
dossier: details.dossier,
|
|
132
|
+
schema_version: CURRENT_SCHEMA_VERSION,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const event = migrateVerdictRecord(rawEvent, CURRENT_SCHEMA_VERSION);
|
|
136
|
+
|
|
137
|
+
ledger.recentEvents.push(event);
|
|
138
|
+
if (ledger.recentEvents.length > MAX_LEDGER_EVENTS_PER_SESSION) {
|
|
139
|
+
ledger.recentEvents.shift();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (type === "allow") {
|
|
143
|
+
ledger.totalDecisions += 1;
|
|
144
|
+
ledger.allowedCount += 1;
|
|
145
|
+
ledger.consecutiveDenies = 0;
|
|
146
|
+
} else if (type === "deny") {
|
|
147
|
+
ledger.totalDecisions += 1;
|
|
148
|
+
ledger.deniedCount += 1;
|
|
149
|
+
ledger.consecutiveDenies += 1;
|
|
150
|
+
if (ledger.consecutiveDenies > 1) {
|
|
151
|
+
ledger.anomalyCount += 1;
|
|
152
|
+
}
|
|
153
|
+
} else if (type === "ask") {
|
|
154
|
+
ledger.totalDecisions += 1;
|
|
155
|
+
ledger.askCount += 1;
|
|
156
|
+
} else if (type === "exhaustion") {
|
|
157
|
+
ledger.retryExhausted = true;
|
|
158
|
+
ledger.anomalyCount += 1;
|
|
159
|
+
} else if (type === "breaker") {
|
|
160
|
+
ledger.breakerTripped = true;
|
|
161
|
+
ledger.anomalyCount += 1;
|
|
162
|
+
} else if (type === "error") {
|
|
163
|
+
ledger.anomalyCount += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Returns a quality summary for the session.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} sessionId
|
|
171
|
+
* @returns {{
|
|
172
|
+
* sessionId: string,
|
|
173
|
+
* totalDecisions: number,
|
|
174
|
+
* allowedCount: number,
|
|
175
|
+
* deniedCount: number,
|
|
176
|
+
* askCount: number,
|
|
177
|
+
* anomalyCount: number,
|
|
178
|
+
* consecutiveDenies: number,
|
|
179
|
+
* qualityScore: number,
|
|
180
|
+
* degradationWarning: boolean,
|
|
181
|
+
* recentEvents: LedgerEvent[]
|
|
182
|
+
* }}
|
|
183
|
+
*/
|
|
184
|
+
getSummary(sessionId) {
|
|
185
|
+
const ledger = this._getOrCreate(sessionId);
|
|
186
|
+
const total = ledger.totalDecisions;
|
|
187
|
+
|
|
188
|
+
// Quality score: 1.0 down towards 0.0 based on denial & anomaly ratio
|
|
189
|
+
let score = 1.0;
|
|
190
|
+
if (total > 0) {
|
|
191
|
+
const penalty = (ledger.deniedCount * 0.2 + ledger.anomalyCount * 0.3) / total;
|
|
192
|
+
score = Math.max(0, Math.min(1.0, 1.0 - penalty));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const degradationWarning = ledger.consecutiveDenies >= 2 ||
|
|
196
|
+
ledger.retryExhausted ||
|
|
197
|
+
ledger.breakerTripped ||
|
|
198
|
+
(total >= 3 && ledger.deniedCount / total >= 0.5);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
sessionId: ledger.sessionId,
|
|
202
|
+
totalDecisions: ledger.totalDecisions,
|
|
203
|
+
allowedCount: ledger.allowedCount,
|
|
204
|
+
deniedCount: ledger.deniedCount,
|
|
205
|
+
askCount: ledger.askCount,
|
|
206
|
+
anomalyCount: ledger.anomalyCount,
|
|
207
|
+
consecutiveDenies: ledger.consecutiveDenies,
|
|
208
|
+
qualityScore: Number(score.toFixed(2)),
|
|
209
|
+
degradationWarning,
|
|
210
|
+
recentEvents: ledger.recentEvents.map((e) => ({ ...e })),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Serializes session ledger state with current schema version (#73).
|
|
216
|
+
* @param {string} sessionId
|
|
217
|
+
* @returns {Record<string, any>}
|
|
218
|
+
*/
|
|
219
|
+
serializeSession(sessionId) {
|
|
220
|
+
const ledger = this._getOrCreate(sessionId);
|
|
221
|
+
return {
|
|
222
|
+
schema_version: CURRENT_SCHEMA_VERSION,
|
|
223
|
+
sessionId: ledger.sessionId,
|
|
224
|
+
totalDecisions: ledger.totalDecisions,
|
|
225
|
+
allowedCount: ledger.allowedCount,
|
|
226
|
+
deniedCount: ledger.deniedCount,
|
|
227
|
+
askCount: ledger.askCount,
|
|
228
|
+
anomalyCount: ledger.anomalyCount,
|
|
229
|
+
consecutiveDenies: ledger.consecutiveDenies,
|
|
230
|
+
breakerTripped: ledger.breakerTripped,
|
|
231
|
+
retryExhausted: ledger.retryExhausted,
|
|
232
|
+
recentEvents: ledger.recentEvents.map((e) => migrateVerdictRecord(e, CURRENT_SCHEMA_VERSION)),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Restores session ledger state from serialized data with automatic migration (#73).
|
|
238
|
+
* @param {Record<string, any>} data
|
|
239
|
+
*/
|
|
240
|
+
deserializeSession(data) {
|
|
241
|
+
if (!data || !data.sessionId) return;
|
|
242
|
+
const ledger = this._getOrCreate(data.sessionId);
|
|
243
|
+
ledger.totalDecisions = data.totalDecisions || 0;
|
|
244
|
+
ledger.allowedCount = data.allowedCount || 0;
|
|
245
|
+
ledger.deniedCount = data.deniedCount || 0;
|
|
246
|
+
ledger.askCount = data.askCount || 0;
|
|
247
|
+
ledger.anomalyCount = data.anomalyCount || 0;
|
|
248
|
+
ledger.consecutiveDenies = data.consecutiveDenies || 0;
|
|
249
|
+
ledger.breakerTripped = Boolean(data.breakerTripped);
|
|
250
|
+
ledger.retryExhausted = Boolean(data.retryExhausted);
|
|
251
|
+
ledger.recentEvents = Array.isArray(data.recentEvents)
|
|
252
|
+
? data.recentEvents.map((e) => migrateVerdictRecord(e, CURRENT_SCHEMA_VERSION))
|
|
253
|
+
: [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
reset(sessionId) {
|
|
257
|
+
const key = sessionId || "default";
|
|
258
|
+
this.sessions.delete(key);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
clear() {
|
|
262
|
+
this.sessions.clear();
|
|
263
|
+
}
|
|
264
|
+
}
|
package/lib/lineage.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// lib/lineage.js
|
|
2
|
+
// Subagent hierarchy and parentSession lineage authority (#98).
|
|
3
|
+
// Tracks session ancestry trees and propagates authority/policies down the lineage chain.
|
|
4
|
+
|
|
5
|
+
export class LineageManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
/** @type {Map<string, string>} childSessionId -> parentSessionId */
|
|
8
|
+
this.parentMap = new Map();
|
|
9
|
+
/** @type {Map<string, string>} childSessionId -> explicit rootSessionId */
|
|
10
|
+
this.rootMap = new Map();
|
|
11
|
+
/** @type {Map<string, Set<string>>} parentSessionId -> Set of childSessionIds */
|
|
12
|
+
this.childrenMap = new Map();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Registers parent-child session relationship.
|
|
17
|
+
* Prevents circular parent-child references.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} childSessionId
|
|
20
|
+
* @param {string} parentSessionId
|
|
21
|
+
* @param {string} [rootSessionId]
|
|
22
|
+
* @returns {boolean} True if successfully registered, false if invalid or circular
|
|
23
|
+
*/
|
|
24
|
+
registerLineage(childSessionId, parentSessionId, rootSessionId = null) {
|
|
25
|
+
if (!childSessionId || !parentSessionId) return false;
|
|
26
|
+
if (childSessionId === parentSessionId) return false;
|
|
27
|
+
|
|
28
|
+
// Check circular references: walking upwards from parent must not reach child
|
|
29
|
+
let curr = parentSessionId;
|
|
30
|
+
const visited = new Set([childSessionId]);
|
|
31
|
+
while (curr) {
|
|
32
|
+
if (visited.has(curr)) return false;
|
|
33
|
+
visited.add(curr);
|
|
34
|
+
curr = this.parentMap.get(curr);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.parentMap.set(childSessionId, parentSessionId);
|
|
38
|
+
if (rootSessionId && rootSessionId !== childSessionId) {
|
|
39
|
+
this.rootMap.set(childSessionId, rootSessionId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!this.childrenMap.has(parentSessionId)) {
|
|
43
|
+
this.childrenMap.set(parentSessionId, new Set());
|
|
44
|
+
}
|
|
45
|
+
this.childrenMap.get(parentSessionId).add(childSessionId);
|
|
46
|
+
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Retrieves the direct parent session ID for a session.
|
|
52
|
+
* @param {string} sessionId
|
|
53
|
+
* @returns {string|null}
|
|
54
|
+
*/
|
|
55
|
+
getParent(sessionId) {
|
|
56
|
+
if (!sessionId) return null;
|
|
57
|
+
return this.parentMap.get(sessionId) || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Retrieves the ultimate root session ID for a session.
|
|
62
|
+
* @param {string} sessionId
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
getRoot(sessionId) {
|
|
66
|
+
if (!sessionId) return "";
|
|
67
|
+
if (this.rootMap.has(sessionId)) {
|
|
68
|
+
return this.rootMap.get(sessionId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let curr = sessionId;
|
|
72
|
+
const visited = new Set();
|
|
73
|
+
while (curr && !visited.has(curr)) {
|
|
74
|
+
visited.add(curr);
|
|
75
|
+
const parent = this.parentMap.get(curr);
|
|
76
|
+
if (!parent) break;
|
|
77
|
+
curr = parent;
|
|
78
|
+
}
|
|
79
|
+
return curr;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Returns ancestry array from current session up to root: [sessionId, parentId, ..., rootId].
|
|
84
|
+
* @param {string} sessionId
|
|
85
|
+
* @returns {string[]}
|
|
86
|
+
*/
|
|
87
|
+
getLineage(sessionId) {
|
|
88
|
+
if (!sessionId) return [];
|
|
89
|
+
const chain = [];
|
|
90
|
+
let curr = sessionId;
|
|
91
|
+
const visited = new Set();
|
|
92
|
+
|
|
93
|
+
while (curr && !visited.has(curr)) {
|
|
94
|
+
visited.add(curr);
|
|
95
|
+
chain.push(curr);
|
|
96
|
+
curr = this.parentMap.get(curr);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return chain;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Checks whether ancestorSessionId is an ancestor of childSessionId.
|
|
104
|
+
* @param {string} childSessionId
|
|
105
|
+
* @param {string} ancestorSessionId
|
|
106
|
+
* @returns {boolean}
|
|
107
|
+
*/
|
|
108
|
+
hasLineageAuthority(childSessionId, ancestorSessionId) {
|
|
109
|
+
if (!childSessionId || !ancestorSessionId) return false;
|
|
110
|
+
if (childSessionId === ancestorSessionId) return true;
|
|
111
|
+
|
|
112
|
+
const lineage = this.getLineage(childSessionId);
|
|
113
|
+
return lineage.includes(ancestorSessionId);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolves effective policy walking up the lineage hierarchy.
|
|
118
|
+
* Evaluates getPolicyFn(sessionId). If undefined or null, falls back to parent, then root.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} sessionId
|
|
121
|
+
* @param {(id: string) => any} getPolicyFn
|
|
122
|
+
* @returns {any}
|
|
123
|
+
*/
|
|
124
|
+
resolveEffectivePolicy(sessionId, getPolicyFn) {
|
|
125
|
+
if (!sessionId || typeof getPolicyFn !== "function") return null;
|
|
126
|
+
|
|
127
|
+
const lineage = this.getLineage(sessionId);
|
|
128
|
+
for (const sid of lineage) {
|
|
129
|
+
try {
|
|
130
|
+
const policy = getPolicyFn(sid);
|
|
131
|
+
if (policy !== undefined && policy !== null) {
|
|
132
|
+
return {
|
|
133
|
+
policy,
|
|
134
|
+
sourceSessionId: sid,
|
|
135
|
+
inherited: sid !== sessionId,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Continue upward traversal on error
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Removes tracking for a session and unlinks from its parent.
|
|
148
|
+
* @param {string} sessionId
|
|
149
|
+
* @returns {boolean}
|
|
150
|
+
*/
|
|
151
|
+
clearSession(sessionId) {
|
|
152
|
+
if (!sessionId) return false;
|
|
153
|
+
const parent = this.parentMap.get(sessionId);
|
|
154
|
+
if (parent && this.childrenMap.has(parent)) {
|
|
155
|
+
this.childrenMap.get(parent).delete(sessionId);
|
|
156
|
+
}
|
|
157
|
+
this.parentMap.delete(sessionId);
|
|
158
|
+
this.rootMap.delete(sessionId);
|
|
159
|
+
this.childrenMap.delete(sessionId);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Clears all lineage mappings.
|
|
165
|
+
*/
|
|
166
|
+
clearAll() {
|
|
167
|
+
this.parentMap.clear();
|
|
168
|
+
this.rootMap.clear();
|
|
169
|
+
this.childrenMap.clear();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Returns current statistics for debugging and auditing.
|
|
174
|
+
* @returns {{ trackedSessions: number, rootSessions: number }}
|
|
175
|
+
*/
|
|
176
|
+
getStats() {
|
|
177
|
+
return {
|
|
178
|
+
trackedSessions: this.parentMap.size,
|
|
179
|
+
parentCount: this.childrenMap.size,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
package/lib/measure-kinds.js
CHANGED
|
@@ -56,6 +56,9 @@ const SYSTEM_LEVEL_TOOLS = new Set([
|
|
|
56
56
|
*/
|
|
57
57
|
export function classifyMeasureKind(execution, context = {}) {
|
|
58
58
|
if (!execution) return MEASURE_KINDS.LOCAL_SINGLE;
|
|
59
|
+
if (typeof execution === "string") {
|
|
60
|
+
execution = { name: "bash", arguments: { command: execution } };
|
|
61
|
+
}
|
|
59
62
|
|
|
60
63
|
const toolName = execution.name || execution.tool || "";
|
|
61
64
|
const args = execution.arguments || {};
|