thumbgate 1.35.0 → 1.37.2
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/.agents/skills/cobble-hot-store-compare-not-clone/SKILL.md +80 -0
- package/.agents/skills/colab-compute-honesty-not-clone/SKILL.md +70 -0
- package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
- package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
- package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
- package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
- package/.agents/skills/token-shunt-honesty-not-clone/SKILL.md +67 -0
- package/.agents/skills/typesafe-typed-questions-not-clone/SKILL.md +82 -0
- package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +1 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/CONTRIBUTING.md +95 -0
- package/README.md +195 -632
- package/THIRD_PARTY_NOTICES.md +89 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +8 -0
- package/adapters/future-agi/FUTURE_AGI.md +23 -0
- package/adapters/future-agi/config.toml +3 -0
- package/adapters/future-agi/future-agi-bridge.js +9 -0
- package/adapters/future-agi/opencode.json +8 -0
- package/adapters/herdr/herdr-plugin.toml +18 -0
- package/adapters/mcp/server-stdio.js +238 -25
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/workos/WORKOS.md +52 -0
- package/bin/cli.js +465 -3
- package/bin/futureagi-bridge +9 -0
- package/config/gate-templates.json +629 -4
- package/config/gates/actor-critic-audit.json +34 -0
- package/config/gates/default.json +9 -3
- package/config/gates/five-walls-governance.json +34 -0
- package/config/gates/future-agi-guardrails.json +34 -0
- package/config/gates/radware-threat-defense-2026.json +61 -0
- package/config/gates/simatree-data-governance.json +33 -0
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +6 -0
- package/config/model-candidates.json +312 -29
- package/config/model-tiers.json +18 -0
- package/config/post-deploy-marketing-pages.json +10 -0
- package/config/progressive/01-wire-only.json +11 -0
- package/config/progressive/02-dashboard-empty-ok.json +10 -0
- package/config/progressive/03-one-lesson.json +10 -0
- package/config/progressive/04-warn-fires.json +11 -0
- package/config/progressive/05-strict-optional.json +11 -0
- package/config/progressive/README.md +15 -0
- package/config/schemas/broker-execution-receipt.schema.json +139 -0
- package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
- package/conformance/provider-attestation/vectors.json +320 -0
- package/docs/specs/provider-execution-attestation-v1.md +69 -0
- package/openapi/openapi.yaml +15 -0
- package/package.json +408 -150
- package/public/about.html +2 -2
- package/public/ai-malpractice-prevention.html +7 -7
- package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
- package/public/blog/a-receipt-is-not-world-state.html +388 -0
- package/public/blog/git-at-agent-scale.html +374 -0
- package/public/blog/no-llm-in-the-gate.html +133 -0
- package/public/blog.html +80 -0
- package/public/case-studies.html +16 -1
- package/public/compare.html +28 -0
- package/public/diagnostic.html +216 -7
- package/public/docs/connectors.html +39 -0
- package/public/federal.html +2 -2
- package/public/founders.html +639 -0
- package/public/index.html +87 -9
- package/public/install.html +8 -8
- package/public/learn.html +39 -0
- package/public/numbers.html +2 -2
- package/public/peter.html +310 -0
- package/public/platform-partners.html +119 -0
- package/public/pricing.html +24 -3
- package/public/privacy.html +117 -0
- package/public/pro.html +17 -0
- package/public/support.html +62 -0
- package/public/terms.html +130 -0
- package/public/third-party-notices.html +95 -0
- package/public/yt.html +351 -0
- package/scripts/action-receipts.js +133 -3
- package/scripts/adaptive-governance-arena.js +349 -0
- package/scripts/admin-override.js +205 -0
- package/scripts/agent-action-inventory.js +869 -0
- package/scripts/agent-audit-trace.js +42 -2
- package/scripts/agent-egress-policy.js +1117 -0
- package/scripts/agent-memory-lifecycle.js +141 -2
- package/scripts/agent-operations-planner.js +441 -1
- package/scripts/agent-readiness.js +68 -0
- package/scripts/agent-security-central.js +647 -0
- package/scripts/allowlist-bridge-honesty.js +417 -0
- package/scripts/async-job-runner.js +102 -11
- package/scripts/audit-trail.js +212 -0
- package/scripts/billing.js +1 -1
- package/scripts/broker-execution-receipts.js +719 -0
- package/scripts/budget-aware-gates-proof.js +423 -0
- package/scripts/claude-feedback-sync.js +29 -3
- package/scripts/claw-harness-production.js +237 -0
- package/scripts/cli-schema.js +203 -1
- package/scripts/cobble-hot-store-split.js +600 -0
- package/scripts/codex-runbook-flywheel.js +318 -0
- package/scripts/colab-compute-honesty.js +281 -0
- package/scripts/context-footprint.js +186 -0
- package/scripts/contextfs.js +143 -61
- package/scripts/dashboard.js +251 -32
- package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
- package/scripts/docker-sandbox-planner.js +18 -0
- package/scripts/double-blind-eval-protocol.js +252 -0
- package/scripts/edotenv-rl-gateway.js +259 -0
- package/scripts/ensure-production-search-corpus.js +162 -0
- package/scripts/eval-holdout.js +311 -0
- package/scripts/feedback-aggregate.js +21 -2
- package/scripts/feedback-loop.js +87 -5
- package/scripts/feedback-quality.js +9 -0
- package/scripts/file-ledger-lock.js +4 -1
- package/scripts/financial-control-plane.js +41 -1
- package/scripts/find-dormant-requires.js +118 -0
- package/scripts/fs-utils.js +84 -8
- package/scripts/gates-engine.js +826 -68
- package/scripts/generate-case-study-outreach.js +24 -15
- package/scripts/git-at-scale.js +628 -0
- package/scripts/governance-conflict-audit.js +1650 -0
- package/scripts/governance-difficulty-curriculum.js +328 -0
- package/scripts/graphrag-retrieval.js +275 -0
- package/scripts/gurobi-optimizer.js +324 -0
- package/scripts/gurobi_optimizer.py +485 -0
- package/scripts/harness-selector.js +82 -1
- package/scripts/hidden-entry-points.js +284 -0
- package/scripts/human-escalation.js +199 -1
- package/scripts/hybrid-feedback-context.js +152 -19
- package/scripts/intent-governed-execution.js +602 -0
- package/scripts/intervention-policy.js +123 -20
- package/scripts/jit-harness-compose.js +628 -0
- package/scripts/jsonl-watcher.js +10 -0
- package/scripts/lesson-embedding-index.js +95 -12
- package/scripts/lesson-retrieval.js +105 -19
- package/scripts/local-model-profile.js +19 -2
- package/scripts/mailer/resend-mailer.js +1 -1
- package/scripts/matryoshka-embedding.js +235 -0
- package/scripts/mcp-oauth.js +42 -4
- package/scripts/mcp-session-handles.js +1016 -0
- package/scripts/mcp-wiring-doctor.js +314 -0
- package/scripts/memory-firewall.js +115 -2
- package/scripts/memory-scope-readiness.js +299 -0
- package/scripts/memory-vs-rag-route.js +161 -0
- package/scripts/model-tier-router.js +148 -21
- package/scripts/nvidia-specdecode-al-doctor.js +536 -0
- package/scripts/openui-catalog-compose-honesty.js +593 -0
- package/scripts/operational-integrity.js +19 -1
- package/scripts/override-audit.js +213 -0
- package/scripts/package-manager-honesty-doctor.js +458 -0
- package/scripts/pr-manager.js +63 -1
- package/scripts/prove-herdr-adapter.js +52 -0
- package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
- package/scripts/prove-workos.js +73 -0
- package/scripts/provider-attestation-conformance.js +192 -0
- package/scripts/provider-receipt-contract.js +136 -0
- package/scripts/qwen38-max-cost-optimizer.js +401 -0
- package/scripts/radware-threat-defense.js +280 -0
- package/scripts/rag-embedding-identity.js +221 -0
- package/scripts/rag-precision-guardrails.js +112 -2
- package/scripts/remote-feedback-capture.js +159 -0
- package/scripts/research-agent-harness.js +256 -0
- package/scripts/rsi-safety-hillclimb.js +200 -0
- package/scripts/rule-sprawl.js +188 -0
- package/scripts/schedule-manager.js +147 -0
- package/scripts/self-heal.js +8 -0
- package/scripts/session-lease.js +415 -0
- package/scripts/simatree-data-governance.js +347 -0
- package/scripts/slo-alert-engine.js +172 -7
- package/scripts/solver-parity.js +539 -0
- package/scripts/stealth-memory-injection-gate.js +333 -0
- package/scripts/switchyard-router.js +366 -0
- package/scripts/telemetry-analytics.js +84 -27
- package/scripts/temporal-decay-weighting.js +138 -0
- package/scripts/test-all.js +165 -0
- package/scripts/token-savings.js +42 -0
- package/scripts/token-shunt-honesty.js +502 -0
- package/scripts/tool-kpi-tracker.js +108 -5
- package/scripts/tool-registry.js +193 -5
- package/scripts/typesafe-typed-questions.js +813 -0
- package/scripts/universal-claim-evaluator.js +14 -2
- package/scripts/vector-store.js +279 -9
- package/scripts/workflow-notebook.js +391 -0
- package/scripts/workflow-sentinel.js +111 -12
- package/scripts/workos-production-guard.js +260 -0
- package/scripts/workspace-search-route.js +515 -0
- package/server.json +2 -2
- package/src/agent-identity-boundary.js +76 -0
- package/src/agent-retrieval-cache.js +155 -0
- package/src/alert-noise-ledger.js +502 -0
- package/src/api/server.js +724 -153
- package/src/git-fast-cache.js +220 -0
- package/src/git-wal-sync.js +156 -0
- package/src/hash-anchored-edit.js +82 -0
- package/src/hermes-platform-protocol.js +475 -0
- package/src/hermes-sync-plane.js +241 -0
- package/src/index.js +30 -1
- package/src/iso42001-compliance-guard.js +97 -0
- package/src/latency-budget.js +244 -0
- package/src/mcp-writeguard.js +316 -0
- package/src/miminions-adapter.js +106 -0
- package/src/pipeline-compass.js +104 -0
- package/src/ppl-alert-pipeline.js +284 -0
- package/src/rendezvous-router.js +90 -0
- package/src/security-questionnaire.js +195 -0
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TypeSafe typed-question FORMAT steal — not a product clone.
|
|
6
|
+
*
|
|
7
|
+
* Sources (signed-in console 2026-09-17, igor@igorganapolsky.com):
|
|
8
|
+
* https://console.typesafe.ai/hook
|
|
9
|
+
* https://console.typesafe.ai/playground (Support agent audit example)
|
|
10
|
+
* https://docs.typesafe.ai/cookbooks/llm_guardrails.md
|
|
11
|
+
* https://docs.typesafe.ai/patterns/confidence-routing.md
|
|
12
|
+
*
|
|
13
|
+
* Transfers (process only):
|
|
14
|
+
* 1. Atomic typed questions (noul / choice / score) over one tool-call state
|
|
15
|
+
* 2. Independent parallel evaluation — questions cannot see each other
|
|
16
|
+
* 3. Code owns route() (pass | review | block) — the model does not emit the verdict
|
|
17
|
+
* 4. Confidence is a second axis; low confidence on high-stakes fails closed
|
|
18
|
+
*
|
|
19
|
+
* Does NOT install typesafe-sdk, call api.typesafe.ai, clone Jev, or wire
|
|
20
|
+
* an LLM adjudicator (#3690 / #3687, ECI pause). Deterministic matchers
|
|
21
|
+
* answer the battery; existing PreToolUse / gate-check remains the enforcer.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
|
|
27
|
+
const SOURCE_URLS = Object.freeze([
|
|
28
|
+
'https://console.typesafe.ai/hook',
|
|
29
|
+
'https://docs.typesafe.ai/cookbooks/llm_guardrails.md',
|
|
30
|
+
'https://docs.typesafe.ai/patterns/confidence-routing.md',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const TYPED_KINDS = Object.freeze(['noul', 'choice', 'score']);
|
|
34
|
+
const ROUTE_PRECEDENCE = Object.freeze(['block', 'review', 'pass']);
|
|
35
|
+
const ROUTE_ACTIONS = Object.freeze(['block', 'review', 'pass']);
|
|
36
|
+
const WRITE_TOOLS = Object.freeze(['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'Bash']);
|
|
37
|
+
|
|
38
|
+
const POLICIES = Object.freeze({
|
|
39
|
+
strict: { reviewThreshold: 0.35, actionThreshold: 0.7, severityBlock: 2 },
|
|
40
|
+
permissive: { reviewThreshold: 0.35, actionThreshold: 0.85, severityBlock: 2 },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const HAZARD_ACTION = Object.freeze({
|
|
44
|
+
secret_exfil: 'block',
|
|
45
|
+
destructive: 'block',
|
|
46
|
+
outbound_send: 'block',
|
|
47
|
+
guardrail_tamper: 'block',
|
|
48
|
+
clone_jev: 'block',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const CLONE_RE =
|
|
52
|
+
/\b(typesafe-sdk|@typesafe\/|api\.typesafe\.ai|clone\s+jev|install\s+jev|llm[- ]adjudicat|wire\s+jev\s+as\s+(?:the\s+)?gate)\b/i;
|
|
53
|
+
|
|
54
|
+
const DEFAULT_BATTERY = Object.freeze({
|
|
55
|
+
secret_exfil: {
|
|
56
|
+
type: 'noul',
|
|
57
|
+
instructions: 'This tool call exfiltrates or writes secrets, keys, or credentials.',
|
|
58
|
+
criteria: {
|
|
59
|
+
true: 'The call reads, prints, posts, or commits secret material',
|
|
60
|
+
false: 'The call does not handle secret material',
|
|
61
|
+
},
|
|
62
|
+
matcher:
|
|
63
|
+
'sk_live_|AKIA[0-9A-Z]{16}|BEGIN (?:RSA |OPENSSH )?PRIVATE KEY|aws_secret_access_key|ghp_[A-Za-z0-9]{20,}|xox[baprs]-',
|
|
64
|
+
stakes: 'high',
|
|
65
|
+
},
|
|
66
|
+
destructive: {
|
|
67
|
+
type: 'noul',
|
|
68
|
+
instructions: 'This tool call is a destructive git or filesystem action.',
|
|
69
|
+
criteria: {
|
|
70
|
+
true: 'Force-push, hard reset, or rm -rf of a root/home path',
|
|
71
|
+
false: 'No destructive rewrite or wipe',
|
|
72
|
+
},
|
|
73
|
+
matcher: 'git\\s+push\\s+(?:[^\\n]*\\s)?(?:--force|-f)(?:\\s|$)|git\\s+reset\\s+--hard|rm\\s+-rf\\s+[/~]',
|
|
74
|
+
warnMatcher: 'git\\s+add\\s+(-A|--all)|rm\\s+-rf\\s+\\.',
|
|
75
|
+
stakes: 'high',
|
|
76
|
+
},
|
|
77
|
+
outbound_send: {
|
|
78
|
+
type: 'noul',
|
|
79
|
+
instructions: 'This tool call sends outbound email or a public message as the operator.',
|
|
80
|
+
criteria: {
|
|
81
|
+
true: 'A send/dispatch tool is invoked',
|
|
82
|
+
false: 'Draft-only or no outbound send',
|
|
83
|
+
},
|
|
84
|
+
matcher: 'gmail.*\\bsend\\b|send_email|messages\\.send|\\bsmtp\\b.*\\bsend\\b|send_draft',
|
|
85
|
+
stakes: 'high',
|
|
86
|
+
},
|
|
87
|
+
guardrail_tamper: {
|
|
88
|
+
type: 'noul',
|
|
89
|
+
instructions: 'This tool call edits ThumbGate gates, prevention rules, or the spend guard.',
|
|
90
|
+
criteria: {
|
|
91
|
+
true: 'A gate, prevention-rule, or spend-guard file is being rewritten',
|
|
92
|
+
false: 'No guardrail file is in the write set',
|
|
93
|
+
},
|
|
94
|
+
matcher: 'config/gates|prevention-rules\\.md|gate-templates\\.json|thumbgate-spend-guard',
|
|
95
|
+
writeTools: WRITE_TOOLS,
|
|
96
|
+
writeCommandMatcher: '(?:^|[\\s;|&])(?:tee|rm|mv|cp|sed\\s+-i|>|>>)',
|
|
97
|
+
stakes: 'high',
|
|
98
|
+
},
|
|
99
|
+
clone_jev: {
|
|
100
|
+
type: 'noul',
|
|
101
|
+
instructions: 'This tool call installs TypeSafe/Jev or an LLM adjudicator as the PreToolUse gate.',
|
|
102
|
+
criteria: {
|
|
103
|
+
true: 'typesafe-sdk, api.typesafe.ai, Jev-as-gate, or LLM adjudication is being wired',
|
|
104
|
+
false: 'Deterministic gates stay in charge',
|
|
105
|
+
},
|
|
106
|
+
matcher:
|
|
107
|
+
'typesafe-sdk|@typesafe/|api\\.typesafe\\.ai|clone\\s+jev|llm[- ]adjudicat|jev-latest.*pretooluse|pretooluse.*jev',
|
|
108
|
+
stakes: 'high',
|
|
109
|
+
},
|
|
110
|
+
hazard_family: {
|
|
111
|
+
type: 'choice',
|
|
112
|
+
instructions: 'Which hazard family does this tool call belong to?',
|
|
113
|
+
criteria: {
|
|
114
|
+
none: 'No listed hazard matched',
|
|
115
|
+
secret: 'Secret exfiltration',
|
|
116
|
+
destructive: 'Destructive git or filesystem',
|
|
117
|
+
outbound: 'Outbound send',
|
|
118
|
+
tamper: 'Guardrail tamper',
|
|
119
|
+
clone: 'TypeSafe/Jev/LLM-adjudicator clone',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
severity: {
|
|
123
|
+
type: 'score',
|
|
124
|
+
instructions: 'How much harm would complying with this tool call do?',
|
|
125
|
+
criteria: [
|
|
126
|
+
'None: ordinary safe call',
|
|
127
|
+
'Mild: warn-level match, no hard floor',
|
|
128
|
+
'Serious: default hard-block family',
|
|
129
|
+
'Severe: secret exfil or guardrail tamper',
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const RAIL_MAP = Object.freeze([
|
|
135
|
+
{ typesafe: 'noul (one hazard each)', thumbgate: 'existing secret / destructive / outbound / tamper matchers' },
|
|
136
|
+
{ typesafe: 'choice over a closed set', thumbgate: 'hazard_family composed in code from which noul fired' },
|
|
137
|
+
{ typesafe: 'score rubric', thumbgate: 'severity 0–3 from the same matchers' },
|
|
138
|
+
{ typesafe: 'route() in application code', thumbgate: 'pass | review | block — never a model-emitted verdict' },
|
|
139
|
+
{ typesafe: 'confidence as a second axis', thumbgate: 'warn-level noul=0.55 → review; high-stakes + severity≥2 → block' },
|
|
140
|
+
{ typesafe: 'Jev / typesafe-sdk / System One API', thumbgate: 'refused (ECI; #3690/#3687 LLM adjudicator parked)' },
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
function normalizeBoolean(value) {
|
|
144
|
+
if (value === true || value === 1) return true;
|
|
145
|
+
if (value === false || value === 0 || value == null) return false;
|
|
146
|
+
return /^(1|true|yes|on)$/i.test(String(value).trim());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function readText(filePath) {
|
|
150
|
+
if (!filePath) return null;
|
|
151
|
+
if (!fs.existsSync(filePath)) {
|
|
152
|
+
const err = new Error(`file not found: ${filePath}`);
|
|
153
|
+
err.code = 'ENOENT';
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function flattenState(payload) {
|
|
160
|
+
if (payload == null) return '';
|
|
161
|
+
if (typeof payload === 'string') return payload;
|
|
162
|
+
const parts = [JSON.stringify(payload)];
|
|
163
|
+
if (payload.tool_name || payload.toolName) {
|
|
164
|
+
parts.push(String(payload.tool_name || payload.toolName));
|
|
165
|
+
}
|
|
166
|
+
const input = payload.tool_input || payload.toolInput || {};
|
|
167
|
+
if (input && typeof input === 'object') {
|
|
168
|
+
for (const key of ['command', 'file_path', 'path', 'content', 'prompt', 'url']) {
|
|
169
|
+
if (input[key] != null) parts.push(String(input[key]));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (payload.command) parts.push(String(payload.command));
|
|
173
|
+
return parts.join('\n');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function compileMatcher(source) {
|
|
177
|
+
if (!source) return null;
|
|
178
|
+
try {
|
|
179
|
+
return new RegExp(source, 'i');
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function toolNameOf(payload) {
|
|
186
|
+
return String((payload && (payload.tool_name || payload.toolName)) || '').trim();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function commandOf(payload) {
|
|
190
|
+
const input = (payload && (payload.tool_input || payload.toolInput)) || {};
|
|
191
|
+
return String((input && input.command) || (payload && payload.command) || '');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isWriteCapable(question, payload) {
|
|
195
|
+
const allowed = Array.isArray(question.writeTools) ? question.writeTools : null;
|
|
196
|
+
if (!allowed || allowed.length === 0) return true;
|
|
197
|
+
const name = toolNameOf(payload);
|
|
198
|
+
const hit = allowed.some((tool) => String(tool).toLowerCase() === name.toLowerCase());
|
|
199
|
+
if (!hit) return false;
|
|
200
|
+
if (name.toLowerCase() === 'bash' && question.writeCommandMatcher) {
|
|
201
|
+
const re = compileMatcher(question.writeCommandMatcher);
|
|
202
|
+
if (re && !re.test(commandOf(payload))) return false;
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function noulFromMatchers(blob, question, payload) {
|
|
208
|
+
if (!isWriteCapable(question, payload || {})) {
|
|
209
|
+
return { noul: 0, band: 'pass' };
|
|
210
|
+
}
|
|
211
|
+
const hard = compileMatcher(question.matcher);
|
|
212
|
+
const warn = compileMatcher(question.warnMatcher);
|
|
213
|
+
if (hard && hard.test(blob)) return { noul: 1, band: 'action' };
|
|
214
|
+
if (warn && warn.test(blob)) return { noul: 0.55, band: 'review' };
|
|
215
|
+
return { noul: 0, band: 'pass' };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function actionForNoul(id, question) {
|
|
219
|
+
if (HAZARD_ACTION[id]) return HAZARD_ACTION[id];
|
|
220
|
+
const action = String((question && question.action) || '').toLowerCase();
|
|
221
|
+
if (ROUTE_ACTIONS.includes(action)) return action;
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validateQuestion(id, question) {
|
|
226
|
+
const errors = [];
|
|
227
|
+
if (!question || typeof question !== 'object') {
|
|
228
|
+
errors.push({ id: 'malformed_question', questionId: id, message: `Question ${id} is not an object.` });
|
|
229
|
+
return errors;
|
|
230
|
+
}
|
|
231
|
+
const type = String(question.type || '').toLowerCase();
|
|
232
|
+
if (!TYPED_KINDS.includes(type)) {
|
|
233
|
+
errors.push({
|
|
234
|
+
id: 'freeform_question',
|
|
235
|
+
questionId: id,
|
|
236
|
+
message: `Question ${id} type=${question.type || '(missing)'} is not noul|choice|score. Free-form judges are refused.`,
|
|
237
|
+
});
|
|
238
|
+
return errors;
|
|
239
|
+
}
|
|
240
|
+
if (!question.instructions) {
|
|
241
|
+
errors.push({ id: 'missing_instructions', questionId: id, message: `Question ${id} is missing instructions.` });
|
|
242
|
+
}
|
|
243
|
+
if (type === 'noul') {
|
|
244
|
+
if (actionForNoul(id, question) == null) {
|
|
245
|
+
errors.push({
|
|
246
|
+
id: 'noul_without_route_action',
|
|
247
|
+
questionId: id,
|
|
248
|
+
message: `Noul ${id} has no route action. Use a built-in hazard id or set action=block|review|pass.`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
const c = question.criteria || {};
|
|
252
|
+
if (c.true == null || c.false == null) {
|
|
253
|
+
errors.push({
|
|
254
|
+
id: 'noul_criteria_shape',
|
|
255
|
+
questionId: id,
|
|
256
|
+
message: `Noul ${id} needs criteria.true and criteria.false.`,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (type === 'choice') {
|
|
261
|
+
const c = question.criteria;
|
|
262
|
+
if (!c || typeof c !== 'object' || Array.isArray(c) || Object.keys(c).length < 2) {
|
|
263
|
+
errors.push({
|
|
264
|
+
id: 'choice_criteria_shape',
|
|
265
|
+
questionId: id,
|
|
266
|
+
message: `Choice ${id} needs a map of at least two options.`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (type === 'score') {
|
|
271
|
+
if (!Array.isArray(question.criteria) || question.criteria.length < 2) {
|
|
272
|
+
errors.push({
|
|
273
|
+
id: 'score_criteria_shape',
|
|
274
|
+
questionId: id,
|
|
275
|
+
message: `Score ${id} needs an ordered criteria array of at least two levels.`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return errors;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function parseBattery(raw) {
|
|
283
|
+
if (raw == null || raw === '') {
|
|
284
|
+
return { ok: true, battery: { ...DEFAULT_BATTERY }, error: null };
|
|
285
|
+
}
|
|
286
|
+
let data = raw;
|
|
287
|
+
if (typeof raw === 'string') {
|
|
288
|
+
try {
|
|
289
|
+
data = JSON.parse(raw);
|
|
290
|
+
} catch {
|
|
291
|
+
return { ok: false, battery: null, error: 'battery_parse_error' };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
295
|
+
return { ok: false, battery: null, error: 'battery_shape_error' };
|
|
296
|
+
}
|
|
297
|
+
const questions = data.questions && typeof data.questions === 'object' ? data.questions : data;
|
|
298
|
+
if (!questions || typeof questions !== 'object' || Array.isArray(questions) || Object.keys(questions).length === 0) {
|
|
299
|
+
return { ok: false, battery: null, error: 'empty_battery' };
|
|
300
|
+
}
|
|
301
|
+
return { ok: true, battery: questions, error: null };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function parsePayload(raw) {
|
|
305
|
+
if (raw == null || raw === '') return { ok: true, payload: {}, error: null };
|
|
306
|
+
if (typeof raw === 'object') return { ok: true, payload: raw, error: null };
|
|
307
|
+
try {
|
|
308
|
+
return { ok: true, payload: JSON.parse(raw), error: null };
|
|
309
|
+
} catch {
|
|
310
|
+
return { ok: true, payload: { command: String(raw) }, error: null };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function composeHazardFamily(nouls) {
|
|
315
|
+
if ((nouls.secret_exfil || 0) >= 0.7) return 'secret';
|
|
316
|
+
if ((nouls.guardrail_tamper || 0) >= 0.7) return 'tamper';
|
|
317
|
+
if ((nouls.clone_jev || 0) >= 0.7) return 'clone';
|
|
318
|
+
if ((nouls.outbound_send || 0) >= 0.7) return 'outbound';
|
|
319
|
+
if ((nouls.destructive || 0) >= 0.7) return 'destructive';
|
|
320
|
+
if ((nouls.destructive || 0) >= 0.35) return 'destructive';
|
|
321
|
+
return 'none';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function composeSeverity(nouls) {
|
|
325
|
+
if ((nouls.secret_exfil || 0) >= 0.7 || (nouls.guardrail_tamper || 0) >= 0.7) return 3;
|
|
326
|
+
if ((nouls.clone_jev || 0) >= 0.7 || (nouls.outbound_send || 0) >= 0.7 || (nouls.destructive || 0) >= 0.7) {
|
|
327
|
+
return 2;
|
|
328
|
+
}
|
|
329
|
+
const anyReview = Object.values(nouls).some((v) => Number(v) >= 0.35);
|
|
330
|
+
return anyReview ? 1 : 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function route({ nouls, severity, policy, battery }) {
|
|
334
|
+
const triggered = [];
|
|
335
|
+
for (const [hazard, probability] of Object.entries(nouls)) {
|
|
336
|
+
const action = actionForNoul(hazard, battery && battery[hazard]);
|
|
337
|
+
if (!action || action === 'pass') continue;
|
|
338
|
+
if (probability >= policy.actionThreshold) triggered.push(action);
|
|
339
|
+
else if (probability >= policy.reviewThreshold) triggered.push('review');
|
|
340
|
+
}
|
|
341
|
+
if (severity >= policy.severityBlock) {
|
|
342
|
+
for (let i = 0; i < triggered.length; i += 1) {
|
|
343
|
+
if (triggered[i] === 'review') triggered[i] = 'block';
|
|
344
|
+
}
|
|
345
|
+
if (triggered.length === 0 && severity >= policy.severityBlock) triggered.push('block');
|
|
346
|
+
}
|
|
347
|
+
return ROUTE_PRECEDENCE.find((action) => triggered.includes(action)) || 'pass';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function evaluateBattery({ battery, blob, flags, payload }) {
|
|
351
|
+
const nouls = {};
|
|
352
|
+
const answers = {};
|
|
353
|
+
const unevaluated = [];
|
|
354
|
+
|
|
355
|
+
for (const [id, question] of Object.entries(battery)) {
|
|
356
|
+
if (!question || typeof question !== 'object') continue;
|
|
357
|
+
const type = String(question.type || '').toLowerCase();
|
|
358
|
+
if (type === 'noul') {
|
|
359
|
+
let result;
|
|
360
|
+
if (flags.cloneJev && id === 'clone_jev') result = { noul: 1, band: 'action' };
|
|
361
|
+
else if (flags.useTypesafeApi && id === 'clone_jev') result = { noul: 1, band: 'action' };
|
|
362
|
+
else if (flags.llmAdjudicate && id === 'clone_jev') result = { noul: 1, band: 'action' };
|
|
363
|
+
else if (question.matcher || question.warnMatcher) {
|
|
364
|
+
result = noulFromMatchers(blob, question, payload);
|
|
365
|
+
} else if (DEFAULT_BATTERY[id] && DEFAULT_BATTERY[id].matcher) {
|
|
366
|
+
result = noulFromMatchers(blob, DEFAULT_BATTERY[id], payload);
|
|
367
|
+
} else {
|
|
368
|
+
unevaluated.push(id);
|
|
369
|
+
result = { noul: null, band: 'unevaluated' };
|
|
370
|
+
}
|
|
371
|
+
nouls[id] = result.noul == null ? 0 : result.noul;
|
|
372
|
+
answers[id] = {
|
|
373
|
+
type: 'noul',
|
|
374
|
+
noul: result.noul,
|
|
375
|
+
band: result.band,
|
|
376
|
+
source: 'deterministic',
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (flags.cloneJev || flags.useTypesafeApi || flags.llmAdjudicate || CLONE_RE.test(blob)) {
|
|
382
|
+
nouls.clone_jev = 1;
|
|
383
|
+
answers.clone_jev = {
|
|
384
|
+
type: 'noul',
|
|
385
|
+
noul: 1,
|
|
386
|
+
band: 'action',
|
|
387
|
+
source: 'deterministic',
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const family = composeHazardFamily(nouls);
|
|
392
|
+
const severity = composeSeverity(nouls);
|
|
393
|
+
|
|
394
|
+
if (battery.hazard_family && String(battery.hazard_family.type).toLowerCase() === 'choice') {
|
|
395
|
+
const options = Object.keys(battery.hazard_family.criteria || {});
|
|
396
|
+
const choice = options.includes(family) ? family : options[0] || family;
|
|
397
|
+
answers.hazard_family = {
|
|
398
|
+
type: 'choice',
|
|
399
|
+
choice,
|
|
400
|
+
confidence: 1,
|
|
401
|
+
source: 'code',
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (battery.severity && String(battery.severity.type).toLowerCase() === 'score') {
|
|
405
|
+
answers.severity = {
|
|
406
|
+
type: 'score',
|
|
407
|
+
score: severity,
|
|
408
|
+
confidence: 1,
|
|
409
|
+
source: 'code',
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return { nouls, answers, unevaluated, family, severity };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function normalizeOptions(raw = {}) {
|
|
417
|
+
const rootDir = path.resolve(String(raw.root || raw.rootDir || process.cwd()));
|
|
418
|
+
const payloadPath = raw.payload
|
|
419
|
+
? path.resolve(rootDir, String(raw.payload))
|
|
420
|
+
: raw.payloadPath
|
|
421
|
+
? path.resolve(rootDir, String(raw.payloadPath))
|
|
422
|
+
: null;
|
|
423
|
+
const batteryPath = raw.battery
|
|
424
|
+
? path.resolve(rootDir, String(raw.battery))
|
|
425
|
+
: raw.batteryPath
|
|
426
|
+
? path.resolve(rootDir, String(raw.batteryPath))
|
|
427
|
+
: null;
|
|
428
|
+
const policyName = String(raw.policy || 'strict').toLowerCase();
|
|
429
|
+
return {
|
|
430
|
+
rootDir,
|
|
431
|
+
payloadPath,
|
|
432
|
+
batteryPath,
|
|
433
|
+
payloadText: raw.payloadText != null ? String(raw.payloadText) : null,
|
|
434
|
+
batteryText: raw.batteryText != null ? String(raw.batteryText) : null,
|
|
435
|
+
toolName: raw['tool-name'] || raw.toolName || null,
|
|
436
|
+
command: raw.command || null,
|
|
437
|
+
policyName: POLICIES[policyName] ? policyName : 'strict',
|
|
438
|
+
mapOnly: normalizeBoolean(raw['map-only'] || raw.mapOnly),
|
|
439
|
+
claimReady: normalizeBoolean(raw['claim-ready'] || raw.claimReady),
|
|
440
|
+
cloneJev: normalizeBoolean(raw['clone-jev'] || raw.cloneJev),
|
|
441
|
+
useTypesafeApi: normalizeBoolean(raw['use-typesafe-api'] || raw.useTypesafeApi),
|
|
442
|
+
llmAdjudicate: normalizeBoolean(raw['llm-adjudicate'] || raw.llmAdjudicate),
|
|
443
|
+
modelEmittedVerdict: raw['model-emitted-verdict'] || raw.modelEmittedVerdict || null,
|
|
444
|
+
strict: normalizeBoolean(raw.strict),
|
|
445
|
+
json: normalizeBoolean(raw.json),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function buildFindings({
|
|
450
|
+
batteryParse,
|
|
451
|
+
payloadParse,
|
|
452
|
+
questionErrors,
|
|
453
|
+
unevaluated,
|
|
454
|
+
flags,
|
|
455
|
+
blob,
|
|
456
|
+
answers,
|
|
457
|
+
}) {
|
|
458
|
+
const findings = [];
|
|
459
|
+
if (!batteryParse.ok) {
|
|
460
|
+
findings.push({
|
|
461
|
+
id: batteryParse.error || 'battery_error',
|
|
462
|
+
severity: 'fail',
|
|
463
|
+
gateId: 'require-typed-pretool-questions',
|
|
464
|
+
message: `Battery failed: ${batteryParse.error}.`,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
if (!payloadParse.ok) {
|
|
468
|
+
findings.push({
|
|
469
|
+
id: payloadParse.error || 'payload_error',
|
|
470
|
+
severity: 'fail',
|
|
471
|
+
gateId: 'require-typed-pretool-questions',
|
|
472
|
+
message: `Payload failed: ${payloadParse.error}.`,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
for (const err of questionErrors) {
|
|
476
|
+
findings.push({
|
|
477
|
+
id: err.id,
|
|
478
|
+
severity: 'fail',
|
|
479
|
+
gateId: 'require-typed-pretool-questions',
|
|
480
|
+
questionId: err.questionId,
|
|
481
|
+
message: err.message,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
for (const id of unevaluated) {
|
|
485
|
+
findings.push({
|
|
486
|
+
id: 'unevaluated_question',
|
|
487
|
+
severity: 'fail',
|
|
488
|
+
gateId: 'require-code-owned-route',
|
|
489
|
+
questionId: id,
|
|
490
|
+
message: `Question ${id} has no matcher and no deterministic evaluator. Do not call Jev to fill it.`,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
if (flags.cloneJev) {
|
|
494
|
+
findings.push({
|
|
495
|
+
id: 'jev_sku_clone',
|
|
496
|
+
severity: 'fail',
|
|
497
|
+
gateId: 'require-code-owned-route',
|
|
498
|
+
message: 'Refused --clone-jev. TypeSafe/Jev is FORMAT only; do not clone System One as a ThumbGate SKU.',
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
if (flags.useTypesafeApi) {
|
|
502
|
+
findings.push({
|
|
503
|
+
id: 'typesafe_api_refused',
|
|
504
|
+
severity: 'fail',
|
|
505
|
+
gateId: 'require-code-owned-route',
|
|
506
|
+
message: 'Refused --use-typesafe-api. Do not call api.typesafe.ai from PreToolUse (ECI; LLM adjudicator parked).',
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
if (flags.llmAdjudicate) {
|
|
510
|
+
findings.push({
|
|
511
|
+
id: 'llm_adjudicator_parked',
|
|
512
|
+
severity: 'fail',
|
|
513
|
+
gateId: 'require-code-owned-route',
|
|
514
|
+
message: 'Refused --llm-adjudicate. Issues #3690/#3687 stay parked. Deterministic typed questions only.',
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
if (CLONE_RE.test(blob)) {
|
|
518
|
+
findings.push({
|
|
519
|
+
id: 'typesafe_clone_signal',
|
|
520
|
+
severity: 'fail',
|
|
521
|
+
gateId: 'require-code-owned-route',
|
|
522
|
+
message: 'Payload asks to install typesafe-sdk, call api.typesafe.ai, clone Jev, or wire an LLM adjudicator.',
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
if (flags.modelEmittedVerdict) {
|
|
526
|
+
findings.push({
|
|
527
|
+
id: 'model_emitted_verdict',
|
|
528
|
+
severity: 'fail',
|
|
529
|
+
gateId: 'require-code-owned-route',
|
|
530
|
+
message: `Refused model-emitted verdict=${flags.modelEmittedVerdict}. Code owns route(); the model does not.`,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (answers.hazard_family && answers.hazard_family.source !== 'code') {
|
|
534
|
+
findings.push({
|
|
535
|
+
id: 'choice_not_composed_in_code',
|
|
536
|
+
severity: 'fail',
|
|
537
|
+
gateId: 'require-code-owned-route',
|
|
538
|
+
message: 'hazard_family must be composed in code from noul answers.',
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
return findings;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function buildTypesafeTypedQuestionsReport(rawOptions = {}) {
|
|
545
|
+
const options = normalizeOptions(rawOptions);
|
|
546
|
+
const ioErrors = [];
|
|
547
|
+
|
|
548
|
+
let batteryRaw = options.batteryText;
|
|
549
|
+
if (batteryRaw == null && options.batteryPath) {
|
|
550
|
+
try {
|
|
551
|
+
batteryRaw = readText(options.batteryPath);
|
|
552
|
+
} catch (err) {
|
|
553
|
+
ioErrors.push({ id: 'battery_read_error', message: err.message });
|
|
554
|
+
batteryRaw = '';
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let payloadRaw = options.payloadText;
|
|
559
|
+
if (payloadRaw == null && options.payloadPath) {
|
|
560
|
+
try {
|
|
561
|
+
payloadRaw = readText(options.payloadPath);
|
|
562
|
+
} catch (err) {
|
|
563
|
+
ioErrors.push({ id: 'payload_read_error', message: err.message });
|
|
564
|
+
payloadRaw = '';
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const batteryParse = parseBattery(batteryRaw);
|
|
569
|
+
const payloadParse = parsePayload(payloadRaw);
|
|
570
|
+
const payload = payloadParse.payload || {};
|
|
571
|
+
if (options.toolName) payload.tool_name = options.toolName;
|
|
572
|
+
if (options.command) {
|
|
573
|
+
payload.tool_input = { ...(payload.tool_input || {}), command: options.command };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const battery = batteryParse.ok ? batteryParse.battery : { ...DEFAULT_BATTERY };
|
|
577
|
+
const questionErrors = [];
|
|
578
|
+
if (batteryParse.ok) {
|
|
579
|
+
for (const [id, question] of Object.entries(battery)) {
|
|
580
|
+
questionErrors.push(...validateQuestion(id, question));
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const blob = flattenState(payload);
|
|
585
|
+
const flags = {
|
|
586
|
+
cloneJev: options.cloneJev,
|
|
587
|
+
useTypesafeApi: options.useTypesafeApi,
|
|
588
|
+
llmAdjudicate: options.llmAdjudicate,
|
|
589
|
+
modelEmittedVerdict: options.modelEmittedVerdict,
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const evaluated = options.mapOnly
|
|
593
|
+
? { nouls: {}, answers: {}, unevaluated: [], family: 'none', severity: 0 }
|
|
594
|
+
: evaluateBattery({ battery, blob, flags, payload });
|
|
595
|
+
|
|
596
|
+
const policy = POLICIES[options.policyName];
|
|
597
|
+
const composedRoute = options.mapOnly
|
|
598
|
+
? 'pass'
|
|
599
|
+
: route({
|
|
600
|
+
nouls: evaluated.nouls,
|
|
601
|
+
severity: evaluated.severity,
|
|
602
|
+
policy,
|
|
603
|
+
battery,
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
const findings = [
|
|
607
|
+
...ioErrors.map((e) => ({
|
|
608
|
+
id: e.id,
|
|
609
|
+
severity: 'fail',
|
|
610
|
+
gateId: 'require-typed-pretool-questions',
|
|
611
|
+
message: e.message,
|
|
612
|
+
})),
|
|
613
|
+
...buildFindings({
|
|
614
|
+
batteryParse,
|
|
615
|
+
payloadParse,
|
|
616
|
+
questionErrors,
|
|
617
|
+
unevaluated: evaluated.unevaluated,
|
|
618
|
+
flags,
|
|
619
|
+
blob,
|
|
620
|
+
answers: evaluated.answers,
|
|
621
|
+
}),
|
|
622
|
+
];
|
|
623
|
+
|
|
624
|
+
if (options.claimReady && findings.some((f) => f.severity === 'fail')) {
|
|
625
|
+
findings.push({
|
|
626
|
+
id: 'claim_without_code_owned_route',
|
|
627
|
+
severity: 'fail',
|
|
628
|
+
gateId: 'require-code-owned-route',
|
|
629
|
+
message: 'Claimed typed-question PreToolUse ready while clone/API/LLM-adjudicator or untyped questions remain.',
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const seen = new Set();
|
|
634
|
+
const deduped = [];
|
|
635
|
+
for (const f of findings) {
|
|
636
|
+
const key = `${f.id}:${f.questionId || ''}`;
|
|
637
|
+
if (seen.has(key)) continue;
|
|
638
|
+
seen.add(key);
|
|
639
|
+
deduped.push(f);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const failCount = deduped.filter((f) => f.severity === 'fail').length;
|
|
643
|
+
const warnCount = deduped.filter((f) => f.severity === 'warn').length;
|
|
644
|
+
let status = 'ready';
|
|
645
|
+
if (failCount > 0) status = 'fail';
|
|
646
|
+
else if (warnCount > 0) status = 'actionable';
|
|
647
|
+
|
|
648
|
+
const codeOwnsRoute = !flags.modelEmittedVerdict
|
|
649
|
+
&& !flags.useTypesafeApi
|
|
650
|
+
&& !flags.llmAdjudicate
|
|
651
|
+
&& !flags.cloneJev
|
|
652
|
+
&& evaluated.unevaluated.length === 0;
|
|
653
|
+
|
|
654
|
+
return {
|
|
655
|
+
name: 'thumbgate-typesafe-typed-questions',
|
|
656
|
+
ok: status !== 'fail',
|
|
657
|
+
status,
|
|
658
|
+
source: SOURCE_URLS[0],
|
|
659
|
+
sources: SOURCE_URLS,
|
|
660
|
+
disclaimer:
|
|
661
|
+
'FORMAT steal from TypeSafe (typed noul/choice/score + code-owned route + confidence as a second axis). Not affiliated with TypeSafe. Does not install typesafe-sdk, call Jev, or wire an LLM adjudicator.',
|
|
662
|
+
rootDir: options.rootDir,
|
|
663
|
+
policy: options.policyName,
|
|
664
|
+
codeOwnsRoute,
|
|
665
|
+
clonedJev: Boolean(flags.cloneJev || flags.useTypesafeApi || CLONE_RE.test(blob)),
|
|
666
|
+
llmAdjudicatorParked: true,
|
|
667
|
+
map: options.mapOnly ? RAIL_MAP : undefined,
|
|
668
|
+
metrics: {
|
|
669
|
+
payloadPath: options.payloadPath,
|
|
670
|
+
batteryPath: options.batteryPath,
|
|
671
|
+
questionCount: Object.values(battery).filter((q) => q && typeof q === 'object').length,
|
|
672
|
+
noulCount: Object.values(battery).filter((q) => q && String(q.type).toLowerCase() === 'noul').length,
|
|
673
|
+
choiceCount: Object.values(battery).filter((q) => q && String(q.type).toLowerCase() === 'choice').length,
|
|
674
|
+
scoreCount: Object.values(battery).filter((q) => q && String(q.type).toLowerCase() === 'score').length,
|
|
675
|
+
mapOnly: options.mapOnly,
|
|
676
|
+
claimReady: options.claimReady,
|
|
677
|
+
hazardFamily: evaluated.family,
|
|
678
|
+
severity: evaluated.severity,
|
|
679
|
+
},
|
|
680
|
+
answers: evaluated.answers,
|
|
681
|
+
route: composedRoute,
|
|
682
|
+
findings: deduped,
|
|
683
|
+
summary: {
|
|
684
|
+
failCount,
|
|
685
|
+
warnCount,
|
|
686
|
+
findingCount: deduped.length,
|
|
687
|
+
recommendedGateCount: [...new Set(deduped.map((f) => f.gateId).filter(Boolean))].length,
|
|
688
|
+
},
|
|
689
|
+
recommendedGates: [...new Set(deduped.map((f) => f.gateId).filter(Boolean))],
|
|
690
|
+
nextActions: [
|
|
691
|
+
'Ask independent noul/choice/score questions over the same PreToolUse state.',
|
|
692
|
+
'Compose pass|review|block in code from those answers — never let a model emit the verdict.',
|
|
693
|
+
'Treat confidence as a second axis: warn-level → review; high-stakes + severity ≥ 2 → block.',
|
|
694
|
+
'Do not install typesafe-sdk, call api.typesafe.ai, clone Jev, or unpark the LLM adjudicator.',
|
|
695
|
+
'Pair with gates require-typed-pretool-questions and require-code-owned-route.',
|
|
696
|
+
],
|
|
697
|
+
exampleCommand:
|
|
698
|
+
'npx thumbgate typesafe-typed-questions --tool-name=Bash --command="git push --force origin main" --json',
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function formatTypesafeTypedQuestionsReport(report) {
|
|
703
|
+
const lines = [
|
|
704
|
+
'',
|
|
705
|
+
'ThumbGate TypeSafe Typed-Questions Doctor',
|
|
706
|
+
'-'.repeat(48),
|
|
707
|
+
`Status : ${report.status}`,
|
|
708
|
+
`Route : ${report.route}`,
|
|
709
|
+
`Policy : ${report.policy}`,
|
|
710
|
+
`Code owns route : ${report.codeOwnsRoute}`,
|
|
711
|
+
`Questions: ${report.metrics.questionCount} (noul=${report.metrics.noulCount}, choice=${report.metrics.choiceCount}, score=${report.metrics.scoreCount})`,
|
|
712
|
+
`Family : ${report.metrics.hazardFamily} severity=${report.metrics.severity}`,
|
|
713
|
+
`Findings : ${report.summary.findingCount} (fail=${report.summary.failCount}, warn=${report.summary.warnCount})`,
|
|
714
|
+
`Source : ${report.source}`,
|
|
715
|
+
];
|
|
716
|
+
if (report.map) {
|
|
717
|
+
lines.push('', 'Rail map:');
|
|
718
|
+
for (const row of report.map) {
|
|
719
|
+
lines.push(` - ${row.typesafe} → ${row.thumbgate}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (report.findings.length) {
|
|
723
|
+
lines.push('', 'Findings:');
|
|
724
|
+
for (const f of report.findings) {
|
|
725
|
+
const gate = f.gateId ? ` [${f.gateId}]` : '';
|
|
726
|
+
const q = f.questionId ? ` ${f.questionId}` : '';
|
|
727
|
+
lines.push(` - [${f.severity}] ${f.id}${q}${gate}`);
|
|
728
|
+
lines.push(` ${f.message}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
lines.push('', 'Next actions:');
|
|
732
|
+
for (const a of report.nextActions) lines.push(` - ${a}`);
|
|
733
|
+
lines.push('', `Example: ${report.exampleCommand}`);
|
|
734
|
+
lines.push(`Note: ${report.disclaimer}`, '');
|
|
735
|
+
return `${lines.join('\n')}\n`;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function parseCliArgs(argv) {
|
|
739
|
+
const options = {};
|
|
740
|
+
for (const arg of argv) {
|
|
741
|
+
if (arg === '--json') { options.json = true; continue; }
|
|
742
|
+
if (arg === '--strict') { options.strict = true; continue; }
|
|
743
|
+
if (arg === '--map-only') { options['map-only'] = true; continue; }
|
|
744
|
+
if (arg === '--claim-ready') { options['claim-ready'] = true; continue; }
|
|
745
|
+
if (arg === '--clone-jev') { options['clone-jev'] = true; continue; }
|
|
746
|
+
if (arg === '--use-typesafe-api') { options['use-typesafe-api'] = true; continue; }
|
|
747
|
+
if (arg === '--llm-adjudicate') { options['llm-adjudicate'] = true; continue; }
|
|
748
|
+
if (arg === '--help' || arg === '-h') { options.help = true; continue; }
|
|
749
|
+
const m = /^--([^=]+)(?:=(.*))?$/.exec(arg);
|
|
750
|
+
if (!m) continue;
|
|
751
|
+
options[m[1]] = m[2] === undefined ? true : m[2];
|
|
752
|
+
}
|
|
753
|
+
return options;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function printHelp() {
|
|
757
|
+
process.stdout.write(`Usage: node scripts/typesafe-typed-questions.js [flags]
|
|
758
|
+
|
|
759
|
+
Flags:
|
|
760
|
+
--payload=PATH PreToolUse JSON payload
|
|
761
|
+
--battery=PATH Typed questions JSON (noul|choice|score)
|
|
762
|
+
--tool-name=NAME Shortcut tool_name
|
|
763
|
+
--command=TEXT Shortcut tool_input.command
|
|
764
|
+
--policy=strict|permissive
|
|
765
|
+
--map-only Print TypeSafe → ThumbGate rail map
|
|
766
|
+
--claim-ready Fail unless code owns the route
|
|
767
|
+
--clone-jev Always fail (SKU clone)
|
|
768
|
+
--use-typesafe-api Always fail (do not call Jev)
|
|
769
|
+
--llm-adjudicate Always fail (#3690/#3687 parked)
|
|
770
|
+
--model-emitted-verdict=V Always fail (code must own route)
|
|
771
|
+
--root=DIR Repo root for relative paths
|
|
772
|
+
--strict Exit 1 on fail/actionable
|
|
773
|
+
--json
|
|
774
|
+
|
|
775
|
+
Sources: ${SOURCE_URLS.join(' ')}
|
|
776
|
+
`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function runCli(argv = process.argv.slice(2)) {
|
|
780
|
+
const args = parseCliArgs(argv);
|
|
781
|
+
if (args.help) {
|
|
782
|
+
printHelp();
|
|
783
|
+
return 0;
|
|
784
|
+
}
|
|
785
|
+
const report = buildTypesafeTypedQuestionsReport(args);
|
|
786
|
+
if (args.json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
787
|
+
else process.stdout.write(formatTypesafeTypedQuestionsReport(report));
|
|
788
|
+
if (args.strict && report.status !== 'ready') return 1;
|
|
789
|
+
if (report.status === 'fail') return 1;
|
|
790
|
+
return 0;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
module.exports = {
|
|
794
|
+
SOURCE_URLS,
|
|
795
|
+
DEFAULT_BATTERY,
|
|
796
|
+
POLICIES,
|
|
797
|
+
RAIL_MAP,
|
|
798
|
+
flattenState,
|
|
799
|
+
parseBattery,
|
|
800
|
+
validateQuestion,
|
|
801
|
+
noulFromMatchers,
|
|
802
|
+
actionForNoul,
|
|
803
|
+
composeHazardFamily,
|
|
804
|
+
composeSeverity,
|
|
805
|
+
route,
|
|
806
|
+
buildTypesafeTypedQuestionsReport,
|
|
807
|
+
formatTypesafeTypedQuestionsReport,
|
|
808
|
+
runCli,
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
|
|
812
|
+
process.exitCode = runCli();
|
|
813
|
+
}
|