@goodandready/dsh-approval-gate 0.1.20 → 0.1.22
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/bands.js +243 -239
- package/lib/engine.js +75 -80
- package/lib/escalation.js +151 -0
- package/lib/guidance.js +119 -0
- package/lib/index.js +85 -86
- package/lib/lineage.js +182 -0
- package/lib/measure-kinds.js +138 -0
- package/lib/messages.js +33 -0
- package/lib/snapshots.js +268 -0
- package/package.json +55 -55
package/lib/index.js
CHANGED
|
@@ -5,6 +5,12 @@ import { DeclarativeRulesEngine } from "./rules-loader.js";
|
|
|
5
5
|
import { LlmClassifier } from "./classifier.js";
|
|
6
6
|
import { ClassifierLatencyTracker } from "./latency.js";
|
|
7
7
|
import { assertHarnessCompatibility, provideServicePolyfill } from "./compat.js";
|
|
8
|
+
import { extractFilePath } from "./dispatcher.js";
|
|
9
|
+
import { SnapshotManager } from "./snapshots.js";
|
|
10
|
+
import { MEASURE_KINDS, classifyMeasureKind } from "./measure-kinds.js";
|
|
11
|
+
import { LineageManager } from "./lineage.js";
|
|
12
|
+
import { generateAgentGuidance, injectAgentGuidance, extractGuidance, GUIDANCE_MARKER } from "./guidance.js";
|
|
13
|
+
import { isRedundantSandboxRequest, buildRedundantSandboxRemediation, checkEscalationJustification } from "./escalation.js";
|
|
8
14
|
import Schema from "@deepseek-ai/schemastery";
|
|
9
15
|
|
|
10
16
|
if (typeof Schema.prototype?.volatile !== "function") {
|
|
@@ -180,6 +186,16 @@ export function readKnobs(ctx, execution) {
|
|
|
180
186
|
}
|
|
181
187
|
}
|
|
182
188
|
|
|
189
|
+
// 5b. Lineage authority inheritance (#98)
|
|
190
|
+
if (!policy && session && (session.parentSession || session.parentId || session.parentSessionId)) {
|
|
191
|
+
const parent = session.parentSession || { id: session.parentId || session.parentSessionId };
|
|
192
|
+
if (typeof parent.approvalPolicy === "string") {
|
|
193
|
+
policy = parent.approvalPolicy;
|
|
194
|
+
} else if (approval && typeof approval.effectivePolicy === "function") {
|
|
195
|
+
try { policy = approval.effectivePolicy(parent) || ""; } catch {}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
183
199
|
// 6. Global static fallback from approval service config
|
|
184
200
|
if (!policy && approval && approval.config && approval.config.policy) {
|
|
185
201
|
policy = approval.config.policy;
|
|
@@ -208,8 +224,12 @@ export const Config = Schema.object({
|
|
|
208
224
|
unattended: Schema.boolean().default(false).volatile(),
|
|
209
225
|
/** Workspace directory for path traversal boundary checks. */
|
|
210
226
|
workspaceDir: Schema.string().default("").volatile(),
|
|
211
|
-
/** Default verdict for unclassified commands: '
|
|
212
|
-
defaultVerdict: Schema.union(["allow", "ask"]).default("
|
|
227
|
+
/** Default verdict for unclassified commands: 'ask' (default, fail-closed) or 'allow' (#163, #172). */
|
|
228
|
+
defaultVerdict: Schema.union(["allow", "ask"]).default("ask").volatile(),
|
|
229
|
+
/** Maximum retained file snapshots per session (#78). */
|
|
230
|
+
maxSnapshotsPerSession: Schema.number().default(50).volatile(),
|
|
231
|
+
/** Maximum file size in bytes for pre-execution snapshots (#77). */
|
|
232
|
+
maxSnapshotBytes: Schema.number().default(1048576).volatile(),
|
|
213
233
|
/** Dry-run / shadow mode for safe testing of rules on production (#128). */
|
|
214
234
|
dryRun: Schema.boolean().default(false).volatile(),
|
|
215
235
|
/** Shadow mode alias for dryRun (#128). */
|
|
@@ -246,6 +266,10 @@ export const Config = Schema.object({
|
|
|
246
266
|
agentsRules: Schema.boolean().default(true).volatile(),
|
|
247
267
|
/** Enforce human confirmation on breaking major dependency bumps in package.json (#95). */
|
|
248
268
|
versionGuard: Schema.boolean().default(true).volatile(),
|
|
269
|
+
/** Require explicit justification for privilege escalation (#107). */
|
|
270
|
+
requireJustification: Schema.boolean().default(false).volatile(),
|
|
271
|
+
/** Enable automatic remediation advice for redundant sandbox privilege requests (#101). */
|
|
272
|
+
enableRedundantSandboxRecovery: Schema.boolean().default(true).volatile(),
|
|
249
273
|
/** Security engine instance (internal/testing). */
|
|
250
274
|
engine: Schema.any().hidden(),
|
|
251
275
|
});
|
|
@@ -313,7 +337,10 @@ export function apply(ctx, config) {
|
|
|
313
337
|
latencyTracker: new ClassifierLatencyTracker({
|
|
314
338
|
slaMs: cfg.classifierSlaMs !== undefined ? cfg.classifierSlaMs : 2000,
|
|
315
339
|
}),
|
|
316
|
-
defaultVerdict: cfg.defaultVerdict || "
|
|
340
|
+
defaultVerdict: cfg.defaultVerdict || "ask",
|
|
341
|
+
requireJustification: cfg.requireJustification === true,
|
|
342
|
+
enableRedundantSandboxRecovery: cfg.enableRedundantSandboxRecovery !== false,
|
|
343
|
+
snapshotManager: new SnapshotManager({ workspaceDir: cfg.workspaceDir || "", maxSnapshotsPerSession: cfg.maxSnapshotsPerSession, maxSnapshotBytes: cfg.maxSnapshotBytes }),
|
|
317
344
|
dryRun: Boolean(cfg.dryRun || cfg.shadowMode),
|
|
318
345
|
auditFailClosed: Boolean(cfg.auditFailClosed),
|
|
319
346
|
auditLogger: new AuditLogger({
|
|
@@ -342,6 +369,26 @@ export function apply(ctx, config) {
|
|
|
342
369
|
resetLatencyTracker: () => (engine.resetLatencyTracker ? engine.resetLatencyTracker() : undefined),
|
|
343
370
|
getArtifacts: (sessionId) => (engine.getArtifacts ? engine.getArtifacts(sessionId) : []),
|
|
344
371
|
clearArtifacts: (sessionId) => (engine.clearArtifacts ? engine.clearArtifacts(sessionId) : undefined),
|
|
372
|
+
takeSnapshot: (targetPath, meta) => (engine.takeSnapshot ? engine.takeSnapshot(targetPath, meta) : null),
|
|
373
|
+
rollbackSnapshot: (id) => (engine.rollbackSnapshot ? engine.rollbackSnapshot(id) : { success: false, error: "no-engine" }),
|
|
374
|
+
getSnapshots: (sessionId) => (engine.getSnapshots ? engine.getSnapshots(sessionId) : []),
|
|
375
|
+
clearSnapshots: (sessionId) => (engine.clearSnapshots ? engine.clearSnapshots(sessionId) : { cleared: 0 }),
|
|
376
|
+
clearAllSnapshots: () => (engine.clearAllSnapshots ? engine.clearAllSnapshots() : { cleared: 0 }),
|
|
377
|
+
getSnapshotStats: () => (engine.snapshotManager ? engine.snapshotManager.getStats() : null),
|
|
378
|
+
lineage: engine.lineage,
|
|
379
|
+
registerLineage: (child, parent, root) => engine.registerLineage(child, parent, root),
|
|
380
|
+
getLineage: (id) => engine.getLineage(id),
|
|
381
|
+
guidance: {
|
|
382
|
+
generate: (opts) => generateAgentGuidance(opts),
|
|
383
|
+
inject: (prompt, opts) => injectAgentGuidance(prompt, opts),
|
|
384
|
+
extract: (prompt) => extractGuidance(prompt),
|
|
385
|
+
},
|
|
386
|
+
getAgentGuidance: (opts) => generateAgentGuidance(opts),
|
|
387
|
+
redundantSandbox: {
|
|
388
|
+
check: (cmd, mode, opts) => isRedundantSandboxRequest(cmd, mode, opts),
|
|
389
|
+
remediation: (cmd, lang) => buildRedundantSandboxRemediation(cmd, lang),
|
|
390
|
+
},
|
|
391
|
+
checkEscalationJustification: (cmd, tool, args, opts) => checkEscalationJustification(cmd, tool, args, opts),
|
|
345
392
|
getSnapshot: () => ({
|
|
346
393
|
active: true,
|
|
347
394
|
dryRun: Boolean(cfg.dryRun || cfg.shadowMode),
|
|
@@ -353,6 +400,7 @@ export function apply(ctx, config) {
|
|
|
353
400
|
cacheStats: engine.cache ? engine.cache.stats() : null,
|
|
354
401
|
breakerState: engine.breaker ? engine.breaker.getState() : null,
|
|
355
402
|
latencyMetrics: engine.getLatencyMetrics ? engine.getLatencyMetrics() : null,
|
|
403
|
+
snapshots: engine.snapshotManager ? engine.snapshotManager.getStats() : null,
|
|
356
404
|
harnessCompat: compat.capabilities,
|
|
357
405
|
}),
|
|
358
406
|
};
|
|
@@ -364,9 +412,12 @@ export function apply(ctx, config) {
|
|
|
364
412
|
const knobs = readKnobs(ctx, execution);
|
|
365
413
|
const who = sessionTag(execution);
|
|
366
414
|
const sessionId = resolveSessionId(execution);
|
|
415
|
+
const callId = execution?.callId || execution?.id || null;
|
|
416
|
+
const agentId = execution?.agent?.id || execution?.agentId || null;
|
|
417
|
+
const subagentId = execution?.agent?.subagentId || execution?.subagentId || null;
|
|
367
418
|
let decision;
|
|
368
419
|
try {
|
|
369
|
-
decision = engine.decide(execution, { sessionId, knobs });
|
|
420
|
+
decision = engine.decide(execution, { sessionId, knobs, callId, agentId, subagentId, sandboxMode: knobs.sandbox, preset: knobs.preset });
|
|
370
421
|
} catch (err) {
|
|
371
422
|
const hit = {
|
|
372
423
|
ask: true,
|
|
@@ -418,6 +469,10 @@ export function apply(ctx, config) {
|
|
|
418
469
|
};
|
|
419
470
|
return { kind: "ask", reason: askMessage(hit, who, translate, ctx && ctx.logger) };
|
|
420
471
|
}
|
|
472
|
+
if (engine.snapshotManager) {
|
|
473
|
+
const targetPath = extractFilePath(execution && execution.arguments);
|
|
474
|
+
if (targetPath) engine.takeSnapshot(targetPath, { sessionId, callId: execution && (execution.callId || execution.id), tool: execution && execution.name });
|
|
475
|
+
}
|
|
421
476
|
return next();
|
|
422
477
|
});
|
|
423
478
|
}
|
|
@@ -425,9 +480,12 @@ export function apply(ctx, config) {
|
|
|
425
480
|
const guard = function (execution) {
|
|
426
481
|
const who = sessionTag(execution);
|
|
427
482
|
const sessionId = resolveSessionId(execution);
|
|
483
|
+
const callId = execution?.callId || execution?.id || null;
|
|
484
|
+
const agentId = execution?.agent?.id || execution?.agentId || null;
|
|
485
|
+
const subagentId = execution?.agent?.subagentId || execution?.subagentId || null;
|
|
428
486
|
let decision;
|
|
429
487
|
try {
|
|
430
|
-
decision = engine.decide(execution, { sessionId });
|
|
488
|
+
decision = engine.decide(execution, { sessionId, callId, agentId, subagentId });
|
|
431
489
|
} catch (err) {
|
|
432
490
|
const hit = {
|
|
433
491
|
deny: true,
|
|
@@ -455,6 +513,10 @@ export function apply(ctx, config) {
|
|
|
455
513
|
logBlocked(ctx, hit, who);
|
|
456
514
|
return denyMessage(hit, who, translate, ctx && ctx.logger);
|
|
457
515
|
}
|
|
516
|
+
if (engine.snapshotManager) {
|
|
517
|
+
const targetPath = extractFilePath(execution && execution.arguments);
|
|
518
|
+
if (targetPath) engine.takeSnapshot(targetPath, { sessionId, callId: execution && (execution.callId || execution.id), tool: execution && execution.name });
|
|
519
|
+
}
|
|
458
520
|
return undefined;
|
|
459
521
|
};
|
|
460
522
|
|
|
@@ -476,96 +538,27 @@ export {
|
|
|
476
538
|
CIRCUIT_STATE_TRIPPED,
|
|
477
539
|
CIRCUIT_STATE_RESUMED,
|
|
478
540
|
} from "./breaker.js";
|
|
479
|
-
export {
|
|
480
|
-
|
|
481
|
-
DEFAULT_GRANT_TTL_MS,
|
|
482
|
-
DEFAULT_GRANT_MAX_USES,
|
|
483
|
-
matchGrantCriteria,
|
|
484
|
-
} from "./grants.js";
|
|
485
|
-
export {
|
|
486
|
-
canonicalizeCall,
|
|
487
|
-
canonicalString,
|
|
488
|
-
hashCall,
|
|
489
|
-
sortKeys,
|
|
490
|
-
} from "./canonical.js";
|
|
541
|
+
export { SessionGrantStore, DEFAULT_GRANT_TTL_MS, DEFAULT_GRANT_MAX_USES, matchGrantCriteria } from "./grants.js";
|
|
542
|
+
export { canonicalizeCall, canonicalString, hashCall, sortKeys } from "./canonical.js";
|
|
491
543
|
export { expandHome, resolveSafePath, isPathTraversal, canonicalizeFsPath } from "./paths.js";
|
|
492
544
|
export { splitSubcommands } from "./tokenizer.js";
|
|
493
545
|
export { redactCredentials, redactUrl } from "./redact.js";
|
|
494
|
-
export {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
} from "./dispatcher.js";
|
|
504
|
-
export {
|
|
505
|
-
parseGitArgv,
|
|
506
|
-
classifyGitOperation,
|
|
507
|
-
inspectGitCommand,
|
|
508
|
-
GIT_CATEGORY_READ,
|
|
509
|
-
GIT_CATEGORY_MUTATE_LOCAL,
|
|
510
|
-
GIT_CATEGORY_REMOTE,
|
|
511
|
-
GIT_CATEGORY_DESTRUCTIVE,
|
|
512
|
-
} from "./parsers/git.js";
|
|
513
|
-
export {
|
|
514
|
-
isDevicePath,
|
|
515
|
-
extractRedirects,
|
|
516
|
-
inspectRedirectSecurity,
|
|
517
|
-
inspectSilentErrors,
|
|
518
|
-
inspectPipelines,
|
|
519
|
-
} from "./parsers/shell-cmds.js";
|
|
520
|
-
export {
|
|
521
|
-
isLoopbackHost,
|
|
522
|
-
isLoopbackUrl,
|
|
523
|
-
isLoopbackCommand,
|
|
524
|
-
} from "./loopback.js";
|
|
525
|
-
export {
|
|
526
|
-
computeFingerprint,
|
|
527
|
-
computeFingerprintHash,
|
|
528
|
-
normalizeCommandString,
|
|
529
|
-
} from "./fingerprint.js";
|
|
530
|
-
export {
|
|
531
|
-
VerdictCache,
|
|
532
|
-
DEFAULT_CACHE_MAX_SIZE,
|
|
533
|
-
DEFAULT_CACHE_TTL_MS,
|
|
534
|
-
} from "./cache.js";
|
|
535
|
-
export {
|
|
536
|
-
RetryExhaustionGuard,
|
|
537
|
-
DEFAULT_MAX_RETRY_ATTEMPTS,
|
|
538
|
-
} from "./retry-exhaustion.js";
|
|
539
|
-
export {
|
|
540
|
-
QualityLedger,
|
|
541
|
-
MAX_LEDGER_EVENTS_PER_SESSION,
|
|
542
|
-
} from "./ledger.js";
|
|
543
|
-
export {
|
|
544
|
-
AuditLogger,
|
|
545
|
-
DEFAULT_AUDIT_LOG_RELATIVE,
|
|
546
|
-
DEFAULT_MAX_AUDIT_BYTES,
|
|
547
|
-
DEFAULT_MAX_AUDIT_LINES,
|
|
548
|
-
DEFAULT_RETAIN_LINES,
|
|
549
|
-
auditRotateContent,
|
|
550
|
-
} from "./audit-log.js";
|
|
546
|
+
export { CommandDispatcher, isReviewerAgent, extractFilePath, TOOL_KIND_SHELL, TOOL_KIND_FILE_WRITE, TOOL_KIND_FILE_READ, TOOL_KIND_NETWORK, TOOL_KIND_GENERIC } from "./dispatcher.js";
|
|
547
|
+
export { parseGitArgv, classifyGitOperation, inspectGitCommand, GIT_CATEGORY_READ, GIT_CATEGORY_MUTATE_LOCAL, GIT_CATEGORY_REMOTE, GIT_CATEGORY_DESTRUCTIVE } from "./parsers/git.js";
|
|
548
|
+
export { isDevicePath, extractRedirects, inspectRedirectSecurity, inspectSilentErrors, inspectPipelines } from "./parsers/shell-cmds.js";
|
|
549
|
+
export { isLoopbackHost, isLoopbackUrl, isLoopbackCommand } from "./loopback.js";
|
|
550
|
+
export { computeFingerprint, computeFingerprintHash, normalizeCommandString } from "./fingerprint.js";
|
|
551
|
+
export { VerdictCache, DEFAULT_CACHE_MAX_SIZE, DEFAULT_CACHE_TTL_MS } from "./cache.js";
|
|
552
|
+
export { RetryExhaustionGuard, DEFAULT_MAX_RETRY_ATTEMPTS } from "./retry-exhaustion.js";
|
|
553
|
+
export { QualityLedger, MAX_LEDGER_EVENTS_PER_SESSION } from "./ledger.js";
|
|
554
|
+
export { AuditLogger, DEFAULT_AUDIT_LOG_RELATIVE, DEFAULT_MAX_AUDIT_BYTES, DEFAULT_MAX_AUDIT_LINES, DEFAULT_RETAIN_LINES, auditRotateContent } from "./audit-log.js";
|
|
551
555
|
export { BlockCounters, categorizeReason } from "./counters.js";
|
|
552
556
|
export { isCriticalOperation } from "./engine.js";
|
|
553
557
|
export { parseYaml, parseScalar, stripComment } from "./yaml-parser.js";
|
|
554
558
|
export { matchConditionValue, evaluateWhen } from "./when-matcher.js";
|
|
555
559
|
export { HotwordsMatcher, DEFAULT_HOTWORDS, extractWords } from "./hotwords.js";
|
|
556
560
|
export { DeclarativeRulesEngine, BUILTIN_RULES_FILE, WORKSPACE_RULES_REL, USER_RULES_REL } from "./rules-loader.js";
|
|
557
|
-
export {
|
|
558
|
-
normalizeChatCompletionsUrl,
|
|
559
|
-
withLlmRetry,
|
|
560
|
-
sanitizeClassifierInput,
|
|
561
|
-
truncateClassifierContext,
|
|
562
|
-
isLowRiskExecution,
|
|
563
|
-
isShapeRejectError,
|
|
564
|
-
extractJsonPayload,
|
|
565
|
-
ClassifierVerdictSchema,
|
|
566
|
-
validateClassifierResponse,
|
|
567
|
-
LlmClassifier,
|
|
568
|
-
} from "./classifier.js";
|
|
561
|
+
export { normalizeChatCompletionsUrl, withLlmRetry, sanitizeClassifierInput, truncateClassifierContext, isLowRiskExecution, isShapeRejectError, extractJsonPayload, ClassifierVerdictSchema, validateClassifierResponse, LlmClassifier } from "./classifier.js";
|
|
569
562
|
export { ContractGuard } from "./contract-guard.js";
|
|
570
563
|
export { OverengineeringGuard, DEFAULT_MAX_FILES_PER_SESSION } from "./overengineering.js";
|
|
571
564
|
export { resolveTempRoot, isGlobalTempPath, checkTempIsolation } from "./paths.js";
|
|
@@ -577,5 +570,11 @@ export { ArtifactRegistry, normalizeArtifactPath } from "./artifacts.js";
|
|
|
577
570
|
export { parseAgentsSecurityRules, loadAgentsRules, evaluateAgentsRules } from "./agents-parser.js";
|
|
578
571
|
export { detectHarnessCapabilities, assertHarnessCompatibility, provideServicePolyfill, registerHookPolyfill } from "./compat.js";
|
|
579
572
|
export { parseSemver, isMajorBump, inspectPackageJsonDiff, VersionGuard } from "./version-guard.js";
|
|
573
|
+
export { SnapshotManager, DEFAULT_MAX_SNAPSHOTS_PER_SESSION, DEFAULT_MAX_SNAPSHOT_BYTES } from "./snapshots.js";
|
|
574
|
+
export { MEASURE_KINDS, classifyMeasureKind, isSystemLevelExecution, isWorkspaceWideExecution } from "./measure-kinds.js";
|
|
575
|
+
export { LineageManager } from "./lineage.js";
|
|
576
|
+
export { generateAgentGuidance, injectAgentGuidance, extractGuidance, GUIDANCE_MARKER } from "./guidance.js";
|
|
577
|
+
export { isRedundantSandboxRequest, buildRedundantSandboxRemediation, checkEscalationJustification, REDUNDANT_SANDBOX_REASON, MISSING_JUSTIFICATION_REASON, INSUFFICIENT_JUSTIFICATION_REASON } from "./escalation.js";
|
|
578
|
+
|
|
580
579
|
|
|
581
580
|
|
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
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// lib/measure-kinds.js
|
|
2
|
+
// Multi-level operational impact scale categorization (#97).
|
|
3
|
+
// Classifies execution into:
|
|
4
|
+
// - 'local-single': operation affects exactly one file or inspects local state
|
|
5
|
+
// - 'workspace-wide': operation affects the entire workspace/repository or multiple files
|
|
6
|
+
// - 'system-level': operation accesses or mutates OS-level, network, or environment state
|
|
7
|
+
|
|
8
|
+
export const MEASURE_KINDS = Object.freeze({
|
|
9
|
+
LOCAL_SINGLE: "local-single",
|
|
10
|
+
WORKSPACE_WIDE: "workspace-wide",
|
|
11
|
+
SYSTEM_LEVEL: "system-level",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const SYSTEM_BINARIES = new Set([
|
|
15
|
+
"sudo", "su", "doas",
|
|
16
|
+
"systemctl", "service", "journalctl", "init",
|
|
17
|
+
"apt", "apt-get", "dpkg", "apk", "dnf", "yum", "pacman", "zypper",
|
|
18
|
+
"curl", "wget", "nc", "netcat", "ncat", "telnet", "ssh", "scp", "sftp", "rsync",
|
|
19
|
+
"iptables", "nftables", "ufw", "firewall-cmd",
|
|
20
|
+
"crontab", "at",
|
|
21
|
+
"reboot", "shutdown", "poweroff", "halt",
|
|
22
|
+
"mount", "umount", "fdisk", "parted", "mkfs", "dd",
|
|
23
|
+
"insmod", "rmmod", "modprobe",
|
|
24
|
+
"docker", "podman", "containerd", "runc", "crictl", "kubectl",
|
|
25
|
+
"passwd", "useradd", "userdel", "usermod", "groupadd",
|
|
26
|
+
"kill", "killall", "pkill", "xkill",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const SYSTEM_PATH_REGEX = /((\/|\b)(etc|boot|sys|proc|dev|usr|var\/log|root)(\/|\b)|~?\/\.(ssh|gnupg|bashrc|zshrc|profile)(\/|\b))/i;
|
|
30
|
+
|
|
31
|
+
const WORKSPACE_COMMAND_PATTERNS = [
|
|
32
|
+
/^(npm|pnpm|yarn|bun)\s+(install|add|remove|uninstall|update|upgrade|ci|build|pack|publish|run)/i,
|
|
33
|
+
/^(cargo|go|dotnet|mvn|gradle)\s+(build|test|clean|install|run)/i,
|
|
34
|
+
/^git\s+(checkout|switch|merge|rebase|pull|push|reset|clean|branch|tag|stash|restore|clone)/i,
|
|
35
|
+
/^(find|grep|ripgrep|rg|ack|ag)\s+/i,
|
|
36
|
+
/\b(make|ninja|cmake|meson)\b/i,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const SINGLE_FILE_TOOLS = new Set([
|
|
40
|
+
"view_file", "read_file", "view",
|
|
41
|
+
"write_to_file", "create_file", "write",
|
|
42
|
+
"replace_file_content", "edit_file", "patch_file",
|
|
43
|
+
"inspect_file",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const SYSTEM_LEVEL_TOOLS = new Set([
|
|
47
|
+
"read_url_content", "search_web", "web_search",
|
|
48
|
+
"browse", "http_request", "network_fetch",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Classifies the operational impact scale for a tool call execution.
|
|
53
|
+
* @param {Record<string, any>} execution
|
|
54
|
+
* @param {Record<string, any>} [context]
|
|
55
|
+
* @returns {'local-single' | 'workspace-wide' | 'system-level'}
|
|
56
|
+
*/
|
|
57
|
+
export function classifyMeasureKind(execution, context = {}) {
|
|
58
|
+
if (!execution) return MEASURE_KINDS.LOCAL_SINGLE;
|
|
59
|
+
if (typeof execution === "string") {
|
|
60
|
+
execution = { name: "bash", arguments: { command: execution } };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const toolName = execution.name || execution.tool || "";
|
|
64
|
+
const args = execution.arguments || {};
|
|
65
|
+
|
|
66
|
+
// 1. Tool-level system categorization
|
|
67
|
+
if (SYSTEM_LEVEL_TOOLS.has(toolName)) {
|
|
68
|
+
return MEASURE_KINDS.SYSTEM_LEVEL;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Tool-level single-file mutations / reads
|
|
72
|
+
if (SINGLE_FILE_TOOLS.has(toolName)) {
|
|
73
|
+
const target = args.TargetFile || args.targetPath || args.path || args.file || "";
|
|
74
|
+
if (typeof target === "string" && SYSTEM_PATH_REGEX.test(target)) {
|
|
75
|
+
return MEASURE_KINDS.SYSTEM_LEVEL;
|
|
76
|
+
}
|
|
77
|
+
return MEASURE_KINDS.LOCAL_SINGLE;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. Shell / command inspection
|
|
81
|
+
const command = typeof args.command === "string" ? args.command.trim() : "";
|
|
82
|
+
if (!command) {
|
|
83
|
+
return MEASURE_KINDS.LOCAL_SINGLE;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Fast check: first word
|
|
87
|
+
const firstWord = command.split(/\s+/)[0]?.toLowerCase();
|
|
88
|
+
if (SYSTEM_BINARIES.has(firstWord)) {
|
|
89
|
+
return MEASURE_KINDS.SYSTEM_LEVEL;
|
|
90
|
+
}
|
|
91
|
+
if ((command.includes("/") || command.includes(".")) && SYSTEM_PATH_REGEX.test(command)) {
|
|
92
|
+
return MEASURE_KINDS.SYSTEM_LEVEL;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check subcommands or pipes for system binaries only if operators present
|
|
96
|
+
if (/[|;&]/.test(command)) {
|
|
97
|
+
const tokens = command.split(/[\s|;&]+/);
|
|
98
|
+
for (const token of tokens) {
|
|
99
|
+
if (SYSTEM_BINARIES.has(token.toLowerCase())) {
|
|
100
|
+
return MEASURE_KINDS.SYSTEM_LEVEL;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check workspace-wide patterns
|
|
106
|
+
for (const pattern of WORKSPACE_COMMAND_PATTERNS) {
|
|
107
|
+
if (pattern.test(command)) {
|
|
108
|
+
return MEASURE_KINDS.WORKSPACE_WIDE;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Multi-target rm or recursive rm
|
|
113
|
+
if (/^rm\s+/i.test(command)) {
|
|
114
|
+
if (/\s-[a-zA-Z]*r/i.test(command) || command.includes("*") || command.split(/\s+/).length > 3) {
|
|
115
|
+
return MEASURE_KINDS.WORKSPACE_WIDE;
|
|
116
|
+
}
|
|
117
|
+
return MEASURE_KINDS.LOCAL_SINGLE;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Simple single-target commands
|
|
121
|
+
if (/^(cat|head|tail|sed|awk|echo|touch|ls|stat|file)\b/i.test(command)) {
|
|
122
|
+
if (command.includes("*") || command.includes(";")) {
|
|
123
|
+
return MEASURE_KINDS.WORKSPACE_WIDE;
|
|
124
|
+
}
|
|
125
|
+
return MEASURE_KINDS.LOCAL_SINGLE;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Default fallback for unrecognized operations
|
|
129
|
+
return MEASURE_KINDS.LOCAL_SINGLE;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function isSystemLevelExecution(execution, context) {
|
|
133
|
+
return classifyMeasureKind(execution, context) === MEASURE_KINDS.SYSTEM_LEVEL;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function isWorkspaceWideExecution(execution, context) {
|
|
137
|
+
return classifyMeasureKind(execution, context) === MEASURE_KINDS.WORKSPACE_WIDE;
|
|
138
|
+
}
|