@drakon-systems/shieldcortex-realtime 4.49.0 → 4.50.0
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/dist/index.js +10 -0
- package/dist/interceptor.js +38 -0
- package/dist/openclaw.plugin.json +1 -1
- package/index.ts +12 -0
- package/interceptor.ts +57 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2900,6 +2900,16 @@ export default {
|
|
|
2900
2900
|
const rec = sessionId ? sessionTaint.get(sessionId) : null;
|
|
2901
2901
|
return rec ? { reason: rec.reason } : null;
|
|
2902
2902
|
},
|
|
2903
|
+
// #227: session action lease — the fs-backed shared implementation,
|
|
2904
|
+
// injected through the same runtime seam as evaluateToolCall. Older
|
|
2905
|
+
// installed packages without the export simply leave the option
|
|
2906
|
+
// undefined (no lease plane — the capability-honesty surface says so).
|
|
2907
|
+
checkActionLease: typeof defenceMod.evaluateToolCallLease === 'function'
|
|
2908
|
+
? (toolName, args, sessionId) => defenceMod.evaluateToolCallLease(toolName, args, { self: sessionId ?? '' })
|
|
2909
|
+
: undefined,
|
|
2910
|
+
releaseActionLease: typeof defenceMod.releaseToolCallLease === 'function'
|
|
2911
|
+
? (toolName, args, sessionId) => defenceMod.releaseToolCallLease(toolName, args, { self: sessionId ?? '' })
|
|
2912
|
+
: undefined,
|
|
2903
2913
|
onAuditEntry: (entry) => syncInterceptEvent(entry, {
|
|
2904
2914
|
cloudApiKey: scConfig.cloudApiKey ?? '',
|
|
2905
2915
|
cloudBaseUrl: scConfig.cloudBaseUrl ?? 'https://api.shieldcortex.ai',
|
package/dist/interceptor.js
CHANGED
|
@@ -203,6 +203,8 @@ const FALLBACK_DANGEROUS_PATTERNS = [
|
|
|
203
203
|
{ re: /\/etc\/(passwd|shadow|sudoers)|~\/\.ssh|id_rsa|\.aws\/credentials|\.env\b/i, signal: 'touch-sensitive-path' },
|
|
204
204
|
// Guard's own approval store (#118): agent-side writes here mint approvals.
|
|
205
205
|
{ re: /\.shieldcortex[\\/]+approvals\b/i, signal: 'touch-approval-store' },
|
|
206
|
+
// Session-lease ledger + store (#227): a freeze an agent can edit is not a freeze.
|
|
207
|
+
{ re: /\.shieldcortex[\\/]+(?:DECISIONS\.md|leases)\b/i, signal: 'touch-decisions-ledger' },
|
|
206
208
|
{ re: /(?:^|[;&|(\n]|\$\()\s*(?:\w+=\S*\s+)*(?:sudo\s+)?uvx\b/i, signal: 'registry-code-exec' },
|
|
207
209
|
{ re: /(?:^|[;&|(\n]|\$\()\s*(?:\w+=\S*\s+)*(?:sudo\s+)?(?:pnpm|yarn)\b[^|;&\n]*\bdlx\b/i, signal: 'registry-code-exec' },
|
|
208
210
|
{ re: /\b(?:base64|openssl|xxd|cat|http)\b[^\n|]*\|(?:[^\n|]*\|)*\s*(?:\w+=\S*\s+)*(?:sudo\s+)?(?:bash|sh|zsh|ksh|python\d?|perl|ruby|node)\b(?:\s+-)?\s*(?:[;&|\n]|$)/i, signal: 'decode-pipe-to-shell' },
|
|
@@ -680,6 +682,34 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
680
682
|
async function runActionGuard(context) {
|
|
681
683
|
if (!actionGuardCfg.enabled)
|
|
682
684
|
return;
|
|
685
|
+
// Session action lease (#227) — EARLY, before the guard evaluator (which may
|
|
686
|
+
// be unwired or throw). A freeze is a HARD control and must bind regardless
|
|
687
|
+
// of the guard's own state; running it only after a successful evaluation
|
|
688
|
+
// would let a frozen action through the guard-unavailable path. Unscoped
|
|
689
|
+
// calls return null and cost nothing; a THROW is treated as no-lease (a
|
|
690
|
+
// broken lease layer must not deny everything); state unreadability fails
|
|
691
|
+
// closed to 'unknown' inside the store.
|
|
692
|
+
let leaseGate = null;
|
|
693
|
+
try {
|
|
694
|
+
leaseGate = options?.checkActionLease?.(context.toolName, context.arguments || {}, context.sessionId) ?? null;
|
|
695
|
+
if (leaseGate?.ledgerChanged) {
|
|
696
|
+
log.warn(`[shieldcortex] DECISIONS.md changed since last read (${leaseGate.ledgerChanged.fromHash.slice(0, 12)} → ${leaseGate.ledgerChanged.toHash.slice(0, 12)}) — tamper evidence, review the ledger`);
|
|
697
|
+
}
|
|
698
|
+
if (leaseGate && leaseGate.decision.verdict !== 'allow') {
|
|
699
|
+
emitAudit({
|
|
700
|
+
...guardAuditBase(context.toolName, { decision: 'block', severity: 'high', family: 'exec', action: `session-lease:${leaseGate.scope}`, reason: leaseGate.decision.reason, signals: ['session-lease', leaseGate.decision.verdict] }, `${context.toolName} :: ${summariseToolArgs(context.arguments)}`),
|
|
701
|
+
action: 'auto_deny', outcome: 'auto_denied',
|
|
702
|
+
});
|
|
703
|
+
log.warn(`[shieldcortex] action-guard SESSION-LEASE refused ${context.toolName} [${leaseGate.scope}/${leaseGate.decision.verdict}]: ${leaseGate.decision.reason}`);
|
|
704
|
+
throw new Error(`ShieldCortex: tool call blocked — ${leaseGate.decision.reason}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
// A ShieldCortex refusal must propagate; a lease-layer malfunction must not.
|
|
709
|
+
if (err instanceof Error && err.message.startsWith('ShieldCortex:'))
|
|
710
|
+
throw err;
|
|
711
|
+
leaseGate = null;
|
|
712
|
+
}
|
|
683
713
|
if (typeof evaluateToolCall !== 'function') {
|
|
684
714
|
handleGuardUnavailable(context, 'evaluateToolCall not wired');
|
|
685
715
|
return;
|
|
@@ -757,6 +787,14 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
757
787
|
const severity = v.severity === 'catastrophic' ? 'critical' : 'high';
|
|
758
788
|
// Catastrophic / exfil — hard block, always enforced when the guard is enabled.
|
|
759
789
|
if (v.decision === 'block') {
|
|
790
|
+
// #227: release any lease this call minted early — a blocked action must
|
|
791
|
+
// not leave a hold on that scope (self-heals at TTL if release fails).
|
|
792
|
+
if (leaseGate?.acquired) {
|
|
793
|
+
try {
|
|
794
|
+
options?.releaseActionLease?.(context.toolName, context.arguments || {}, context.sessionId);
|
|
795
|
+
}
|
|
796
|
+
catch { /* self-heals */ }
|
|
797
|
+
}
|
|
760
798
|
emitAudit({ ...base, action: 'auto_deny', outcome: 'auto_denied' });
|
|
761
799
|
// Surface the block to the gateway log (journald). Blocks are recorded in
|
|
762
800
|
// the ShieldCortex audit jsonl, but were otherwise invisible to an operator
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.50.0",
|
|
4
4
|
"name": "ShieldCortex Real-time Scanner",
|
|
5
5
|
"description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
|
|
6
6
|
"kind": null,
|
package/index.ts
CHANGED
|
@@ -3431,6 +3431,18 @@ export default {
|
|
|
3431
3431
|
const rec = sessionId ? sessionTaint.get(sessionId) : null;
|
|
3432
3432
|
return rec ? { reason: rec.reason } : null;
|
|
3433
3433
|
},
|
|
3434
|
+
// #227: session action lease — the fs-backed shared implementation,
|
|
3435
|
+
// injected through the same runtime seam as evaluateToolCall. Older
|
|
3436
|
+
// installed packages without the export simply leave the option
|
|
3437
|
+
// undefined (no lease plane — the capability-honesty surface says so).
|
|
3438
|
+
checkActionLease: typeof (defenceMod as any).evaluateToolCallLease === 'function'
|
|
3439
|
+
? (toolName, args, sessionId) =>
|
|
3440
|
+
(defenceMod as any).evaluateToolCallLease(toolName, args, { self: sessionId ?? '' })
|
|
3441
|
+
: undefined,
|
|
3442
|
+
releaseActionLease: typeof (defenceMod as any).releaseToolCallLease === 'function'
|
|
3443
|
+
? (toolName, args, sessionId) =>
|
|
3444
|
+
(defenceMod as any).releaseToolCallLease(toolName, args, { self: sessionId ?? '' })
|
|
3445
|
+
: undefined,
|
|
3434
3446
|
onAuditEntry: (entry) => syncInterceptEvent(entry, {
|
|
3435
3447
|
cloudApiKey: (scConfig as any).cloudApiKey ?? '',
|
|
3436
3448
|
cloudBaseUrl: (scConfig as any).cloudBaseUrl ?? 'https://api.shieldcortex.ai',
|
package/interceptor.ts
CHANGED
|
@@ -438,6 +438,8 @@ const FALLBACK_DANGEROUS_PATTERNS: Array<{ re: RegExp; signal: string }> = [
|
|
|
438
438
|
{ re: /\/etc\/(passwd|shadow|sudoers)|~\/\.ssh|id_rsa|\.aws\/credentials|\.env\b/i, signal: 'touch-sensitive-path' },
|
|
439
439
|
// Guard's own approval store (#118): agent-side writes here mint approvals.
|
|
440
440
|
{ re: /\.shieldcortex[\\/]+approvals\b/i, signal: 'touch-approval-store' },
|
|
441
|
+
// Session-lease ledger + store (#227): a freeze an agent can edit is not a freeze.
|
|
442
|
+
{ re: /\.shieldcortex[\\/]+(?:DECISIONS\.md|leases)\b/i, signal: 'touch-decisions-ledger' },
|
|
441
443
|
{ re: /(?:^|[;&|(\n]|\$\()\s*(?:\w+=\S*\s+)*(?:sudo\s+)?uvx\b/i, signal: 'registry-code-exec' },
|
|
442
444
|
{ re: /(?:^|[;&|(\n]|\$\()\s*(?:\w+=\S*\s+)*(?:sudo\s+)?(?:pnpm|yarn)\b[^|;&\n]*\bdlx\b/i, signal: 'registry-code-exec' },
|
|
443
445
|
{ re: /\b(?:base64|openssl|xxd|cat|http)\b[^\n|]*\|(?:[^\n|]*\|)*\s*(?:\w+=\S*\s+)*(?:sudo\s+)?(?:bash|sh|zsh|ksh|python\d?|perl|ruby|node)\b(?:\s+-)?\s*(?:[;&|\n]|$)/i, signal: 'decode-pipe-to-shell' },
|
|
@@ -770,6 +772,24 @@ interface InterceptorOptions {
|
|
|
770
772
|
* (or throwing) means no escalation — a broken scanner must never become a
|
|
771
773
|
* new source of denials. */
|
|
772
774
|
sessionTaint?: (sessionId: string | undefined) => { reason: string } | null;
|
|
775
|
+
/** #227: session action lease — injected from `shieldcortex/defence` at
|
|
776
|
+
* runtime (evaluateToolCallLease). Null for unscoped calls (the common
|
|
777
|
+
* case); a non-allow decision for a scoped call is a refusal that must
|
|
778
|
+
* precede every approval affordance. A THROW is treated as no-lease; state
|
|
779
|
+
* unreadability fails closed INSIDE the implementation. */
|
|
780
|
+
checkActionLease?: (
|
|
781
|
+
toolName: string,
|
|
782
|
+
args: Record<string, unknown>,
|
|
783
|
+
sessionId: string | undefined,
|
|
784
|
+
) => {
|
|
785
|
+
scope: string;
|
|
786
|
+
decision: { verdict: string; reason: string };
|
|
787
|
+
acquired?: boolean;
|
|
788
|
+
ledgerChanged?: { fromHash: string; toHash: string };
|
|
789
|
+
} | null;
|
|
790
|
+
/** #227: release a lease this call minted early, when the guard then blocks
|
|
791
|
+
* the action. Best-effort; a hold self-heals at its TTL if this is absent. */
|
|
792
|
+
releaseActionLease?: (toolName: string, args: Record<string, unknown>, sessionId: string | undefined) => void;
|
|
773
793
|
/** Approval broker (#143), injected from `shieldcortex/defence` at runtime.
|
|
774
794
|
* Absent, or present with `config.enabled: false`, means no model is ever
|
|
775
795
|
* consulted and the guard behaves exactly as it did before #143. */
|
|
@@ -983,6 +1003,38 @@ export function createInterceptor(
|
|
|
983
1003
|
async function runActionGuard(context: ToolCallContext): Promise<void> {
|
|
984
1004
|
if (!actionGuardCfg.enabled) return;
|
|
985
1005
|
|
|
1006
|
+
// Session action lease (#227) — EARLY, before the guard evaluator (which may
|
|
1007
|
+
// be unwired or throw). A freeze is a HARD control and must bind regardless
|
|
1008
|
+
// of the guard's own state; running it only after a successful evaluation
|
|
1009
|
+
// would let a frozen action through the guard-unavailable path. Unscoped
|
|
1010
|
+
// calls return null and cost nothing; a THROW is treated as no-lease (a
|
|
1011
|
+
// broken lease layer must not deny everything); state unreadability fails
|
|
1012
|
+
// closed to 'unknown' inside the store.
|
|
1013
|
+
let leaseGate: {
|
|
1014
|
+
scope: string; decision: { verdict: string; reason: string };
|
|
1015
|
+
acquired?: boolean; ledgerChanged?: { fromHash: string; toHash: string };
|
|
1016
|
+
} | null = null;
|
|
1017
|
+
try {
|
|
1018
|
+
leaseGate = options?.checkActionLease?.(context.toolName, context.arguments || {}, context.sessionId) ?? null;
|
|
1019
|
+
if (leaseGate?.ledgerChanged) {
|
|
1020
|
+
log.warn(
|
|
1021
|
+
`[shieldcortex] DECISIONS.md changed since last read (${leaseGate.ledgerChanged.fromHash.slice(0, 12)} → ${leaseGate.ledgerChanged.toHash.slice(0, 12)}) — tamper evidence, review the ledger`,
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
if (leaseGate && leaseGate.decision.verdict !== 'allow') {
|
|
1025
|
+
emitAudit({
|
|
1026
|
+
...guardAuditBase(context.toolName, { decision: 'block', severity: 'high', family: 'exec', action: `session-lease:${leaseGate.scope}`, reason: leaseGate.decision.reason, signals: ['session-lease', leaseGate.decision.verdict] } as ToolGuardVerdictLike, `${context.toolName} :: ${summariseToolArgs(context.arguments)}`),
|
|
1027
|
+
action: 'auto_deny', outcome: 'auto_denied',
|
|
1028
|
+
});
|
|
1029
|
+
log.warn(`[shieldcortex] action-guard SESSION-LEASE refused ${context.toolName} [${leaseGate.scope}/${leaseGate.decision.verdict}]: ${leaseGate.decision.reason}`);
|
|
1030
|
+
throw new Error(`ShieldCortex: tool call blocked — ${leaseGate.decision.reason}`);
|
|
1031
|
+
}
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
// A ShieldCortex refusal must propagate; a lease-layer malfunction must not.
|
|
1034
|
+
if (err instanceof Error && err.message.startsWith('ShieldCortex:')) throw err;
|
|
1035
|
+
leaseGate = null;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
986
1038
|
if (typeof evaluateToolCall !== 'function') {
|
|
987
1039
|
handleGuardUnavailable(context, 'evaluateToolCall not wired');
|
|
988
1040
|
return;
|
|
@@ -1065,6 +1117,11 @@ export function createInterceptor(
|
|
|
1065
1117
|
|
|
1066
1118
|
// Catastrophic / exfil — hard block, always enforced when the guard is enabled.
|
|
1067
1119
|
if (v.decision === 'block') {
|
|
1120
|
+
// #227: release any lease this call minted early — a blocked action must
|
|
1121
|
+
// not leave a hold on that scope (self-heals at TTL if release fails).
|
|
1122
|
+
if (leaseGate?.acquired) {
|
|
1123
|
+
try { options?.releaseActionLease?.(context.toolName, context.arguments || {}, context.sessionId); } catch { /* self-heals */ }
|
|
1124
|
+
}
|
|
1068
1125
|
emitAudit({ ...base, action: 'auto_deny', outcome: 'auto_denied' });
|
|
1069
1126
|
// Surface the block to the gateway log (journald). Blocks are recorded in
|
|
1070
1127
|
// the ShieldCortex audit jsonl, but were otherwise invisible to an operator
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.50.0",
|
|
4
4
|
"name": "ShieldCortex Real-time Scanner",
|
|
5
5
|
"description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
|
|
6
6
|
"kind": null,
|
package/package.json
CHANGED